Merge branch 'release/v2.0.0' into 'main'
Release/v2.0.0 See merge request leon.hoppe/HopFrame!13
This commit is contained in:
34
.gitlab-ci.yml
Normal file
34
.gitlab-ci.yml
Normal file
@@ -0,0 +1,34 @@
|
||||
image: mcr.microsoft.com/dotnet/sdk:8.0
|
||||
|
||||
stages:
|
||||
- build
|
||||
- test
|
||||
- publish
|
||||
|
||||
before_script:
|
||||
- echo "Setting up environment"
|
||||
- 'dotnet --version'
|
||||
|
||||
build:
|
||||
stage: build
|
||||
script:
|
||||
- dotnet restore
|
||||
- dotnet build --configuration Release --no-restore
|
||||
artifacts:
|
||||
paths:
|
||||
- "**/bin/Release"
|
||||
|
||||
test:
|
||||
stage: test
|
||||
script:
|
||||
- dotnet test --no-restore --verbosity normal
|
||||
|
||||
publish:
|
||||
stage: publish
|
||||
script:
|
||||
- dotnet pack -c Release -o .
|
||||
- for nupkg in *.nupkg; do dotnet nuget push $nupkg -k $NUGET_API_KEY -s https://api.nuget.org/v3/index.json; done
|
||||
only:
|
||||
- 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:$PROJECT_DIR$/RestApiTest/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>
|
||||
|
||||
1025
.idea/config/applicationhost.config
generated
Normal file
1025
.idea/config/applicationhost.config
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Web", "src\HopFram
|
||||
EndProject
|
||||
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
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -42,6 +44,10 @@ Global
|
||||
{8F983A37-63CF-48D5-988D-58B78EF8AECD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8F983A37-63CF-48D5-988D-58B78EF8AECD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8F983A37-63CF-48D5-988D-58B78EF8AECD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
EndGlobalSection
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<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_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_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_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" />
|
||||
|
||||
@@ -5,13 +5,15 @@ A simple backend management api for ASP.NET Core Web APIs
|
||||
- [x] Database management
|
||||
- [x] User authentication
|
||||
- [x] Permission management
|
||||
- [x] Frontend dashboards
|
||||
- [x] Generated frontend administration boards
|
||||
|
||||
# Usage
|
||||
There are two different versions of HopFrame, either the Web API version or the full Blazor web version.
|
||||
|
||||
## Ho to use the Web API version
|
||||
|
||||
> **Hint:** For more information about the HopFrame installation and usage go to the [docs](./docs).
|
||||
|
||||
1. Add the HopFrame.Api library to your project:
|
||||
|
||||
```
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
@startuml ApiModels
|
||||
|
||||
namespace HopFrame.Security {
|
||||
class UserLogin {
|
||||
+Email: string
|
||||
+Password: string
|
||||
}
|
||||
|
||||
class UserRegister {
|
||||
+Username: string
|
||||
+Email: string
|
||||
+Password: string
|
||||
}
|
||||
}
|
||||
|
||||
namespace HopFrame.Api {
|
||||
class SingleValueResult<TValue> {
|
||||
+Value: TValue
|
||||
}
|
||||
|
||||
class UserPasswordValidation {
|
||||
+Password: string
|
||||
}
|
||||
}
|
||||
|
||||
@enduml
|
||||
@@ -1,37 +0,0 @@
|
||||
@startuml BaseModels
|
||||
set namespaceSeparator none
|
||||
|
||||
namespace HopFrame.Database {
|
||||
class User {
|
||||
+Id: Guid
|
||||
+Username: string
|
||||
+Email: string
|
||||
+CreatedAt: DateTime
|
||||
+Permissions: IList<Permission>
|
||||
}
|
||||
|
||||
class Permission {
|
||||
+Id: long
|
||||
+PermissionName: string
|
||||
+Owner: Guid
|
||||
+GrantedAt: DateTime
|
||||
}
|
||||
|
||||
class PermissionGroup {
|
||||
+Name: string
|
||||
+IsDefaultGroup: bool
|
||||
+Description: string
|
||||
+CreatedAt: DateTime
|
||||
+Permissions: IList<Permission>
|
||||
}
|
||||
|
||||
interface IPermissionOwner {}
|
||||
}
|
||||
|
||||
IPermissionOwner <|-- User
|
||||
IPermissionOwner <|-- PermissionGroup
|
||||
|
||||
User .. Permission
|
||||
PermissionGroup .. Permission
|
||||
|
||||
@enduml
|
||||
@@ -1,38 +0,0 @@
|
||||
@startuml DatabaseModels
|
||||
set namespaceSeparator none
|
||||
|
||||
namespace HopFrame.Database {
|
||||
class UserEntry {
|
||||
+Id: string
|
||||
+Username: string
|
||||
+Email: string
|
||||
+Password: string
|
||||
+CreatedAt: DateTime
|
||||
}
|
||||
|
||||
class TokenEntry {
|
||||
+Type: int
|
||||
+Token: string
|
||||
+UserId: string
|
||||
+CreatedAt: DateTime
|
||||
}
|
||||
|
||||
class PermissionEntry {
|
||||
+RecordId: long
|
||||
+PermissionText: string
|
||||
+UserId: string
|
||||
+GrantedAt: DateTime
|
||||
}
|
||||
|
||||
class GroupEntry {
|
||||
+Name: string
|
||||
+Default: bool
|
||||
+Description: string
|
||||
+CreatedAt: DateTime
|
||||
}
|
||||
}
|
||||
|
||||
UserEntry *-- TokenEntry
|
||||
UserEntry *-- PermissionEntry
|
||||
|
||||
@enduml
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 12 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 13 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 14 KiB |
@@ -1,13 +0,0 @@
|
||||
# HopFrame documentation
|
||||
These sides contain all documentation available for the HopFrame modules
|
||||
|
||||
## Content
|
||||
| Topic | Description | Document |
|
||||
|----------|------------------------------------------------|-----------------------|
|
||||
| Models | All models used by the HopFrame | [link](./models.md) |
|
||||
| Services | All services provided by the HopFrame | [link](./services.md) |
|
||||
| Usage | How to properly implement the HopFrame modules | [link](./usage.md) |
|
||||
|
||||
## Dependencies
|
||||
Both the HopFrame.Api and HopFrame.Web modules are dependent on the HopFrame.Database and HopFrame.Security modules.
|
||||
So all models and services provided by these modules are available in the other modules as well.
|
||||
30
docs/api/authorization.md
Normal file
30
docs/api/authorization.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# HopFrame Authentication
|
||||
|
||||
With the provided HopFrame services, you can secure your endpoints so that only logged-in users or users with the right permissions can access the endpoint.
|
||||
|
||||
## Usage
|
||||
You can secure your endpoints by adding the `Authorized` attribute.
|
||||
|
||||
```csharp
|
||||
// Everyone can access this endpoint
|
||||
[HttpGet("hello")]
|
||||
public ActionResult<string> HelloWorld() {
|
||||
return "Hello, World!";
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// Only logged-in users can access this endpoint
|
||||
[HttpGet("hello"), Authorized]
|
||||
public ActionResult<string> HelloWorld() {
|
||||
return "Hello, World!";
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// Only logged-in users with the specified permissions can access this endpoint
|
||||
[HttpGet("hello"), Authorized("test.permission", "test.permission.another")]
|
||||
public ActionResult<string> HelloWorld() {
|
||||
return "Hello, World!";
|
||||
}
|
||||
```
|
||||
21
docs/api/endpoints.md
Normal file
21
docs/api/endpoints.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# HopFrame Endpoints
|
||||
HopFrame currently only supports endpoints for authentication out of the box.
|
||||
|
||||
> **Hint:** with the help of the [repositories](../repositories.md) you can very easily create missing endpoints for HopFrame components yourself.
|
||||
|
||||
## All currently supported endpoints
|
||||
|
||||
> **Hint:** you can use the build-in [swagger](https://swagger.io/) ui to explore and test all endpoints of your application __including__ HopFrame endpoints.
|
||||
|
||||
### SecurityController
|
||||
Base endpoint: `/api/v1/authentication`\
|
||||
**Important:** All primitive data types (including `string`) are return as a [`SingleValueResult`](./models.md#SingleValueResult)
|
||||
|
||||
|
||||
| Method | Endpoint | Payload | Returns |
|
||||
|--------|---------------|--------------------------------------------------------------|-----------------------|
|
||||
| PUT | /login | [UserLogin](../models.md#UserLogin) | access token (string) |
|
||||
| POST | /register | [UserRegister](../models.md#UserRegister) | access token (string) |
|
||||
| GET | /authenticate | | access token (string) |
|
||||
| DELETE | /logout | | |
|
||||
| DELETE | /delete | [UserPasswordValidation](./models.md#UserPasswordValidation) | |
|
||||
16
docs/api/installation.md
Normal file
16
docs/api/installation.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Ho to use the Web API version
|
||||
This Installation adds all HopFrame [endpoints](./endpoints.md) and [repositories](../repositories.md) to the application.
|
||||
|
||||
1. Add the HopFrame.Api library to your project:
|
||||
|
||||
```
|
||||
dotnet add package HopFrame.Api
|
||||
```
|
||||
|
||||
2. Create a [DbContext](../database.md) that inherits the ``HopDbContext`` and add a data source
|
||||
|
||||
3. Add the HopFrame services to your application, provide the previously created `DatabaseContext` that inherits from `HopDbContextBase`
|
||||
|
||||
```csharp
|
||||
builder.Services.AddHopFrame<DatabaseContext>();
|
||||
```
|
||||
33
docs/api/logicresults.md
Normal file
33
docs/api/logicresults.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# LogicResults
|
||||
LogicResults provide another layer of abstraction above the ActionResults.
|
||||
They help you sending the right `HttpStatusCode` with the right data.
|
||||
|
||||
## Usage
|
||||
1. Create an endpoint that returns an `ActionResult`:
|
||||
|
||||
```csharp
|
||||
[HttpGet("hello")]
|
||||
public ActionResult<string> Hello() {
|
||||
return new ActionResult<string>("Hello, World!");
|
||||
}
|
||||
```
|
||||
2. Now instead of directly returning the `ActionResult`, return a `LogicResult`
|
||||
|
||||
```csharp
|
||||
[HttpGet("hello")]
|
||||
public ActionResult<string> Hello() {
|
||||
return LogicResult<string>.Ok("Hello, World!");
|
||||
}
|
||||
```
|
||||
3. This allows you to very easily change the return type by simply calling the right function
|
||||
|
||||
```csharp
|
||||
[HttpGet("hello")]
|
||||
public ActionResult<string> Hello() {
|
||||
if (!Auth.IsLoggedIn)
|
||||
return LogicResult<string>.Forbidden();
|
||||
|
||||
return LogicResult<string>.Ok("Hello, World!");
|
||||
}
|
||||
```
|
||||
> **Hint:** You can also provide an error message for status codes that are not in the 200 range.
|
||||
16
docs/api/models.md
Normal file
16
docs/api/models.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# HopFrame Models
|
||||
All models used by the RestAPI are listed below
|
||||
|
||||
## SingleValueResult
|
||||
```csharp
|
||||
public struct SingleValueResult<TValue>(TValue value) {
|
||||
public TValue Value { get; set; } = value;
|
||||
}
|
||||
```
|
||||
|
||||
## UserPasswordValidation
|
||||
```csharp
|
||||
public sealed class UserPasswordValidation {
|
||||
public string Password { get; set; }
|
||||
}
|
||||
```
|
||||
144
docs/blazor/admin.md
Normal file
144
docs/blazor/admin.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# HopFrame Admin Pages
|
||||
Admin pages can be defined through a `AdminContext` similar to how a `DbContext` is defined. They generate administration pages like [`/administration/users`](./pages.md)
|
||||
simply by reading the structure of the provided model and optionally some additional configuration.
|
||||
|
||||
> **Fun fact:** The already existing pages `/administration/users` and `/administration/groups` are also generated using an internal `AdminContext`.
|
||||
|
||||
## Usage
|
||||
1. Create a class that inherits the `AdminPagesContext` base class
|
||||
|
||||
```csharp
|
||||
public class AdminContext : AdminPagesContext {
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
2. Add your admin pages as properties to the class
|
||||
|
||||
```csharp
|
||||
public class AdminContext : AdminPagesContext {
|
||||
|
||||
public AdminPage<Address> Addresses { get; set; }
|
||||
|
||||
public AdminPage<Employee> Employees { get; set; }
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
> **Hint:** you can specify the url of the admin page by adding the `AdminPageUrl` Attribute
|
||||
|
||||
3. **Optionally** you can further configure your pages in the `OnModelCreating` method
|
||||
|
||||
```csharp
|
||||
public class AdminContext : AdminPagesContext {
|
||||
|
||||
public AdminPage<Address> Addresses { get; set; }
|
||||
public AdminPage<Employee> Employees { get; set; }
|
||||
|
||||
public override void OnModelCreating(IAdminContextGenerator generator) {
|
||||
base.OnModelCreating(generator);
|
||||
|
||||
generator.Page<Employee>()
|
||||
.Property(e => e.Address)
|
||||
.IsSelector();
|
||||
|
||||
generator.Page<Address>()
|
||||
.Property(a => a.Employee)
|
||||
.Ignore();
|
||||
|
||||
generator.Page<Address>()
|
||||
.Property(a => a.AddressId)
|
||||
.IsSelector<Employee>()
|
||||
.Parser<Employee>((model, e) => model.AddressId = e.EmployeeId);
|
||||
|
||||
generator.Page<Employee>()
|
||||
.ConfigureRepository<EmployeeProvider>()
|
||||
.ListingProperty(e => e.Name);
|
||||
|
||||
generator.Page<Address>()
|
||||
.ConfigureRepository<AddressProvider>()
|
||||
.ListingProperty(a => a.City);
|
||||
}
|
||||
}
|
||||
```
|
||||
4. **Optionally** you can also add some of the following attributes to your classes / properties to further configure the admin pages:\
|
||||
\
|
||||
Attributes for classes and properties:
|
||||
|
||||
```csharp
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)]
|
||||
public sealed class AdminNameAttribute(string name) : Attribute {
|
||||
public string Name { get; set; } = name;
|
||||
}
|
||||
```
|
||||
|
||||
Attributes for classes:
|
||||
|
||||
```csharp
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class AdminButtonConfigAttribute(bool showCreateButton = true, bool showDeleteButton = true, bool showUpdateButton = true) : Attribute {
|
||||
public bool ShowCreateButton { get; set; } = showCreateButton;
|
||||
public bool ShowDeleteButton { get; set; } = showDeleteButton;
|
||||
public bool ShowUpdateButton { get; set; } = showUpdateButton;
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class AdminPermissionsAttribute(string view = null, string create = null, string update = null, string delete = null) : Attribute {
|
||||
public AdminPagePermissions Permissions { get; set; } = new() {
|
||||
Create = create,
|
||||
Update = update,
|
||||
Delete = delete,
|
||||
View = view
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class AdminDescriptionAttribute(string description) : Attribute {
|
||||
public string Description { get; set; } = description;
|
||||
}
|
||||
```
|
||||
|
||||
Attributes for properties:
|
||||
|
||||
```csharp
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class AdminHideValueAttribute : Attribute;
|
||||
```
|
||||
|
||||
```csharp
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class AdminIgnoreAttribute(bool onlyForListing = false) : Attribute {
|
||||
public bool OnlyForListing { get; set; } = onlyForListing;
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class AdminPrefixAttribute(string prefix) : Attribute {
|
||||
public string Prefix { get; set; } = prefix;
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class AdminUneditableAttribute : Attribute;
|
||||
```
|
||||
|
||||
```csharp
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class AdminUniqueAttribute : Attribute;
|
||||
```
|
||||
|
||||
```csharp
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class AdminUnsortableAttribute : Attribute;
|
||||
```
|
||||
|
||||
```csharp
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class ListingPropertyAttribute : Attribute;
|
||||
```
|
||||
20
docs/blazor/auth.md
Normal file
20
docs/blazor/auth.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Auth Service
|
||||
The `IAuthService` provides some useful methods to handle user authentication (login/register).
|
||||
|
||||
## Usage
|
||||
Simply define the `IAuthService` as a dependency
|
||||
|
||||
```csharp
|
||||
public interface IAuthService {
|
||||
Task Register(UserRegister register);
|
||||
Task<bool> Login(UserLogin login);
|
||||
Task Logout();
|
||||
|
||||
Task<Token> RefreshLogin();
|
||||
Task<bool> IsLoggedIn();
|
||||
}
|
||||
```
|
||||
## Automatically refresh user sessions
|
||||
1. Make sure you have implemented the `AuthMiddleware` how it's described in step 5 of the [installation](./installation.md).
|
||||
|
||||
2. After that, the access token of the user gets automatically refreshed as long as the refresh token is valid.
|
||||
20
docs/blazor/authorization.md
Normal file
20
docs/blazor/authorization.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# HopFrame Authentication
|
||||
|
||||
With the provided HopFrame services, you can secure your blazor pages so that only logged-in users or users with the right permissions can access the page.
|
||||
|
||||
## Usage
|
||||
You can secure your Blazor pages by using the `AuthorizedView` component.
|
||||
Everything placed inside this component will only be displayed if the authorization was successful.
|
||||
You can also redirect the user if the authorization fails by specifying a `RedirectIfUnauthorized` url.
|
||||
|
||||
```html
|
||||
<!-- You can either specify one 'Permission', multiple 'Permissions' or none if the user only needs to be logged-in -->
|
||||
<AuthorizedView Permission="test.permission">
|
||||
<p>This paragraph is only visible if the user is logged-in and has the required permission</p>
|
||||
</AuthorizedView>
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- This component will redirect the user to the login page if the user is unauthorized -->
|
||||
<AuthorizedView RedirectIfUnauthorized="/login" />
|
||||
```
|
||||
36
docs/blazor/installation.md
Normal file
36
docs/blazor/installation.md
Normal file
@@ -0,0 +1,36 @@
|
||||
## How to use the Blazor API
|
||||
This Installation adds all HopFrame [pages](./pages.md) and [repositories](../repositories.md) to the application.
|
||||
|
||||
1. Add the HopFrame.Web library to your project
|
||||
|
||||
```
|
||||
dotnet add package HopFrame.Web
|
||||
```
|
||||
|
||||
2. Create a [DbContext](../database.md) that inherits the ``HopDbContext`` and add a data source
|
||||
|
||||
3. Add the HopFrame services to your application, provide the previously created `DatabaseContext` that inherits from `HopDbContextBase`
|
||||
|
||||
```csharp
|
||||
builder.Services.AddHopFrame<DatabaseContext>();
|
||||
```
|
||||
|
||||
4. **Optional:** You can also add your [AdminContext](./admin.md)
|
||||
|
||||
```csharp
|
||||
builder.Services.AddAdminContext<AdminContext>();
|
||||
```
|
||||
|
||||
5. Add the authentication middleware to your app
|
||||
|
||||
```csharp
|
||||
app.UseMiddleware<AuthMiddleware>();
|
||||
```
|
||||
|
||||
6. Add the HopFrame pages to your Razor components
|
||||
|
||||
```csharp
|
||||
app.MapRazorComponents<App>()
|
||||
.AddHopFrameAdminPages()
|
||||
.AddInteractiveServerRenderMode();
|
||||
```
|
||||
14
docs/blazor/pages.md
Normal file
14
docs/blazor/pages.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# HopFrame Pages
|
||||
By default, the HopFrame provides some blazor pages for managing user accounts and permissions
|
||||
|
||||
## All currently supported blazor pages
|
||||
|
||||
| Page | Endpoint | Permission | Usage |
|
||||
|-----------------|------------------------|----------------------------|--------------------------------------------------------------------------------------------------------|
|
||||
| Admin Dashboard | /administration | hopframe.admin | This page provides an overview to all admin pages built-in and created by [AdminContexts](./admin.md). |
|
||||
| Admin Login | /administration/login | | This page is a simple login screen so no login screen needs to be created to access the admin pages. |
|
||||
| User Dashboard | /administration/users | hopframe.admin.users.view | This page serves as a management site for all users and their permissions. |
|
||||
| Group Dashboard | /administration/groups | hopframe.admin.groups.view | This page serves as a management site for all groups and their permissions. |
|
||||
|
||||
> **Hint:** All pages created by [AdminContexts](./admin.md) are also under the `/administration/` location. This can unfortunately __not__ be changed at the moment.
|
||||
|
||||
35
docs/database.md
Normal file
35
docs/database.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Database initialization
|
||||
You also need to initialize the data source with the tables from HopFrame.
|
||||
|
||||
## Create a DbContext
|
||||
|
||||
1. Create a c# class that inherits from the `HopDbContextBase` and add a data source (In the example Sqlite is used)\
|
||||
**IMPORTANT:** You need to leave the `base.OnConfiguring(optionsBuilder)` in place so the HopFrame model relations are set correctly.
|
||||
|
||||
```csharp
|
||||
public class DatabaseContext : HopDbContextBase {
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
|
||||
base.OnConfiguring(optionsBuilder);
|
||||
|
||||
optionsBuilder.UseSqlite("...");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. Register the `DatabaseContext` as a service
|
||||
|
||||
```csharp
|
||||
builder.Services.AddDbContext<DatabaseContext>();
|
||||
```
|
||||
|
||||
3. Create a database migration
|
||||
|
||||
```bash
|
||||
dotnet ef migrations add Initial
|
||||
```
|
||||
|
||||
4. Apply the migration to the data source
|
||||
|
||||
```bash
|
||||
dotnet ef database update
|
||||
```
|
||||
@@ -1,21 +1,71 @@
|
||||
# Models for HopFrame
|
||||
# HopFrame base models
|
||||
All models listed below are part of the core HopFrame components and accessible in all installation variations
|
||||
|
||||
This page shows all models that HopFrame uses.
|
||||
> **Note:** All properties of the models that are `virtual` are relational properties and don't directly correspond to columns in the database.
|
||||
|
||||
## User
|
||||
```csharp
|
||||
public class User : IPermissionOwner {
|
||||
public Guid Id { get; init; }
|
||||
public string Username { get; set; }
|
||||
public string Email { get; set; }
|
||||
public string Password { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public virtual List<Permission> Permissions { get; set; }
|
||||
public virtual List<Token> Tokens { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
## Base Models
|
||||
These are the models used by the various database services.
|
||||
## PermissionGroup
|
||||
```csharp
|
||||
public class PermissionGroup : IPermissionOwner {
|
||||
public string Name { get; init; }
|
||||
public bool IsDefaultGroup { get; set; }
|
||||
public string Description { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public virtual List<Permission> Permissions { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||

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

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

|
||||
## IPermissionOwner
|
||||
```csharp
|
||||
public interface IPermissionOwner;
|
||||
```
|
||||
|
||||
25
docs/readme.md
Normal file
25
docs/readme.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# HopFrame Documentation
|
||||
|
||||
The HopFrame comes in two variations, you can eiter only use the backend with some basic endpoints or with fully fledged blazor pages for managing HopFrame components.
|
||||
|
||||
## Shared HopFrame Modules
|
||||
|
||||
- [Database](./database.md)
|
||||
- [Repositories](./repositories.md)
|
||||
- [Base Models](./models.md)
|
||||
|
||||
## HopFrame Web API
|
||||
|
||||
- [Installation](./api/installation.md)
|
||||
- [Endpoints](./api/endpoints.md)
|
||||
- [Authorization](./api/authorization.md)
|
||||
- [Models](./api/models.md)
|
||||
- [LogicResults](./api/logicresults.md)
|
||||
|
||||
## HopFrame Blazor library
|
||||
|
||||
- [Installation](./blazor/installation.md)
|
||||
- [Pages](./blazor/pages.md)
|
||||
- [Authorization](./blazor/authorization.md)
|
||||
- [Auth Service](./blazor/auth.md)
|
||||
- [Admin Context](./blazor/admin.md)
|
||||
75
docs/repositories.md
Normal file
75
docs/repositories.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# HopFrame Repositories
|
||||
The HopFrame provies repositories for the various build in database models as an abstraction around the `HopDbContext` to ensure, that the data is proccessed and saved correctly.
|
||||
|
||||
## Overview
|
||||
The repositories can also be used by simply defining them as a dependency in your service / controller.
|
||||
|
||||
### User Repository
|
||||
|
||||
```csharp
|
||||
public interface IUserRepository {
|
||||
Task<IList<User>> GetUsers();
|
||||
|
||||
Task<User> GetUser(Guid userId);
|
||||
|
||||
Task<User> GetUserByEmail(string email);
|
||||
|
||||
Task<User> GetUserByUsername(string username);
|
||||
|
||||
Task<User> AddUser(User user);
|
||||
|
||||
Task UpdateUser(User user);
|
||||
|
||||
Task DeleteUser(User user);
|
||||
|
||||
Task<bool> CheckUserPassword(User user, string password);
|
||||
|
||||
Task ChangePassword(User user, string password);
|
||||
}
|
||||
```
|
||||
|
||||
### Group Repository
|
||||
|
||||
```csharp
|
||||
public interface IGroupRepository {
|
||||
Task<IList<PermissionGroup>> GetPermissionGroups();
|
||||
|
||||
Task<IList<PermissionGroup>> GetDefaultGroups();
|
||||
|
||||
Task<IList<PermissionGroup>> GetUserGroups(User user);
|
||||
|
||||
Task<PermissionGroup> GetPermissionGroup(string name);
|
||||
|
||||
Task EditPermissionGroup(PermissionGroup group);
|
||||
|
||||
Task<PermissionGroup> CreatePermissionGroup(PermissionGroup group);
|
||||
|
||||
Task DeletePermissionGroup(PermissionGroup group);
|
||||
}
|
||||
```
|
||||
|
||||
### Permission Repository
|
||||
|
||||
```csharp
|
||||
public interface IPermissionRepository {
|
||||
Task<bool> HasPermission(IPermissionOwner owner, params string[] permissions);
|
||||
|
||||
Task<Permission> AddPermission(IPermissionOwner owner, string permission);
|
||||
|
||||
Task RemovePermission(IPermissionOwner owner, string permission);
|
||||
|
||||
Task<IList<string>> GetFullPermissions(IPermissionOwner owner);
|
||||
}
|
||||
```
|
||||
|
||||
### Token Repository
|
||||
|
||||
```csharp
|
||||
public interface ITokenRepository {
|
||||
Task<Token> GetToken(string content);
|
||||
|
||||
Task<Token> CreateToken(int type, User owner);
|
||||
|
||||
Task DeleteUserTokens(User owner);
|
||||
}
|
||||
```
|
||||
145
docs/services.md
145
docs/services.md
@@ -1,145 +0,0 @@
|
||||
# HopFrame Services
|
||||
This page describes all services provided by the HopFrame.
|
||||
You can use these services by specifying them as a dependency. All of them are scoped dependencies.
|
||||
|
||||
## HopFrame.Security
|
||||
### ITokenContext
|
||||
This service provides the information given by the current request
|
||||
|
||||
```csharp
|
||||
public interface ITokenContext {
|
||||
bool IsAuthenticated { get; }
|
||||
|
||||
User User { get; }
|
||||
|
||||
Guid AccessToken { get; }
|
||||
}
|
||||
```
|
||||
|
||||
### IUserService
|
||||
This service simplifies the data access of the user table in the database.
|
||||
|
||||
```csharp
|
||||
public interface IUserService {
|
||||
Task<IList<User>> GetUsers();
|
||||
|
||||
Task<User> GetUser(Guid userId);
|
||||
|
||||
Task<User> GetUserByEmail(string email);
|
||||
|
||||
Task<User> GetUserByUsername(string username);
|
||||
|
||||
Task<User> AddUser(UserRegister user);
|
||||
|
||||
Task UpdateUser(User user);
|
||||
|
||||
Task DeleteUser(User user);
|
||||
|
||||
Task<bool> CheckUserPassword(User user, string password);
|
||||
|
||||
Task ChangePassword(User user, string password);
|
||||
}
|
||||
```
|
||||
|
||||
### IPermissionService
|
||||
This service handles all permission and group interactions with the data source.
|
||||
|
||||
```csharp
|
||||
public interface IPermissionService {
|
||||
Task<bool> HasPermission(string permission, Guid user);
|
||||
|
||||
Task<IList<PermissionGroup>> GetPermissionGroups();
|
||||
|
||||
Task<PermissionGroup> GetPermissionGroup(string name);
|
||||
|
||||
Task EditPermissionGroup(PermissionGroup group);
|
||||
|
||||
Task<IList<PermissionGroup>> GetUserPermissionGroups(User user);
|
||||
|
||||
Task RemoveGroupFromUser(User user, PermissionGroup group);
|
||||
|
||||
Task<PermissionGroup> CreatePermissionGroup(string name, bool isDefault = false, string description = null);
|
||||
|
||||
Task DeletePermissionGroup(PermissionGroup group);
|
||||
|
||||
Task<Permission> GetPermission(string name, IPermissionOwner owner);
|
||||
|
||||
Task AddPermission(IPermissionOwner owner, string permission);
|
||||
|
||||
Task RemovePermission(Permission permission);
|
||||
|
||||
Task<string[]> GetFullPermissions(string user);
|
||||
}
|
||||
```
|
||||
|
||||
## HopFrame.Api
|
||||
### LogicResult
|
||||
Logic result is an extension of the ActionResult for an ApiController. It provides simple Http status results with either a message or data by specifying the generic type.
|
||||
|
||||
```csharp
|
||||
public class LogicResult : ILogicResult {
|
||||
public static LogicResult Ok();
|
||||
|
||||
public static LogicResult BadRequest();
|
||||
|
||||
public static LogicResult BadRequest(string message);
|
||||
|
||||
public static LogicResult Forbidden();
|
||||
|
||||
public static LogicResult Forbidden(string message);
|
||||
|
||||
public static LogicResult NotFound();
|
||||
|
||||
public static LogicResult NotFound(string message);
|
||||
|
||||
public static LogicResult Conflict();
|
||||
|
||||
public static LogicResult Conflict(string message);
|
||||
|
||||
public static LogicResult Forward(LogicResult result);
|
||||
|
||||
public static LogicResult Forward<T>(ILogicResult<T> result);
|
||||
|
||||
public static implicit operator ActionResult(LogicResult v);
|
||||
}
|
||||
|
||||
public class LogicResult<T> : ILogicResult<T> {
|
||||
public static LogicResult<T> Ok();
|
||||
|
||||
public static LogicResult<T> Ok(T result);
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### IAuthLogic
|
||||
This service handles all logic needed to provide the authentication endpoints by using the LogicResults.
|
||||
|
||||
```csharp
|
||||
public interface IAuthLogic {
|
||||
Task<LogicResult<SingleValueResult<string>>> Login(UserLogin login);
|
||||
|
||||
Task<LogicResult<SingleValueResult<string>>> Register(UserRegister register);
|
||||
|
||||
Task<LogicResult<SingleValueResult<string>>> Authenticate();
|
||||
|
||||
Task<LogicResult> Logout();
|
||||
|
||||
Task<LogicResult> Delete(UserPasswordValidation validation);
|
||||
}
|
||||
```
|
||||
|
||||
## HopFrame.Web
|
||||
### IAuthService
|
||||
This service handles all the authentication like login or register. It properly creates all tokens so the user can be identified
|
||||
|
||||
```csharp
|
||||
public interface IAuthService {
|
||||
Task Register(UserRegister register);
|
||||
Task<bool> Login(UserLogin login);
|
||||
Task Logout();
|
||||
|
||||
Task<TokenEntry> RefreshLogin();
|
||||
Task<bool> IsLoggedIn();
|
||||
}
|
||||
```
|
||||
@@ -1,70 +0,0 @@
|
||||
# HopFrame Usage
|
||||
There are two different versions of HopFrame, either the Web API version or the full Blazor web version.
|
||||
|
||||
## Ho to use the Web API version
|
||||
|
||||
1. Add the HopFrame.Api library to your project:
|
||||
|
||||
```
|
||||
dotnet add package HopFrame.Api
|
||||
```
|
||||
|
||||
2. Create a DbContext that inherits the ``HopDbContext`` and add a data source
|
||||
|
||||
```csharp
|
||||
public class DatabaseContext : HopDbContextBase {
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
|
||||
base.OnConfiguring(optionsBuilder);
|
||||
|
||||
optionsBuilder.UseSqlite("...");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. Add the DbContext and HopFrame to your services
|
||||
|
||||
```csharp
|
||||
builder.Services.AddDbContext<DatabaseContext>();
|
||||
builder.Services.AddHopFrame<DatabaseContext>();
|
||||
```
|
||||
|
||||
## How to use the Blazor API
|
||||
|
||||
1. Add the HopFrame.Web library to your project
|
||||
|
||||
```
|
||||
dotnet add package HopFrame.Web
|
||||
```
|
||||
|
||||
2. Create a DbContext that inherits the ``HopDbContext`` and add a data source
|
||||
|
||||
```csharp
|
||||
public class DatabaseContext : HopDbContextBase {
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
|
||||
base.OnConfiguring(optionsBuilder);
|
||||
|
||||
optionsBuilder.UseSqlite("...");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. Add the DbContext and HopFrame to your services
|
||||
|
||||
```csharp
|
||||
builder.Services.AddDbContext<DatabaseContext>();
|
||||
builder.Services.AddHopFrame<DatabaseContext>();
|
||||
```
|
||||
|
||||
4. Add the authentication middleware to your app
|
||||
|
||||
```csharp
|
||||
app.UseMiddleware<AuthMiddleware>();
|
||||
```
|
||||
|
||||
5. Add the HopFrame pages to your Razor components
|
||||
|
||||
```csharp
|
||||
app.MapRazorComponents<App>()
|
||||
.AddHopFrameAdminPages()
|
||||
.AddInteractiveServerRenderMode();
|
||||
```
|
||||
@@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
namespace HopFrame.Api.Controller;
|
||||
|
||||
[ApiController]
|
||||
[Route("authentication")]
|
||||
[Route("api/v1/authentication")]
|
||||
public class SecurityController(IAuthLogic auth) : ControllerBase {
|
||||
|
||||
[HttpPut("login")]
|
||||
|
||||
@@ -27,10 +27,11 @@ public static class ServiceCollectionExtensions {
|
||||
/// <param name="services">The service provider to add the services to</param>
|
||||
/// <typeparam name="TDbContext">The data source for all HopFrame entities</typeparam>
|
||||
public static void AddHopFrameNoEndpoints<TDbContext>(this IServiceCollection services) where TDbContext : HopDbContextBase {
|
||||
services.AddHopFrameRepositories<TDbContext>();
|
||||
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||
services.AddScoped<IAuthLogic, AuthLogic<TDbContext>>();
|
||||
services.AddScoped<IAuthLogic, AuthLogic>();
|
||||
|
||||
services.AddHopFrameAuthentication<TDbContext>();
|
||||
services.AddHopFrameAuthentication();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<Nullable>disable</Nullable>
|
||||
|
||||
<PackageId>HopFrame.Api</PackageId>
|
||||
<Version>1.1.0</Version>
|
||||
<Version>2.0.0</Version>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<IsPackable>true</IsPackable>
|
||||
|
||||
@@ -8,9 +8,18 @@ public interface IAuthLogic {
|
||||
|
||||
Task<LogicResult<SingleValueResult<string>>> Register(UserRegister register);
|
||||
|
||||
/// <summary>
|
||||
/// Reassures that the user has a valid refresh token and generates a new access token
|
||||
/// </summary>
|
||||
/// <returns>The newly generated access token</returns>
|
||||
Task<LogicResult<SingleValueResult<string>>> Authenticate();
|
||||
|
||||
Task<LogicResult> Logout();
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the user account that called the endpoint if the provided password is correct
|
||||
/// </summary>
|
||||
/// <param name="validation">The password od the user</param>
|
||||
/// <returns></returns>
|
||||
Task<LogicResult> Delete(UserPasswordValidation validation);
|
||||
}
|
||||
@@ -1,16 +1,14 @@
|
||||
using HopFrame.Api.Models;
|
||||
using HopFrame.Database;
|
||||
using HopFrame.Database.Models.Entries;
|
||||
using HopFrame.Database.Models;
|
||||
using HopFrame.Database.Repositories;
|
||||
using HopFrame.Security.Authentication;
|
||||
using HopFrame.Security.Claims;
|
||||
using HopFrame.Security.Models;
|
||||
using HopFrame.Security.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace HopFrame.Api.Logic.Implementation;
|
||||
|
||||
public class AuthLogic<TDbContext>(TDbContext context, IUserService users, ITokenContext tokenContext, IHttpContextAccessor accessor) : IAuthLogic where TDbContext : HopDbContextBase {
|
||||
public class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenContext tokenContext, IHttpContextAccessor accessor) : IAuthLogic {
|
||||
|
||||
public async Task<LogicResult<SingleValueResult<string>>> Login(UserLogin login) {
|
||||
var user = await users.GetUserByEmail(login.Email);
|
||||
@@ -21,34 +19,21 @@ public class AuthLogic<TDbContext>(TDbContext context, IUserService users, IToke
|
||||
if (!await users.CheckUserPassword(user, login.Password))
|
||||
return LogicResult<SingleValueResult<string>>.Forbidden("The provided password is not correct");
|
||||
|
||||
var refreshToken = new TokenEntry {
|
||||
CreatedAt = DateTime.Now,
|
||||
Token = Guid.NewGuid().ToString(),
|
||||
Type = TokenEntry.RefreshTokenType,
|
||||
UserId = user.Id.ToString()
|
||||
};
|
||||
var accessToken = new TokenEntry {
|
||||
CreatedAt = DateTime.Now,
|
||||
Token = Guid.NewGuid().ToString(),
|
||||
Type = TokenEntry.AccessTokenType,
|
||||
UserId = user.Id.ToString()
|
||||
};
|
||||
var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user);
|
||||
var accessToken = await tokens.CreateToken(Token.AccessTokenType, user);
|
||||
|
||||
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Token, new CookieOptions {
|
||||
MaxAge = HopFrameAuthentication<TDbContext>.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.Token, new CookieOptions {
|
||||
MaxAge = HopFrameAuthentication<TDbContext>.AccessTokenTime,
|
||||
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
|
||||
MaxAge = HopFrameAuthentication.AccessTokenTime,
|
||||
HttpOnly = true,
|
||||
Secure = true
|
||||
});
|
||||
|
||||
await context.Tokens.AddRangeAsync(refreshToken, accessToken);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
return LogicResult<SingleValueResult<string>>.Ok(accessToken.Token);
|
||||
return LogicResult<SingleValueResult<string>>.Ok(accessToken.Content.ToString());
|
||||
}
|
||||
|
||||
public async Task<LogicResult<SingleValueResult<string>>> Register(UserRegister register) {
|
||||
@@ -59,36 +44,27 @@ public class AuthLogic<TDbContext>(TDbContext context, IUserService users, IToke
|
||||
if (allUsers.Any(user => user.Username == register.Username || user.Email == register.Email))
|
||||
return LogicResult<SingleValueResult<string>>.Conflict("Username or Email is already registered");
|
||||
|
||||
var user = await users.AddUser(register);
|
||||
var user = await users.AddUser(new User {
|
||||
Username = register.Username,
|
||||
Email = register.Email,
|
||||
Password = register.Password
|
||||
});
|
||||
|
||||
var refreshToken = new TokenEntry {
|
||||
CreatedAt = DateTime.Now,
|
||||
Token = Guid.NewGuid().ToString(),
|
||||
Type = TokenEntry.RefreshTokenType,
|
||||
UserId = user.Id.ToString()
|
||||
};
|
||||
var accessToken = new TokenEntry {
|
||||
CreatedAt = DateTime.Now,
|
||||
Token = Guid.NewGuid().ToString(),
|
||||
Type = TokenEntry.AccessTokenType,
|
||||
UserId = user.Id.ToString()
|
||||
};
|
||||
var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user);
|
||||
var accessToken = await tokens.CreateToken(Token.AccessTokenType, user);
|
||||
|
||||
await context.Tokens.AddRangeAsync(refreshToken, accessToken);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Token, new CookieOptions {
|
||||
MaxAge = HopFrameAuthentication<TDbContext>.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.Token, new CookieOptions {
|
||||
MaxAge = HopFrameAuthentication<TDbContext>.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.Token);
|
||||
return LogicResult<SingleValueResult<string>>.Ok(accessToken.Content.ToString());
|
||||
}
|
||||
|
||||
public async Task<LogicResult<SingleValueResult<string>>> Authenticate() {
|
||||
@@ -97,31 +73,26 @@ public class AuthLogic<TDbContext>(TDbContext context, IUserService users, IToke
|
||||
if (string.IsNullOrEmpty(refreshToken))
|
||||
return LogicResult<SingleValueResult<string>>.Conflict("Refresh token not provided");
|
||||
|
||||
var token = await context.Tokens.SingleOrDefaultAsync(token => token.Token == refreshToken && token.Type == TokenEntry.RefreshTokenType);
|
||||
var token = await tokens.GetToken(refreshToken);
|
||||
|
||||
if (token.Type != Token.RefreshTokenType)
|
||||
return LogicResult<SingleValueResult<string>>.BadRequest("The provided token is not a refresh token");
|
||||
|
||||
if (token is null)
|
||||
return LogicResult<SingleValueResult<string>>.NotFound("Refresh token not valid");
|
||||
|
||||
if (token.CreatedAt + HopFrameAuthentication<TDbContext>.RefreshTokenTime < DateTime.Now)
|
||||
if (token.CreatedAt + HopFrameAuthentication.RefreshTokenTime < DateTime.Now)
|
||||
return LogicResult<SingleValueResult<string>>.Conflict("Refresh token is expired");
|
||||
|
||||
var accessToken = new TokenEntry {
|
||||
CreatedAt = DateTime.Now,
|
||||
Token = Guid.NewGuid().ToString(),
|
||||
Type = TokenEntry.AccessTokenType,
|
||||
UserId = token.UserId
|
||||
};
|
||||
var accessToken = await tokens.CreateToken(Token.AccessTokenType, token.Owner);
|
||||
|
||||
await context.Tokens.AddAsync(accessToken);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Token, new CookieOptions {
|
||||
MaxAge = HopFrameAuthentication<TDbContext>.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.Token);
|
||||
return LogicResult<SingleValueResult<string>>.Ok(accessToken.Content.ToString());
|
||||
}
|
||||
|
||||
public async Task<LogicResult> Logout() {
|
||||
@@ -131,17 +102,7 @@ public class AuthLogic<TDbContext>(TDbContext context, IUserService users, IToke
|
||||
if (string.IsNullOrEmpty(accessToken) || string.IsNullOrEmpty(refreshToken))
|
||||
return LogicResult.Conflict("access or refresh token not provided");
|
||||
|
||||
var tokenEntries = await context.Tokens.Where(token =>
|
||||
(token.Token == accessToken && token.Type == TokenEntry.AccessTokenType) ||
|
||||
(token.Token == refreshToken && token.Type == TokenEntry.RefreshTokenType))
|
||||
.ToArrayAsync();
|
||||
|
||||
if (tokenEntries.Length != 2)
|
||||
return LogicResult.NotFound("One or more of the provided tokens was not found");
|
||||
|
||||
context.Tokens.Remove(tokenEntries[0]);
|
||||
context.Tokens.Remove(tokenEntries[1]);
|
||||
await context.SaveChangesAsync();
|
||||
await tokens.DeleteUserTokens(tokenContext.User);
|
||||
|
||||
accessor.HttpContext?.Response.Cookies.Delete(ITokenContext.RefreshTokenType);
|
||||
accessor.HttpContext?.Response.Cookies.Delete(ITokenContext.AccessTokenType);
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
namespace HopFrame.Api.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Useful for endpoints that only return a single int or string
|
||||
/// </summary>
|
||||
/// <param name="value">The value of the result</param>
|
||||
/// <typeparam name="TValue">The type of the result</typeparam>
|
||||
public struct SingleValueResult<TValue>(TValue value) {
|
||||
public TValue Value { get; set; } = value;
|
||||
|
||||
|
||||
@@ -1,100 +1,4 @@
|
||||
# HopFrame API module
|
||||
This module contains some useful endpoints for user login / register management.
|
||||
|
||||
## Ho to use the Web API version
|
||||
|
||||
1. Add the HopFrame.Api library to your project:
|
||||
|
||||
```
|
||||
dotnet add package HopFrame.Api
|
||||
```
|
||||
|
||||
2. Create a DbContext that inherits the ``HopDbContext`` and add a data source
|
||||
|
||||
```csharp
|
||||
public class DatabaseContext : HopDbContextBase {
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
|
||||
base.OnConfiguring(optionsBuilder);
|
||||
|
||||
optionsBuilder.UseSqlite("...");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. Add the DbContext and HopFrame to your services
|
||||
|
||||
```csharp
|
||||
builder.Services.AddDbContext<DatabaseContext>();
|
||||
builder.Services.AddHopFrame<DatabaseContext>();
|
||||
```
|
||||
|
||||
# Endpoints
|
||||
By default, the module provides a controller for handling authentication based requests by the user.
|
||||
You can explore the contoller by the build in swagger site from ASP .NET.
|
||||
|
||||
## Disable the Endpoints
|
||||
|
||||
```csharp
|
||||
builder.Services.AddDbContext<DatabaseContext>();
|
||||
//builder.Services.AddHopFrame<DatabaseContext>();
|
||||
services.AddHopFrameNoEndpoints<TDbContext>();
|
||||
```
|
||||
|
||||
# Services added in this module
|
||||
You can use these services by specifying them as a dependency. All of them are scoped dependencies.
|
||||
|
||||
## LogicResult
|
||||
Logic result is an extension of the ActionResult for an ApiController. It provides simple Http status results with either a message or data by specifying the generic type.
|
||||
|
||||
```csharp
|
||||
public class LogicResult : ILogicResult {
|
||||
public static LogicResult Ok();
|
||||
|
||||
public static LogicResult BadRequest();
|
||||
|
||||
public static LogicResult BadRequest(string message);
|
||||
|
||||
public static LogicResult Forbidden();
|
||||
|
||||
public static LogicResult Forbidden(string message);
|
||||
|
||||
public static LogicResult NotFound();
|
||||
|
||||
public static LogicResult NotFound(string message);
|
||||
|
||||
public static LogicResult Conflict();
|
||||
|
||||
public static LogicResult Conflict(string message);
|
||||
|
||||
public static LogicResult Forward(LogicResult result);
|
||||
|
||||
public static LogicResult Forward<T>(ILogicResult<T> result);
|
||||
|
||||
public static implicit operator ActionResult(LogicResult v);
|
||||
}
|
||||
|
||||
public class LogicResult<T> : ILogicResult<T> {
|
||||
public static LogicResult<T> Ok();
|
||||
|
||||
public static LogicResult<T> Ok(T result);
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## IAuthLogic
|
||||
This service handles all logic needed to provide the authentication endpoints by using the LogicResults.
|
||||
|
||||
```csharp
|
||||
public interface IAuthLogic {
|
||||
Task<LogicResult<SingleValueResult<string>>> Login(UserLogin login);
|
||||
|
||||
Task<LogicResult<SingleValueResult<string>>> Register(UserRegister register);
|
||||
|
||||
Task<LogicResult<SingleValueResult<string>>> Authenticate();
|
||||
|
||||
Task<LogicResult> Logout();
|
||||
|
||||
Task<LogicResult> Delete(UserPasswordValidation validation);
|
||||
}
|
||||
```
|
||||
For more information about the HopFrame visit the [docs](https://git.leon-hoppe.de/leon.hoppe/HopFrame).
|
||||
@@ -1,6 +1,6 @@
|
||||
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
|
||||
|
||||
namespace HopFrame.Security;
|
||||
namespace HopFrame.Database;
|
||||
|
||||
public static class EncryptionManager {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using HopFrame.Database.Models.Entries;
|
||||
using HopFrame.Database.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace HopFrame.Database;
|
||||
@@ -8,25 +8,27 @@ namespace HopFrame.Database;
|
||||
/// </summary>
|
||||
public abstract class HopDbContextBase : DbContext {
|
||||
|
||||
public virtual DbSet<UserEntry> Users { get; set; }
|
||||
public virtual DbSet<PermissionEntry> Permissions { get; set; }
|
||||
public virtual DbSet<TokenEntry> Tokens { get; set; }
|
||||
public virtual DbSet<GroupEntry> Groups { get; set; }
|
||||
public virtual DbSet<User> Users { get; set; }
|
||||
public virtual DbSet<Permission> Permissions { get; set; }
|
||||
public virtual DbSet<Token> Tokens { get; set; }
|
||||
public virtual DbSet<PermissionGroup> Groups { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder) {
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.Entity<UserEntry>();
|
||||
modelBuilder.Entity<PermissionEntry>();
|
||||
modelBuilder.Entity<TokenEntry>();
|
||||
modelBuilder.Entity<GroupEntry>();
|
||||
}
|
||||
modelBuilder.Entity<User>()
|
||||
.HasMany(u => u.Tokens)
|
||||
.WithOne(t => t.Owner)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
/// <summary>
|
||||
/// Gets executed when a user is deleted through the IUserService from the
|
||||
/// HopFrame.Security package. You can override this method to also delete
|
||||
/// related user specific entries in the database
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
public virtual void OnUserDelete(UserEntry user) {}
|
||||
modelBuilder.Entity<User>()
|
||||
.HasMany(u => u.Permissions)
|
||||
.WithOne(p => p.User)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder.Entity<PermissionGroup>()
|
||||
.HasMany(g => g.Permissions)
|
||||
.WithOne(p => p.Group)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
@@ -7,13 +7,14 @@
|
||||
<Nullable>disable</Nullable>
|
||||
|
||||
<PackageId>HopFrame.Database</PackageId>
|
||||
<Version>1.1.0</Version>
|
||||
<Version>2.0.0</Version>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<IsPackable>true</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="8.0.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace HopFrame.Database.Models.Entries;
|
||||
|
||||
public class GroupEntry {
|
||||
[Key, Required, MaxLength(50)]
|
||||
public string Name { get; set; }
|
||||
|
||||
[Required, DefaultValue(false)]
|
||||
public bool Default { get; set; }
|
||||
|
||||
[MaxLength(500)]
|
||||
public string Description { get; set; }
|
||||
|
||||
[Required]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace HopFrame.Database.Models.Entries;
|
||||
|
||||
public sealed class PermissionEntry {
|
||||
[Key, Required, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public long RecordId { get; set; }
|
||||
|
||||
[Required, MaxLength(255)]
|
||||
public string PermissionText { get; set; }
|
||||
|
||||
[Required, MinLength(36), MaxLength(36)]
|
||||
public string UserId { get; set; }
|
||||
|
||||
[Required]
|
||||
public DateTime GrantedAt { get; set; }
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace HopFrame.Database.Models.Entries;
|
||||
|
||||
public class UserEntry {
|
||||
[Key, Required, MinLength(36), MaxLength(36)]
|
||||
public string Id { get; set; }
|
||||
|
||||
[MaxLength(50)]
|
||||
public string Username { get; set; }
|
||||
|
||||
[Required, MaxLength(50), EmailAddress]
|
||||
public string Email { get; set; }
|
||||
|
||||
[Required, MinLength(8), MaxLength(255)]
|
||||
public string Password { get; set; }
|
||||
|
||||
[Required]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
using HopFrame.Database.Models.Entries;
|
||||
|
||||
namespace HopFrame.Database.Models;
|
||||
|
||||
public static class ModelExtensions {
|
||||
|
||||
/// <summary>
|
||||
/// Converts the database model to a friendly user model
|
||||
/// </summary>
|
||||
/// <param name="entry">the database model</param>
|
||||
/// <param name="contextBase">the data source for the permissions and users</param>
|
||||
/// <returns></returns>
|
||||
public static User ToUserModel(this UserEntry entry, HopDbContextBase contextBase) {
|
||||
var user = new User {
|
||||
Id = Guid.Parse(entry.Id),
|
||||
Username = entry.Username,
|
||||
Email = entry.Email,
|
||||
CreatedAt = entry.CreatedAt
|
||||
};
|
||||
|
||||
user.Permissions = contextBase.Permissions
|
||||
.Where(perm => perm.UserId == entry.Id)
|
||||
.Select(perm => perm.ToPermissionModel())
|
||||
.ToList();
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
public static Permission ToPermissionModel(this PermissionEntry entry) {
|
||||
Guid.TryParse(entry.UserId, out var userId);
|
||||
|
||||
return new Permission {
|
||||
Owner = userId,
|
||||
PermissionName = entry.PermissionText,
|
||||
GrantedAt = entry.GrantedAt,
|
||||
Id = entry.RecordId
|
||||
};
|
||||
}
|
||||
|
||||
public static PermissionGroup ToPermissionGroup(this GroupEntry entry, HopDbContextBase contextBase) {
|
||||
var group = new PermissionGroup {
|
||||
Name = entry.Name,
|
||||
IsDefaultGroup = entry.Default,
|
||||
Description = entry.Description,
|
||||
CreatedAt = entry.CreatedAt
|
||||
};
|
||||
|
||||
group.Permissions = contextBase.Permissions
|
||||
.Where(perm => perm.UserId == group.Name)
|
||||
.Select(perm => perm.ToPermissionModel())
|
||||
.ToList();
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,10 +1,26 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace HopFrame.Database.Models;
|
||||
|
||||
public sealed class Permission {
|
||||
public class Permission {
|
||||
|
||||
[Key, Required, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public long Id { get; init; }
|
||||
|
||||
[Required, MaxLength(255)]
|
||||
public string PermissionName { get; set; }
|
||||
public Guid Owner { get; set; }
|
||||
|
||||
[Required]
|
||||
public DateTime GrantedAt { get; set; }
|
||||
|
||||
[ForeignKey("UserId"), JsonIgnore]
|
||||
public virtual User User { get; set; }
|
||||
|
||||
[ForeignKey("GroupName"), JsonIgnore]
|
||||
public virtual PermissionGroup Group { get; set; }
|
||||
|
||||
}
|
||||
|
||||
public interface IPermissionOwner {}
|
||||
public interface IPermissionOwner;
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace HopFrame.Database.Models;
|
||||
|
||||
public class PermissionGroup : IPermissionOwner {
|
||||
|
||||
[Key, Required, MaxLength(50)]
|
||||
public string Name { get; init; }
|
||||
|
||||
[Required, DefaultValue(false)]
|
||||
public bool IsDefaultGroup { get; set; }
|
||||
|
||||
[MaxLength(500)]
|
||||
public string Description { get; set; }
|
||||
|
||||
[Required]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public IList<Permission> Permissions { get; set; }
|
||||
|
||||
public virtual List<Permission> Permissions { get; set; }
|
||||
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace HopFrame.Database.Models.Entries;
|
||||
namespace HopFrame.Database.Models;
|
||||
|
||||
public class TokenEntry {
|
||||
public class Token {
|
||||
public const int RefreshTokenType = 0;
|
||||
public const int AccessTokenType = 1;
|
||||
|
||||
@@ -15,11 +17,11 @@ public class TokenEntry {
|
||||
public int Type { get; set; }
|
||||
|
||||
[Key, Required, MinLength(36), MaxLength(36)]
|
||||
public string Token { get; set; }
|
||||
|
||||
[Required, MinLength(36), MaxLength(36)]
|
||||
public string UserId { get; set; }
|
||||
public Guid Content { get; set; }
|
||||
|
||||
[Required]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
[ForeignKey("UserId"), JsonIgnore]
|
||||
public virtual User Owner { get; set; }
|
||||
}
|
||||
@@ -1,9 +1,28 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace HopFrame.Database.Models;
|
||||
|
||||
public sealed class User : IPermissionOwner {
|
||||
public class User : IPermissionOwner {
|
||||
|
||||
[Key, Required, MinLength(36), MaxLength(36)]
|
||||
public Guid Id { get; init; }
|
||||
|
||||
[Required, MaxLength(50)]
|
||||
public string Username { get; set; }
|
||||
|
||||
[Required, MaxLength(50), EmailAddress]
|
||||
public string Email { get; set; }
|
||||
|
||||
[Required, MinLength(8), MaxLength(255), JsonIgnore]
|
||||
public string Password { get; set; }
|
||||
|
||||
[Required]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public IList<Permission> Permissions { get; set; }
|
||||
|
||||
public virtual List<Permission> Permissions { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public virtual List<Token> Tokens { get; set; }
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace HopFrame.Security.Authorization;
|
||||
namespace HopFrame.Database;
|
||||
|
||||
public static class PermissionValidator {
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
# HopFrame Database module
|
||||
This module contains all the logic for the database communication
|
||||
This module contains all the logic for the database communication.
|
||||
|
||||
For more information about the HopFrame visit the [docs](https://git.leon-hoppe.de/leon.hoppe/HopFrame).
|
||||
|
||||
21
src/HopFrame.Database/Repositories/IGroupRepository.cs
Normal file
21
src/HopFrame.Database/Repositories/IGroupRepository.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using HopFrame.Database.Models;
|
||||
|
||||
namespace HopFrame.Database.Repositories;
|
||||
|
||||
public interface IGroupRepository {
|
||||
Task<IList<PermissionGroup>> GetPermissionGroups();
|
||||
|
||||
Task<IList<PermissionGroup>> GetDefaultGroups();
|
||||
|
||||
Task<IList<PermissionGroup>> GetUserGroups(User user);
|
||||
|
||||
Task<PermissionGroup> GetPermissionGroup(string name);
|
||||
|
||||
Task EditPermissionGroup(PermissionGroup group);
|
||||
|
||||
Task<PermissionGroup> CreatePermissionGroup(PermissionGroup group);
|
||||
|
||||
Task DeletePermissionGroup(PermissionGroup group);
|
||||
|
||||
internal Task<IList<string>> GetFullGroupPermissions(string group);
|
||||
}
|
||||
23
src/HopFrame.Database/Repositories/IPermissionRepository.cs
Normal file
23
src/HopFrame.Database/Repositories/IPermissionRepository.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using HopFrame.Database.Models;
|
||||
|
||||
namespace HopFrame.Database.Repositories;
|
||||
|
||||
public interface IPermissionRepository {
|
||||
Task<bool> HasPermission(IPermissionOwner owner, params string[] permissions);
|
||||
|
||||
/// <summary>
|
||||
/// permission system:<br/>
|
||||
/// - "*" -> all rights<br/>
|
||||
/// - "group.[name]" -> group member<br/>
|
||||
/// - "[namespace].[name]" -> single permission<br/>
|
||||
/// - "[namespace].*" -> all permissions in the namespace
|
||||
/// </summary>
|
||||
/// <param name="owner"></param>
|
||||
/// <param name="permission"></param>
|
||||
/// <returns></returns>
|
||||
Task<Permission> AddPermission(IPermissionOwner owner, string permission);
|
||||
|
||||
Task RemovePermission(IPermissionOwner owner, string permission);
|
||||
|
||||
Task<IList<string>> GetFullPermissions(IPermissionOwner owner);
|
||||
}
|
||||
9
src/HopFrame.Database/Repositories/ITokenRepository.cs
Normal file
9
src/HopFrame.Database/Repositories/ITokenRepository.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using HopFrame.Database.Models;
|
||||
|
||||
namespace HopFrame.Database.Repositories;
|
||||
|
||||
public interface ITokenRepository {
|
||||
Task<Token> GetToken(string content);
|
||||
Task<Token> CreateToken(int type, User owner);
|
||||
Task DeleteUserTokens(User owner);
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
using HopFrame.Database.Models;
|
||||
using HopFrame.Security.Models;
|
||||
|
||||
namespace HopFrame.Security.Services;
|
||||
namespace HopFrame.Database.Repositories;
|
||||
|
||||
public interface IUserService {
|
||||
public interface IUserRepository {
|
||||
Task<IList<User>> GetUsers();
|
||||
|
||||
Task<User> GetUser(Guid userId);
|
||||
@@ -12,13 +11,8 @@ public interface IUserService {
|
||||
|
||||
Task<User> GetUserByUsername(string username);
|
||||
|
||||
Task<User> AddUser(UserRegister user);
|
||||
Task<User> AddUser(User user);
|
||||
|
||||
/// <summary>
|
||||
/// IMPORTANT:<br/>
|
||||
/// This function does not add or remove any permissions to the user.
|
||||
/// For that please use <see cref="IPermissionService"/>
|
||||
/// </summary>
|
||||
Task UpdateUser(User user);
|
||||
|
||||
Task DeleteUser(User user);
|
||||
@@ -0,0 +1,79 @@
|
||||
using HopFrame.Database.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace HopFrame.Database.Repositories.Implementation;
|
||||
|
||||
internal sealed class GroupRepository<TDbContext>(TDbContext context) : IGroupRepository where TDbContext : HopDbContextBase {
|
||||
public async Task<IList<PermissionGroup>> GetPermissionGroups() {
|
||||
return await context.Groups
|
||||
.Include(g => g.Permissions)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<PermissionGroup>> GetDefaultGroups() {
|
||||
return await context.Groups
|
||||
.Include(g => g.Permissions)
|
||||
.Where(g => g.IsDefaultGroup)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public Task<IList<PermissionGroup>> GetUserGroups(User user) {
|
||||
return Task.FromResult((IList<PermissionGroup>) context.Groups
|
||||
.Include(g => g.Permissions)
|
||||
.AsEnumerable()
|
||||
.Where(g => user.Permissions.Any(p => p.PermissionName == g.Name))
|
||||
.ToList());
|
||||
}
|
||||
|
||||
public async Task<PermissionGroup> GetPermissionGroup(string name) {
|
||||
return await context.Groups
|
||||
.Include(g => g.Permissions)
|
||||
.Where(g => g.Name == name)
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task EditPermissionGroup(PermissionGroup group) {
|
||||
var orig = await context.Groups.SingleOrDefaultAsync(g => g.Name == group.Name);
|
||||
|
||||
if (orig is null) return;
|
||||
|
||||
var entity = context.Groups.Update(orig);
|
||||
|
||||
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);
|
||||
await context.SaveChangesAsync();
|
||||
return group;
|
||||
}
|
||||
|
||||
public async Task DeletePermissionGroup(PermissionGroup group) {
|
||||
context.Groups.Remove(group);
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<string>> GetFullGroupPermissions(string group) {
|
||||
var permissions = await context.Permissions
|
||||
.Include(p => p.Group)
|
||||
.Where(p => p.Group != null)
|
||||
.Where(p => p.Group.Name == group)
|
||||
.Select(p => p.PermissionName)
|
||||
.ToListAsync();
|
||||
|
||||
var groups = permissions
|
||||
.Where(p => p.StartsWith("group."))
|
||||
.ToList();
|
||||
|
||||
foreach (var subgroup in groups) {
|
||||
permissions.AddRange(await GetFullGroupPermissions(subgroup));
|
||||
}
|
||||
|
||||
return permissions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
using HopFrame.Database.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace HopFrame.Database.Repositories.Implementation;
|
||||
|
||||
internal sealed class PermissionRepository<TDbContext>(TDbContext context, IGroupRepository groupRepository) : IPermissionRepository where TDbContext : HopDbContextBase {
|
||||
public async Task<bool> HasPermission(IPermissionOwner owner, params string[] permissions) {
|
||||
var perms = (await GetFullPermissions(owner)).ToArray();
|
||||
|
||||
foreach (var permission in permissions) {
|
||||
if (!PermissionValidator.IncludesPermission(permission, perms)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<Permission> AddPermission(IPermissionOwner owner, string permission) {
|
||||
var entry = new Permission {
|
||||
GrantedAt = DateTime.Now,
|
||||
PermissionName = permission
|
||||
};
|
||||
|
||||
if (owner is User user) {
|
||||
entry.User = user;
|
||||
}else if (owner is PermissionGroup group) {
|
||||
entry.Group = group;
|
||||
}
|
||||
|
||||
await context.Permissions.AddAsync(entry);
|
||||
await context.SaveChangesAsync();
|
||||
return entry;
|
||||
}
|
||||
|
||||
public async Task RemovePermission(IPermissionOwner owner, string permission) {
|
||||
Permission entry = null;
|
||||
|
||||
if (owner is User user) {
|
||||
entry = await context.Permissions
|
||||
.Include(p => p.User)
|
||||
.Where(p => p.User != null)
|
||||
.Where(p => p.User.Id == user.Id)
|
||||
.Where(p => p.PermissionName == permission)
|
||||
.SingleOrDefaultAsync();
|
||||
}else if (owner is PermissionGroup group) {
|
||||
entry = await context.Permissions
|
||||
.Include(p => p.Group)
|
||||
.Where(p => p.Group != null)
|
||||
.Where(p =>p.Group.Name == group.Name)
|
||||
.Where(p => p.PermissionName == permission)
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
if (entry is not null) {
|
||||
context.Permissions.Remove(entry);
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IList<string>> GetFullPermissions(IPermissionOwner owner) {
|
||||
var permissions = new List<string>();
|
||||
|
||||
if (owner is User user) {
|
||||
var perms = await context.Permissions
|
||||
.Include(p => p.User)
|
||||
.Where(p => p.User != null)
|
||||
.Where(p => p.User.Id == user.Id)
|
||||
.ToListAsync();
|
||||
|
||||
permissions.AddRange(perms.Select(p => p.PermissionName));
|
||||
}else if (owner is PermissionGroup group) {
|
||||
var perms = await context.Permissions
|
||||
.Include(p => p.Group)
|
||||
.Where(p => p.Group != null)
|
||||
.Where(p =>p.Group.Name == group.Name)
|
||||
.ToListAsync();
|
||||
|
||||
permissions.AddRange(perms.Select(p => p.PermissionName));
|
||||
}
|
||||
|
||||
var groups = permissions
|
||||
.Where(p => p.StartsWith("group."))
|
||||
.ToList();
|
||||
foreach (var group in groups) {
|
||||
permissions.AddRange(await groupRepository.GetFullGroupPermissions(group));
|
||||
}
|
||||
|
||||
return permissions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using HopFrame.Database.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace HopFrame.Database.Repositories.Implementation;
|
||||
|
||||
internal sealed class TokenRepository<TDbContext>(TDbContext context) : ITokenRepository where TDbContext : HopDbContextBase {
|
||||
|
||||
public async Task<Token> GetToken(string content) {
|
||||
var success = Guid.TryParse(content, out Guid guid);
|
||||
if (!success) return null;
|
||||
|
||||
return await context.Tokens
|
||||
.Include(t => t.Owner)
|
||||
.Where(t => t.Content == guid)
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<Token> CreateToken(int type, User owner) {
|
||||
var token = new Token {
|
||||
CreatedAt = DateTime.Now,
|
||||
Content = Guid.NewGuid(),
|
||||
Type = type,
|
||||
Owner = owner
|
||||
};
|
||||
|
||||
await context.Tokens.AddAsync(token);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
public async Task DeleteUserTokens(User owner) {
|
||||
var tokens = await context.Tokens
|
||||
.Include(t => t.Owner)
|
||||
.Where(t => t.Owner.Id == owner.Id)
|
||||
.ToListAsync();
|
||||
|
||||
context.Tokens.RemoveRange(tokens);
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using HopFrame.Database.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Query;
|
||||
|
||||
namespace HopFrame.Database.Repositories.Implementation;
|
||||
|
||||
internal sealed class UserRepository<TDbContext>(TDbContext context, IGroupRepository groupRepository) : IUserRepository where TDbContext : HopDbContextBase {
|
||||
|
||||
private IIncludableQueryable<User, IList<Token>> IncludeReferences() {
|
||||
return context.Users
|
||||
.Include(u => u.Permissions)
|
||||
.Include(u => u.Tokens);
|
||||
}
|
||||
|
||||
public async Task<IList<User>> GetUsers() {
|
||||
return await IncludeReferences()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<User> GetUser(Guid userId) {
|
||||
return await IncludeReferences()
|
||||
.Where(u => u.Id == userId)
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<User> GetUserByEmail(string email) {
|
||||
return await IncludeReferences()
|
||||
.Where(u => u.Email == email)
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<User> GetUserByUsername(string username) {
|
||||
return await IncludeReferences()
|
||||
.Where(u => u.Username == username)
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<User> AddUser(User user) {
|
||||
if (await GetUserByEmail(user.Email) is not null) return null;
|
||||
if (await GetUserByUsername(user.Username) is not null) return null;
|
||||
|
||||
var entry = new User {
|
||||
Id = Guid.NewGuid(),
|
||||
Email = user.Email,
|
||||
Username = user.Username,
|
||||
CreatedAt = DateTime.Now,
|
||||
Permissions = user.Permissions ?? new List<Permission>(),
|
||||
Tokens = user.Tokens
|
||||
};
|
||||
entry.Password = EncryptionManager.Hash(user.Password, Encoding.Default.GetBytes(entry.CreatedAt.ToString(CultureInfo.InvariantCulture)));
|
||||
|
||||
var defaultGroups = await groupRepository.GetDefaultGroups();
|
||||
foreach (var group in defaultGroups) {
|
||||
entry.Permissions.Add(new Permission {
|
||||
PermissionName = group.Name,
|
||||
GrantedAt = DateTime.Now
|
||||
});
|
||||
}
|
||||
|
||||
await context.Users.AddAsync(entry);
|
||||
await context.SaveChangesAsync();
|
||||
return entry;
|
||||
}
|
||||
|
||||
public async Task UpdateUser(User user) {
|
||||
var entry = await IncludeReferences()
|
||||
.SingleOrDefaultAsync(entry => entry.Id == user.Id);
|
||||
if (entry is null) return;
|
||||
|
||||
entry.Email = user.Email;
|
||||
entry.Username = user.Username;
|
||||
entry.Permissions = user.Permissions;
|
||||
entry.Tokens = user.Tokens;
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task DeleteUser(User user) {
|
||||
var entry = await context.Users
|
||||
.SingleOrDefaultAsync(entry => entry.Id == user.Id);
|
||||
|
||||
if (entry is null) return;
|
||||
|
||||
context.Users.Remove(entry);
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> CheckUserPassword(User user, string password) {
|
||||
var hash = EncryptionManager.Hash(password, Encoding.Default.GetBytes(user.CreatedAt.ToString(CultureInfo.InvariantCulture)));
|
||||
|
||||
var entry = await context.Users
|
||||
.Where(entry => entry.Id == user.Id)
|
||||
.SingleOrDefaultAsync();
|
||||
|
||||
return entry.Password == hash;
|
||||
}
|
||||
|
||||
public async Task ChangePassword(User user, string password) {
|
||||
var entry = await context.Users
|
||||
.Where(entry => entry.Id == user.Id)
|
||||
.SingleOrDefaultAsync();
|
||||
|
||||
if (entry is null) return;
|
||||
|
||||
var hash = EncryptionManager.Hash(password, Encoding.Default.GetBytes(user.CreatedAt.ToString(CultureInfo.InvariantCulture)));
|
||||
entry.Password = hash;
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
}
|
||||
18
src/HopFrame.Database/ServiceCollectionExtensions.cs
Normal file
18
src/HopFrame.Database/ServiceCollectionExtensions.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using HopFrame.Database.Repositories;
|
||||
using HopFrame.Database.Repositories.Implementation;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace HopFrame.Database;
|
||||
|
||||
public static class ServiceCollectionExtensions {
|
||||
|
||||
public static IServiceCollection AddHopFrameRepositories<TDbContext>(this IServiceCollection services) where TDbContext : HopDbContextBase {
|
||||
services.AddScoped<IGroupRepository, GroupRepository<TDbContext>>();
|
||||
services.AddScoped<IPermissionRepository, PermissionRepository<TDbContext>>();
|
||||
services.AddScoped<IUserRepository, UserRepository<TDbContext>>();
|
||||
services.AddScoped<ITokenRepository, TokenRepository<TDbContext>>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using HopFrame.Database;
|
||||
using HopFrame.Database.Repositories;
|
||||
using HopFrame.Security.Claims;
|
||||
using HopFrame.Security.Services;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
@@ -13,15 +11,15 @@ using Microsoft.Extensions.Options;
|
||||
|
||||
namespace HopFrame.Security.Authentication;
|
||||
|
||||
public class HopFrameAuthentication<TDbContext>(
|
||||
public class HopFrameAuthentication(
|
||||
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder,
|
||||
ISystemClock clock,
|
||||
TDbContext context,
|
||||
IPermissionService perms)
|
||||
: AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder, clock)
|
||||
where TDbContext : HopDbContextBase {
|
||||
ITokenRepository tokens,
|
||||
IUserRepository users,
|
||||
IPermissionRepository perms)
|
||||
: AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder, clock) {
|
||||
|
||||
public const string SchemeName = "HopCore.Authentication";
|
||||
public static readonly TimeSpan AccessTokenTime = new(0, 0, 5, 0);
|
||||
@@ -33,20 +31,20 @@ public class HopFrameAuthentication<TDbContext>(
|
||||
if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Headers["Token"];
|
||||
if (string.IsNullOrEmpty(accessToken)) return AuthenticateResult.Fail("No Access Token provided");
|
||||
|
||||
var tokenEntry = await context.Tokens.SingleOrDefaultAsync(token => token.Token == accessToken);
|
||||
var tokenEntry = await tokens.GetToken(accessToken);
|
||||
|
||||
if (tokenEntry is null) return AuthenticateResult.Fail("The provided Access Token does not exist");
|
||||
if (tokenEntry.CreatedAt + AccessTokenTime < DateTime.Now) return AuthenticateResult.Fail("The provided Access Token is expired");
|
||||
|
||||
if (!await context.Users.AnyAsync(user => user.Id == tokenEntry.UserId))
|
||||
if (tokenEntry.Owner is null)
|
||||
return AuthenticateResult.Fail("The provided Access Token does not match any user");
|
||||
|
||||
var claims = new List<Claim> {
|
||||
new(HopFrameClaimTypes.AccessTokenId, accessToken),
|
||||
new(HopFrameClaimTypes.UserId, tokenEntry.UserId)
|
||||
new(HopFrameClaimTypes.UserId, tokenEntry.Owner.Id.ToString())
|
||||
};
|
||||
|
||||
var permissions = await perms.GetFullPermissions(tokenEntry.UserId);
|
||||
var permissions = await perms.GetFullPermissions(tokenEntry.Owner);
|
||||
claims.AddRange(permissions.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm)));
|
||||
|
||||
var principal = new ClaimsPrincipal();
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
using HopFrame.Database;
|
||||
using HopFrame.Security.Claims;
|
||||
using HopFrame.Security.Services;
|
||||
using HopFrame.Security.Services.Implementation;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -17,13 +14,11 @@ public static class HopFrameAuthenticationExtensions {
|
||||
/// <param name="service">The service provider to add the services to</param>
|
||||
/// <typeparam name="TDbContext">The database object that saves all entities that are important for the security api</typeparam>
|
||||
/// <returns></returns>
|
||||
public static IServiceCollection AddHopFrameAuthentication<TDbContext>(this IServiceCollection service) where TDbContext : HopDbContextBase {
|
||||
public static IServiceCollection AddHopFrameAuthentication(this IServiceCollection service) {
|
||||
service.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||
service.AddScoped<ITokenContext, TokenContextImplementor<TDbContext>>();
|
||||
service.AddScoped<IPermissionService, PermissionService<TDbContext>>();
|
||||
service.AddScoped<IUserService, UserService<TDbContext>>();
|
||||
service.AddScoped<ITokenContext, TokenContextImplementor>();
|
||||
|
||||
service.AddAuthentication(HopFrameAuthentication<TDbContext>.SchemeName).AddScheme<AuthenticationSchemeOptions, HopFrameAuthentication<TDbContext>>(HopFrameAuthentication<TDbContext>.SchemeName, _ => {});
|
||||
service.AddAuthentication(HopFrameAuthentication.SchemeName).AddScheme<AuthenticationSchemeOptions, HopFrameAuthentication>(HopFrameAuthentication.SchemeName, _ => {});
|
||||
service.AddAuthorization();
|
||||
|
||||
return service;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using HopFrame.Database;
|
||||
using HopFrame.Security.Claims;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Authorization;
|
||||
|
||||
@@ -20,5 +20,5 @@ public interface ITokenContext {
|
||||
/// <summary>
|
||||
/// The access token the user provided
|
||||
/// </summary>
|
||||
Guid AccessToken { get; }
|
||||
Token AccessToken { get; }
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
using HopFrame.Database;
|
||||
using HopFrame.Database.Models;
|
||||
using HopFrame.Database.Repositories;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace HopFrame.Security.Claims;
|
||||
|
||||
internal sealed class TokenContextImplementor<TDbContext>(IHttpContextAccessor accessor, TDbContext context) : ITokenContext where TDbContext : HopDbContextBase {
|
||||
internal sealed class TokenContextImplementor(IHttpContextAccessor accessor, IUserRepository users, ITokenRepository tokens) : ITokenContext {
|
||||
public bool IsAuthenticated => !string.IsNullOrEmpty(accessor.HttpContext?.User.GetAccessTokenId());
|
||||
|
||||
public User User => context.Users
|
||||
.SingleOrDefault(user => user.Id == accessor.HttpContext.User.GetUserId())?
|
||||
.ToUserModel(context);
|
||||
public User User => users.GetUser(Guid.Parse(accessor.HttpContext?.User.GetUserId() ?? Guid.Empty.ToString())).GetAwaiter().GetResult();
|
||||
|
||||
public Guid AccessToken => Guid.Parse(accessor.HttpContext?.User.GetAccessTokenId() ?? Guid.Empty.ToString());
|
||||
public Token AccessToken => tokens.GetToken(accessor.HttpContext?.User.GetAccessTokenId()).GetAwaiter().GetResult();
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
<RootNamespace>HopFrame.Security</RootNamespace>
|
||||
|
||||
<PackageId>HopFrame.Security</PackageId>
|
||||
<Version>1.1.0</Version>
|
||||
<Version>2.0.0</Version>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<IsPackable>true</IsPackable>
|
||||
|
||||
@@ -1,74 +1,4 @@
|
||||
# HopFrame Security module
|
||||
this module contains all handlers for the login and register validation. It also checks the user permissions.
|
||||
|
||||
# Services added in this module
|
||||
You can use these services by specifying them as a dependency. All of them are scoped dependencies.
|
||||
|
||||
## ITokenContext
|
||||
This service provides the information given by the current request
|
||||
|
||||
```csharp
|
||||
public interface ITokenContext {
|
||||
bool IsAuthenticated { get; }
|
||||
|
||||
User User { get; }
|
||||
|
||||
Guid AccessToken { get; }
|
||||
}
|
||||
```
|
||||
|
||||
## IUserService
|
||||
This service simplifies the data access of the user table in the database.
|
||||
|
||||
```csharp
|
||||
public interface IUserService {
|
||||
Task<IList<User>> GetUsers();
|
||||
|
||||
Task<User> GetUser(Guid userId);
|
||||
|
||||
Task<User> GetUserByEmail(string email);
|
||||
|
||||
Task<User> GetUserByUsername(string username);
|
||||
|
||||
Task<User> AddUser(UserRegister user);
|
||||
|
||||
Task UpdateUser(User user);
|
||||
|
||||
Task DeleteUser(User user);
|
||||
|
||||
Task<bool> CheckUserPassword(User user, string password);
|
||||
|
||||
Task ChangePassword(User user, string password);
|
||||
}
|
||||
```
|
||||
|
||||
## IPermissionService
|
||||
This service handles all permission and group interactions with the data source.
|
||||
|
||||
```csharp
|
||||
public interface IPermissionService {
|
||||
Task<bool> HasPermission(string permission, Guid user);
|
||||
|
||||
Task<IList<PermissionGroup>> GetPermissionGroups();
|
||||
|
||||
Task<PermissionGroup> GetPermissionGroup(string name);
|
||||
|
||||
Task EditPermissionGroup(PermissionGroup group);
|
||||
|
||||
Task<IList<PermissionGroup>> GetUserPermissionGroups(User user);
|
||||
|
||||
Task RemoveGroupFromUser(User user, PermissionGroup group);
|
||||
|
||||
Task<PermissionGroup> CreatePermissionGroup(string name, bool isDefault = false, string description = null);
|
||||
|
||||
Task DeletePermissionGroup(PermissionGroup group);
|
||||
|
||||
Task<Permission> GetPermission(string name, IPermissionOwner owner);
|
||||
|
||||
Task AddPermission(IPermissionOwner owner, string permission);
|
||||
|
||||
Task RemovePermission(Permission permission);
|
||||
|
||||
Task<string[]> GetFullPermissions(string user);
|
||||
}
|
||||
```
|
||||
For more information about the HopFrame visit the [docs](https://git.leon-hoppe.de/leon.hoppe/HopFrame).
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
using HopFrame.Database.Models;
|
||||
|
||||
namespace HopFrame.Security.Services;
|
||||
|
||||
/// <summary>
|
||||
/// permission system:<br/>
|
||||
/// - "*" -> all rights<br/>
|
||||
/// - "group.[name]" -> group member<br/>
|
||||
/// - "[namespace].[name]" -> single permission<br/>
|
||||
/// - "[namespace].*" -> all permissions in the namespace
|
||||
/// </summary>
|
||||
public interface IPermissionService {
|
||||
|
||||
Task<bool> HasPermission(string permission, Guid user);
|
||||
|
||||
Task<IList<PermissionGroup>> GetPermissionGroups();
|
||||
|
||||
Task<PermissionGroup> GetPermissionGroup(string name);
|
||||
|
||||
Task EditPermissionGroup(PermissionGroup group);
|
||||
|
||||
Task<IList<PermissionGroup>> GetUserPermissionGroups(User user);
|
||||
|
||||
Task RemoveGroupFromUser(User user, PermissionGroup group);
|
||||
|
||||
Task<PermissionGroup> CreatePermissionGroup(string name, bool isDefault = false, string description = null);
|
||||
|
||||
Task DeletePermissionGroup(PermissionGroup group);
|
||||
|
||||
Task<Permission> GetPermission(string name, IPermissionOwner owner);
|
||||
|
||||
/// <summary>
|
||||
/// permission system:<br/>
|
||||
/// - "*" -> all rights<br/>
|
||||
/// - "group.[name]" -> group member<br/>
|
||||
/// - "[namespace].[name]" -> single permission<br/>
|
||||
/// - "[namespace].*" -> all permissions in the namespace
|
||||
/// </summary>
|
||||
/// <param name="owner"></param>
|
||||
/// <param name="permission"></param>
|
||||
/// <returns></returns>
|
||||
Task AddPermission(IPermissionOwner owner, string permission);
|
||||
|
||||
Task RemovePermission(Permission permission);
|
||||
|
||||
Task<string[]> GetFullPermissions(string user);
|
||||
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
using HopFrame.Database;
|
||||
using HopFrame.Database.Models;
|
||||
using HopFrame.Database.Models.Entries;
|
||||
using HopFrame.Security.Authorization;
|
||||
using HopFrame.Security.Claims;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace HopFrame.Security.Services.Implementation;
|
||||
|
||||
internal sealed class PermissionService<TDbContext>(TDbContext context, ITokenContext current) : IPermissionService where TDbContext : HopDbContextBase {
|
||||
public async Task<bool> HasPermission(string permission) {
|
||||
return await HasPermission(permission, current.User.Id);
|
||||
}
|
||||
|
||||
public async Task<bool> HasPermissions(params string[] permissions) {
|
||||
var user = current.User.Id.ToString();
|
||||
var perms = await GetFullPermissions(user);
|
||||
|
||||
foreach (var permission in permissions) {
|
||||
if (!PermissionValidator.IncludesPermission(permission, perms)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> HasAnyPermission(params string[] permissions) {
|
||||
var user = current.User.Id.ToString();
|
||||
var perms = await GetFullPermissions(user);
|
||||
|
||||
foreach (var permission in permissions) {
|
||||
if (PermissionValidator.IncludesPermission(permission, perms)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<bool> HasPermission(string permission, Guid user) {
|
||||
var permissions = await GetFullPermissions(user.ToString());
|
||||
|
||||
return PermissionValidator.IncludesPermission(permission, permissions);
|
||||
}
|
||||
|
||||
public async Task<IList<PermissionGroup>> GetPermissionGroups() {
|
||||
return await context.Groups
|
||||
.Select(group => group.ToPermissionGroup(context))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public Task<PermissionGroup> GetPermissionGroup(string name) {
|
||||
return context.Groups
|
||||
.Where(group => group.Name == name)
|
||||
.Select(group => group.ToPermissionGroup(context))
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task EditPermissionGroup(PermissionGroup group) {
|
||||
var orig = await context.Groups.SingleOrDefaultAsync(g => g.Name == group.Name);
|
||||
|
||||
if (orig is null) return;
|
||||
|
||||
var entity = context.Groups.Update(orig);
|
||||
|
||||
entity.Entity.Default = group.IsDefaultGroup;
|
||||
entity.Entity.Description = group.Description;
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<IList<PermissionGroup>> GetUserPermissionGroups(User user) {
|
||||
var groups = await context.Groups.ToListAsync();
|
||||
var perms = await GetFullPermissions(user.Id.ToString());
|
||||
|
||||
return groups
|
||||
.Where(group => perms.Contains(group.Name))
|
||||
.Select(group => group.ToPermissionGroup(context))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task RemoveGroupFromUser(User user, PermissionGroup group) {
|
||||
var entry = await context.Permissions
|
||||
.Where(perm => perm.PermissionText == group.Name && perm.UserId == user.Id.ToString())
|
||||
.SingleOrDefaultAsync();
|
||||
|
||||
if (entry is null) return;
|
||||
|
||||
context.Permissions.Remove(entry);
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<PermissionGroup> CreatePermissionGroup(string name, bool isDefault = false, string description = null) {
|
||||
var group = new GroupEntry {
|
||||
Name = name,
|
||||
Description = description,
|
||||
Default = isDefault,
|
||||
CreatedAt = DateTime.Now
|
||||
};
|
||||
|
||||
await context.Groups.AddAsync(group);
|
||||
|
||||
if (isDefault) {
|
||||
var users = await context.Users.ToListAsync();
|
||||
|
||||
foreach (var user in users) {
|
||||
await context.Permissions.AddAsync(new PermissionEntry {
|
||||
GrantedAt = DateTime.Now,
|
||||
PermissionText = group.Name,
|
||||
UserId = user.Id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
return group.ToPermissionGroup(context);
|
||||
}
|
||||
|
||||
public async Task DeletePermissionGroup(PermissionGroup group) {
|
||||
var entry = await context.Groups.SingleOrDefaultAsync(entry => entry.Name == group.Name);
|
||||
context.Groups.Remove(entry);
|
||||
|
||||
var permissions = await context.Permissions
|
||||
.Where(perm => perm.UserId == group.Name || perm.PermissionText == group.Name)
|
||||
.ToListAsync();
|
||||
|
||||
if (permissions.Count > 0) {
|
||||
context.Permissions.RemoveRange(permissions);
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<Permission> GetPermission(string name, IPermissionOwner owner) {
|
||||
var ownerId = (owner is User user) ? user.Id.ToString() : ((PermissionGroup)owner).Name;
|
||||
|
||||
return await context.Permissions
|
||||
.Where(perm => perm.PermissionText == name && perm.UserId == ownerId)
|
||||
.Select(perm => perm.ToPermissionModel())
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task AddPermission(IPermissionOwner owner, string permission) {
|
||||
var userId = owner is User user ? user.Id.ToString() : (owner as PermissionGroup)?.Name;
|
||||
|
||||
await context.Permissions.AddAsync(new PermissionEntry {
|
||||
UserId = userId,
|
||||
PermissionText = permission,
|
||||
GrantedAt = DateTime.Now
|
||||
});
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task RemovePermission(Permission permission) {
|
||||
var entry = await context.Permissions.SingleOrDefaultAsync(entry => entry.RecordId == permission.Id);
|
||||
context.Permissions.Remove(entry);
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<string[]> GetFullPermissions(string user) {
|
||||
var permissions = await context.Permissions
|
||||
.Where(perm => perm.UserId == user)
|
||||
.Select(perm => perm.PermissionText)
|
||||
.ToListAsync();
|
||||
|
||||
var groups = permissions
|
||||
.Where(perm => perm.StartsWith("group."))
|
||||
.ToList();
|
||||
|
||||
var groupPerms = new List<string>();
|
||||
foreach (var group in groups) {
|
||||
var perms = await GetFullPermissions(group);
|
||||
groupPerms.AddRange(perms);
|
||||
}
|
||||
|
||||
permissions.AddRange(groupPerms);
|
||||
|
||||
return permissions.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using HopFrame.Database;
|
||||
using HopFrame.Database.Models;
|
||||
using HopFrame.Database.Models.Entries;
|
||||
using HopFrame.Security.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace HopFrame.Security.Services.Implementation;
|
||||
|
||||
internal sealed class UserService<TDbContext>(TDbContext context) : IUserService where TDbContext : HopDbContextBase {
|
||||
public async Task<IList<User>> GetUsers() {
|
||||
return await context.Users
|
||||
.Select(user => user.ToUserModel(context))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public Task<User> GetUser(Guid userId) {
|
||||
var id = userId.ToString();
|
||||
|
||||
return context.Users
|
||||
.Where(user => user.Id == id)
|
||||
.Select(user => user.ToUserModel(context))
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
public Task<User> GetUserByEmail(string email) {
|
||||
return context.Users
|
||||
.Where(user => user.Email == email)
|
||||
.Select(user => user.ToUserModel(context))
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
public Task<User> GetUserByUsername(string username) {
|
||||
return context.Users
|
||||
.Where(user => user.Username == username)
|
||||
.Select(user => user.ToUserModel(context))
|
||||
.SingleOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<User> AddUser(UserRegister user) {
|
||||
if (await GetUserByEmail(user.Email) is not null) return null;
|
||||
if (await GetUserByUsername(user.Username) is not null) return null;
|
||||
|
||||
var entry = new UserEntry {
|
||||
Id = Guid.NewGuid().ToString(),
|
||||
Email = user.Email,
|
||||
Username = user.Username,
|
||||
CreatedAt = DateTime.Now
|
||||
};
|
||||
entry.Password = EncryptionManager.Hash(user.Password, Encoding.Default.GetBytes(entry.CreatedAt.ToString(CultureInfo.InvariantCulture)));
|
||||
|
||||
await context.Users.AddAsync(entry);
|
||||
|
||||
var defaultGroups = await context.Groups
|
||||
.Where(group => group.Default)
|
||||
.Select(group => "group." + group.Name)
|
||||
.ToListAsync();
|
||||
|
||||
await context.Permissions.AddRangeAsync(defaultGroups.Select(group => new PermissionEntry {
|
||||
GrantedAt = DateTime.Now,
|
||||
PermissionText = group,
|
||||
UserId = entry.Id
|
||||
}));
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
return entry.ToUserModel(context);
|
||||
}
|
||||
|
||||
public async Task UpdateUser(User user) {
|
||||
var id = user.Id.ToString();
|
||||
var entry = await context.Users
|
||||
.SingleOrDefaultAsync(entry => entry.Id == id);
|
||||
if (entry is null) return;
|
||||
|
||||
entry.Email = user.Email;
|
||||
entry.Username = user.Username;
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task DeleteUser(User user) {
|
||||
var id = user.Id.ToString();
|
||||
var entry = await context.Users
|
||||
.SingleOrDefaultAsync(entry => entry.Id == id);
|
||||
|
||||
if (entry is null) return;
|
||||
|
||||
context.Users.Remove(entry);
|
||||
|
||||
var userTokens = await context.Tokens
|
||||
.Where(token => token.UserId == id)
|
||||
.ToArrayAsync();
|
||||
context.Tokens.RemoveRange(userTokens);
|
||||
|
||||
var userPermissions = await context.Permissions
|
||||
.Where(perm => perm.UserId == id)
|
||||
.ToArrayAsync();
|
||||
context.Permissions.RemoveRange(userPermissions);
|
||||
|
||||
context.OnUserDelete(entry);
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> CheckUserPassword(User user, string password) {
|
||||
var id = user.Id.ToString();
|
||||
var hash = EncryptionManager.Hash(password, Encoding.Default.GetBytes(user.CreatedAt.ToString(CultureInfo.InvariantCulture)));
|
||||
|
||||
var entry = await context.Users
|
||||
.Where(entry => entry.Id == id)
|
||||
.SingleOrDefaultAsync();
|
||||
|
||||
return entry.Password == hash;
|
||||
}
|
||||
|
||||
public async Task ChangePassword(User user, string password) {
|
||||
var entry = await context.Users
|
||||
.Where(entry => entry.Id == user.Id.ToString())
|
||||
.SingleOrDefaultAsync();
|
||||
|
||||
if (entry is null) return;
|
||||
|
||||
var hash = EncryptionManager.Hash(password, Encoding.Default.GetBytes(user.CreatedAt.ToString(CultureInfo.InvariantCulture)));
|
||||
entry.Password = hash;
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
9
src/HopFrame.Web.Admin/AdminPagesContext.cs
Normal file
9
src/HopFrame.Web.Admin/AdminPagesContext.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using HopFrame.Web.Admin.Generators;
|
||||
|
||||
namespace HopFrame.Web.Admin;
|
||||
|
||||
public abstract class AdminPagesContext {
|
||||
|
||||
public virtual void OnModelCreating(IAdminContextGenerator generator) {}
|
||||
|
||||
}
|
||||
6
src/HopFrame.Web.Admin/Attributes/AdminNameAttribute.cs
Normal file
6
src/HopFrame.Web.Admin/Attributes/AdminNameAttribute.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace HopFrame.Web.Admin.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)]
|
||||
public sealed class AdminNameAttribute(string name) : Attribute {
|
||||
public string Name { get; set; } = name;
|
||||
}
|
||||
10
src/HopFrame.Web.Admin/Attributes/AdminPageUrlAttribute.cs
Normal file
10
src/HopFrame.Web.Admin/Attributes/AdminPageUrlAttribute.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace HopFrame.Web.Admin.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// This attribute specifies the url of the admin page and needs to be applied on the AdminPage property in the AdminContext directly
|
||||
/// </summary>
|
||||
/// <param name="url">The page url: '/administration/{url}'</param>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class AdminPageUrlAttribute(string url) : Attribute {
|
||||
public string Url { get; set; } = url;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace HopFrame.Web.Admin.Attributes.Classes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class AdminButtonConfigAttribute(bool showCreateButton = true, bool showDeleteButton = true, bool showUpdateButton = true) : Attribute {
|
||||
public bool ShowCreateButton { get; set; } = showCreateButton;
|
||||
public bool ShowDeleteButton { get; set; } = showDeleteButton;
|
||||
public bool ShowUpdateButton { get; set; } = showUpdateButton;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace HopFrame.Web.Admin.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class AdminDescriptionAttribute(string description) : Attribute {
|
||||
public string Description { get; set; } = description;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using HopFrame.Web.Admin.Models;
|
||||
|
||||
namespace HopFrame.Web.Admin.Attributes.Classes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class AdminPermissionsAttribute(string view = null, string create = null, string update = null, string delete = null) : Attribute {
|
||||
public AdminPagePermissions Permissions { get; set; } = new() {
|
||||
Create = create,
|
||||
Update = update,
|
||||
Delete = delete,
|
||||
View = view
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace HopFrame.Web.Admin.Attributes.Members;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class AdminBoldAttribute(bool bold = true) : Attribute {
|
||||
public bool Bold { get; set; } = bold;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace HopFrame.Web.Admin.Attributes.Members;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class AdminHideValueAttribute : Attribute;
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace HopFrame.Web.Admin.Attributes.Members;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class AdminIgnoreAttribute(bool onlyForListing = false) : Attribute {
|
||||
public bool OnlyForListing { get; set; } = onlyForListing;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace HopFrame.Web.Admin.Attributes.Members;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class AdminPrefixAttribute(string prefix) : Attribute {
|
||||
public string Prefix { get; set; } = prefix;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace HopFrame.Web.Admin.Attributes.Members;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class AdminUneditableAttribute : Attribute;
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace HopFrame.Web.Admin.Attributes.Members;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class AdminUniqueAttribute : Attribute;
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace HopFrame.Web.Admin.Attributes.Members;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class AdminUnsortableAttribute : Attribute;
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace HopFrame.Web.Admin.Attributes.Members;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class ListingPropertyAttribute : Attribute;
|
||||
12
src/HopFrame.Web.Admin/Generators/IAdminContextGenerator.cs
Normal file
12
src/HopFrame.Web.Admin/Generators/IAdminContextGenerator.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace HopFrame.Web.Admin.Generators;
|
||||
|
||||
public interface IAdminContextGenerator {
|
||||
|
||||
/// <summary>
|
||||
/// Returns the generator object for the specified Admin Page. This needs to be within the same Admin Context.
|
||||
/// </summary>
|
||||
/// <typeparam name="TModel">The Model of the Admin Page</typeparam>
|
||||
/// <returns></returns>
|
||||
IAdminPageGenerator<TModel> Page<TModel>();
|
||||
|
||||
}
|
||||
102
src/HopFrame.Web.Admin/Generators/IAdminPageGenerator.cs
Normal file
102
src/HopFrame.Web.Admin/Generators/IAdminPageGenerator.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using System.ComponentModel;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace HopFrame.Web.Admin.Generators;
|
||||
|
||||
public interface IAdminPageGenerator<TModel> {
|
||||
|
||||
/// <summary>
|
||||
/// Sets the title of the Admin Page
|
||||
/// </summary>
|
||||
/// <param name="title">the specified title</param>
|
||||
/// <returns></returns>
|
||||
IAdminPageGenerator<TModel> Title(string title);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the description of the Admin Page
|
||||
/// </summary>
|
||||
/// <param name="description">the specified description</param>
|
||||
/// <returns></returns>
|
||||
IAdminPageGenerator<TModel> Description(string description);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the permission needed to view the Admin Page
|
||||
/// </summary>
|
||||
/// <param name="permission">the specified permission</param>
|
||||
/// <returns></returns>
|
||||
IAdminPageGenerator<TModel> ViewPermission(string permission);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the permission needed to create a new Entry
|
||||
/// </summary>
|
||||
/// <param name="permission">the specified permission</param>
|
||||
/// <returns></returns>
|
||||
IAdminPageGenerator<TModel> CreatePermission(string permission);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the permission needed to update an Entry
|
||||
/// </summary>
|
||||
/// <param name="permission">the specified permission</param>
|
||||
/// <returns></returns>
|
||||
IAdminPageGenerator<TModel> UpdatePermission(string permission);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the permission needed to delete an Entry
|
||||
/// </summary>
|
||||
/// <param name="permission">the specified permission</param>
|
||||
/// <returns></returns>
|
||||
IAdminPageGenerator<TModel> DeletePermission(string permission);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables the create button
|
||||
/// </summary>
|
||||
/// <param name="show">the specified state</param>
|
||||
/// <returns></returns>
|
||||
IAdminPageGenerator<TModel> ShowCreateButton(bool show);
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables the delete button
|
||||
/// </summary>
|
||||
/// <param name="show">the specified state</param>
|
||||
/// <returns></returns>
|
||||
IAdminPageGenerator<TModel> ShowDeleteButton(bool show);
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables the update button
|
||||
/// </summary>
|
||||
/// <param name="show">the specified state</param>
|
||||
/// <returns></returns>
|
||||
IAdminPageGenerator<TModel> ShowUpdateButton(bool show);
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the default sort property and direction
|
||||
/// </summary>
|
||||
/// <param name="propertyExpression">Which property should be sorted</param>
|
||||
/// <param name="direction">In which direction should be sorted</param>
|
||||
/// <returns></returns>
|
||||
IAdminPageGenerator<TModel> DefaultSort<TProperty>(Expression<Func<TModel, TProperty>> propertyExpression, ListSortDirection direction);
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the repository provider for the page
|
||||
/// </summary>
|
||||
/// <typeparam name="TRepository">The specified provider</typeparam>
|
||||
/// <returns></returns>
|
||||
IAdminPageGenerator<TModel> ConfigureProvider<TRepository>() where TRepository : ModelProvider<TModel>;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the generator of the specified property
|
||||
/// </summary>
|
||||
/// <param name="propertyExpression">The property</param>
|
||||
/// <returns></returns>
|
||||
IAdminPropertyGenerator<TProperty, TModel> Property<TProperty>(Expression<Func<TModel, TProperty>> propertyExpression);
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the default property that should be displayed as a property in other listings
|
||||
/// </summary>
|
||||
/// <param name="propertyExpression">The property</param>
|
||||
/// <returns></returns>
|
||||
IAdminPageGenerator<TModel> ListingProperty<TProperty>(Expression<Func<TModel, TProperty>> propertyExpression);
|
||||
|
||||
}
|
||||
123
src/HopFrame.Web.Admin/Generators/IAdminPropertyGenerator.cs
Normal file
123
src/HopFrame.Web.Admin/Generators/IAdminPropertyGenerator.cs
Normal file
@@ -0,0 +1,123 @@
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace HopFrame.Web.Admin.Generators;
|
||||
|
||||
public interface IAdminPropertyGenerator<TProperty, TModel> {
|
||||
|
||||
/// <summary>
|
||||
/// Should the property be sortable or not
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IAdminPropertyGenerator<TProperty, TModel> Sortable(bool sortable);
|
||||
|
||||
/// <summary>
|
||||
/// Should the admin be able to edit the property after creation or not
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IAdminPropertyGenerator<TProperty, TModel> Editable(bool editable);
|
||||
|
||||
/// <summary>
|
||||
/// Should the value of the property be displayed while editing or not (useful for passwords and tokens)
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IAdminPropertyGenerator<TProperty, TModel> DisplayValueWhileEditing(bool display);
|
||||
|
||||
/// <summary>
|
||||
/// Should the property be a column on the page list or not
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IAdminPropertyGenerator<TProperty, TModel> DisplayInListing(bool display = true);
|
||||
|
||||
/// <summary>
|
||||
/// Should the property be ignored completely
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IAdminPropertyGenerator<TProperty, TModel> Ignore(bool ignore = true);
|
||||
|
||||
/// <summary>
|
||||
/// Is the value of the property database generated and is not meant to be changed
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IAdminPropertyGenerator<TProperty, TModel> Generated(bool generated = true);
|
||||
|
||||
/// <summary>
|
||||
/// Should the property value be bold in the listing or not
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IAdminPropertyGenerator<TProperty, TModel> Bold(bool bold = true);
|
||||
|
||||
/// <summary>
|
||||
/// Is the value of the property unique under all other entries in the dataset
|
||||
/// </summary>
|
||||
/// <param name="unique"></param>
|
||||
/// <returns></returns>
|
||||
IAdminPropertyGenerator<TProperty, TModel> Unique(bool unique = true);
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the display name in the listing and editing/creation
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IAdminPropertyGenerator<TProperty, TModel> DisplayName(string displayName);
|
||||
|
||||
/// <summary>
|
||||
/// Has the value of the property a never changing prefix that doesn't need to be specified or displayed
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IAdminPropertyGenerator<TProperty, TModel> Prefix(string prefix);
|
||||
|
||||
/// <summary>
|
||||
/// The specified function gets called before creation/edit to verify that the entered value matches the property requirements
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IAdminPropertyGenerator<TProperty, TModel> Validator(Func<TProperty, string> validator);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the input type in creation/edit to a selector for the property type. The property type needs to have its own admin page in order for the selector to work!
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IAdminPropertyGenerator<TProperty, TModel> IsSelector(bool selector = true);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the input type in creation/edit to a selector for the specified type. The specified type needs to have its own admin page in order for the selector to work!
|
||||
/// </summary>
|
||||
/// <param name="selector"></param>
|
||||
/// <typeparam name="TSelectorType"></typeparam>
|
||||
/// <returns></returns>
|
||||
IAdminPropertyGenerator<TProperty, TModel> IsSelector<TSelectorType>(bool selector = true);
|
||||
|
||||
/// <summary>
|
||||
/// The specified function gets called, whenever the entry is changed/created in order to convert the raw string input to the proper property type
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IAdminPropertyGenerator<TProperty, TModel> Parser(Func<TModel, string, TProperty> parser);
|
||||
|
||||
/// <summary>
|
||||
/// The specified function gets called, whenever the entry is changed/created in order to convert the raw string input to the proper property type
|
||||
/// </summary>
|
||||
/// <typeparam name="TInput">Needs to be specified if the field is not a plain string field (like a selector with a different type)</typeparam>
|
||||
/// <returns></returns>
|
||||
IAdminPropertyGenerator<TProperty, TModel> Parser<TInput>(Func<TModel, TInput, TProperty> parser);
|
||||
|
||||
/// <summary>
|
||||
/// The specified function gets called, whenever the entry is changed/created in order to convert the raw string input to the proper property type
|
||||
/// </summary>
|
||||
/// <typeparam name="TInput">Needs to be specified if the field is not a plain string field (like a selector with a different type)</typeparam>
|
||||
/// <typeparam name="TInnerProperty">Needs to be specified if the property type is a List</typeparam>
|
||||
/// <returns></returns>
|
||||
IAdminPropertyGenerator<TProperty, TModel> Parser<TInput, TInnerProperty>(Func<TModel, TInput, TInnerProperty> parser);
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the default property that should be displayed as a value
|
||||
/// </summary>
|
||||
/// <param name="propertyExpression"></param>
|
||||
/// <returns></returns>
|
||||
IAdminPropertyGenerator<TProperty, TModel> DisplayProperty(Expression<Func<TProperty, object>> propertyExpression);
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the default property that should be displayed as a value
|
||||
/// </summary>
|
||||
/// <typeparam name="TInnerProperty">Needs to be specified if the property type is a List</typeparam>
|
||||
/// <returns></returns>
|
||||
IAdminPropertyGenerator<TProperty, TModel> DisplayProperty<TInnerProperty>(Expression<Func<TInnerProperty, object>> propertyExpression);
|
||||
|
||||
}
|
||||
11
src/HopFrame.Web.Admin/Generators/IGenerator.cs
Normal file
11
src/HopFrame.Web.Admin/Generators/IGenerator.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace HopFrame.Web.Admin.Generators;
|
||||
|
||||
public interface IGenerator<out TGeneratedType> {
|
||||
|
||||
/// <summary>
|
||||
/// Compiles the generator with all specified options
|
||||
/// </summary>
|
||||
/// <returns>The compiled data structure</returns>
|
||||
TGeneratedType Compile();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
using HopFrame.Web.Admin.Attributes;
|
||||
using HopFrame.Web.Admin.Models;
|
||||
|
||||
namespace HopFrame.Web.Admin.Generators.Implementation;
|
||||
|
||||
internal class AdminContextGenerator : IAdminContextGenerator {
|
||||
|
||||
private readonly IDictionary<Type, object> _adminPages = new Dictionary<Type, object>();
|
||||
|
||||
public IAdminPageGenerator<TModel> Page<TModel>() {
|
||||
if (_adminPages.TryGetValue(typeof(TModel), out var pageGenerator))
|
||||
return pageGenerator as IAdminPageGenerator<TModel>;
|
||||
|
||||
var generator = Activator.CreateInstance(typeof(IAdminPageGenerator<TModel>)) as AdminPageGenerator<TModel>;
|
||||
generator?.ApplyConfigurationFromAttributes(typeof(TModel).GetCustomAttributes(false));
|
||||
|
||||
_adminPages.Add(typeof(TModel), generator);
|
||||
|
||||
return generator;
|
||||
}
|
||||
|
||||
public AdminPage<TModel> CompilePage<TModel>() {
|
||||
var generator = _adminPages[typeof(TModel)];
|
||||
if (generator is null) return null;
|
||||
|
||||
return (generator as AdminPageGenerator<TModel>)?.Compile();
|
||||
}
|
||||
|
||||
public TContext CompileContext<TContext>(IServiceProvider provider) where TContext : AdminPagesContext {
|
||||
var type = typeof(TContext);
|
||||
var compileMethod = typeof(AdminContextGenerator).GetMethod(nameof(CompilePage));
|
||||
|
||||
var properties = type.GetProperties();
|
||||
|
||||
var dependencies = ResolveDependencies<TContext>(provider);
|
||||
var context = Activator.CreateInstance(type, dependencies) as TContext;
|
||||
|
||||
foreach (var property in properties) {
|
||||
var propertyType = property.PropertyType.GenericTypeArguments[0];
|
||||
var pageGeneratorType = typeof(AdminPageGenerator<>).MakeGenericType(propertyType);
|
||||
var generatorInstance = Activator.CreateInstance(pageGeneratorType);
|
||||
|
||||
var titleMethod = pageGeneratorType.GetMethod(nameof(AdminPageGenerator<TContext>.Title));
|
||||
titleMethod?.Invoke(generatorInstance, [property.Name]);
|
||||
|
||||
var populateMethod = pageGeneratorType.GetMethod(nameof(AdminPageGenerator<TContext>.ApplyConfigurationFromAttributes));
|
||||
populateMethod?.Invoke(generatorInstance, [propertyType.GetCustomAttributes(false)]);
|
||||
|
||||
_adminPages.Add(propertyType, generatorInstance);
|
||||
}
|
||||
|
||||
context?.OnModelCreating(this);
|
||||
|
||||
foreach (var property in properties) {
|
||||
var modelType = property.PropertyType.GenericTypeArguments[0];
|
||||
var method = compileMethod?.MakeGenericMethod(modelType);
|
||||
var compiledPage = method?.Invoke(this, []) as AdminPage;
|
||||
|
||||
var url = property.Name;
|
||||
if (property.GetCustomAttributes(false).Any(a => a is AdminPageUrlAttribute)) {
|
||||
var attribute = property.GetCustomAttributes(false)
|
||||
.Single(a => a is AdminPageUrlAttribute) as AdminPageUrlAttribute;
|
||||
|
||||
url = attribute?.Url;
|
||||
}
|
||||
compiledPage!.Url = url;
|
||||
|
||||
property.SetValue(context, compiledPage);
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
private object[] ResolveDependencies<TContext>(IServiceProvider provider) {
|
||||
return ResolveDependencies(typeof(TContext), provider);
|
||||
}
|
||||
|
||||
public static object[] ResolveDependencies(Type type, IServiceProvider provider) {
|
||||
var ctors = type.GetConstructors();
|
||||
|
||||
if (ctors.Length == 0) return [];
|
||||
if (ctors.Length > 1)
|
||||
throw new ArgumentException($"Dependencies of {type.Name} could not be resolved (multiple constructors)!");
|
||||
|
||||
var ctor = ctors[0];
|
||||
var depTypes = ctor.GetParameters();
|
||||
var dependencies = new object[depTypes.Length];
|
||||
|
||||
for (var i = 0; i < depTypes.Length; i++) {
|
||||
dependencies[i] = provider.GetService(depTypes[i].ParameterType);
|
||||
}
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
using System.ComponentModel;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using HopFrame.Web.Admin.Attributes;
|
||||
using HopFrame.Web.Admin.Attributes.Classes;
|
||||
using HopFrame.Web.Admin.Models;
|
||||
|
||||
namespace HopFrame.Web.Admin.Generators.Implementation;
|
||||
|
||||
internal sealed class AdminPageGenerator<TModel> : IAdminPageGenerator<TModel>, IGenerator<AdminPage<TModel>> {
|
||||
|
||||
public readonly AdminPage<TModel> Page;
|
||||
private readonly Dictionary<string, object> _propertyGenerators;
|
||||
|
||||
public AdminPageGenerator() {
|
||||
Page = new AdminPage<TModel> {
|
||||
Permissions = new AdminPagePermissions(),
|
||||
ModelType = typeof(TModel)
|
||||
};
|
||||
_propertyGenerators = new Dictionary<string, object>();
|
||||
|
||||
var type = typeof(TModel);
|
||||
var properties = type.GetProperties();
|
||||
var generatorType = typeof(AdminPropertyGenerator<,>);
|
||||
|
||||
foreach (var property in properties) {
|
||||
var attributes = property.GetCustomAttributes(false);
|
||||
var genericType = generatorType.MakeGenericType(property.PropertyType, type);
|
||||
|
||||
var generator = Activator.CreateInstance(genericType, [property.Name, property.PropertyType]);
|
||||
|
||||
var method = genericType
|
||||
.GetMethod(nameof(AdminPropertyGenerator<object, object>.ApplyConfigurationFromAttributes))?
|
||||
.MakeGenericMethod(type);
|
||||
method?.Invoke(generator, [this, attributes, property]);
|
||||
|
||||
_propertyGenerators.Add(property.Name, generator);
|
||||
}
|
||||
}
|
||||
|
||||
public IAdminPageGenerator<TModel> Title(string title) {
|
||||
Page.Title = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IAdminPageGenerator<TModel> Description(string description) {
|
||||
Page.Description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IAdminPageGenerator<TModel> ViewPermission(string permission) {
|
||||
Page.Permissions.View = permission;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IAdminPageGenerator<TModel> CreatePermission(string permission) {
|
||||
Page.Permissions.Create = permission;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IAdminPageGenerator<TModel> UpdatePermission(string permission) {
|
||||
Page.Permissions.Update = permission;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IAdminPageGenerator<TModel> DeletePermission(string permission) {
|
||||
Page.Permissions.Delete = permission;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IAdminPageGenerator<TModel> ShowCreateButton(bool show) {
|
||||
Page.ShowCreateButton = show;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IAdminPageGenerator<TModel> ShowDeleteButton(bool show) {
|
||||
Page.ShowDeleteButton = show;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IAdminPageGenerator<TModel> ShowUpdateButton(bool show) {
|
||||
Page.ShowUpdateButton = show;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IAdminPageGenerator<TModel> DefaultSort<TProperty>(Expression<Func<TModel, TProperty>> propertyExpression, ListSortDirection direction) {
|
||||
var property = GetPropertyInfo(propertyExpression);
|
||||
|
||||
Page.DefaultSortPropertyName = property.Name;
|
||||
Page.DefaultSortDirection = direction;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IAdminPageGenerator<TModel> ConfigureProvider<TRepository>() where TRepository : ModelProvider<TModel> {
|
||||
Page.RepositoryProvider = typeof(TRepository);
|
||||
return this;
|
||||
}
|
||||
|
||||
public IAdminPropertyGenerator<TProperty, TModel> Property<TProperty>(Expression<Func<TModel, TProperty>> propertyExpression) {
|
||||
var property = GetPropertyInfo(propertyExpression);
|
||||
|
||||
if (_propertyGenerators.TryGetValue(property.Name, out var propertyGenerator))
|
||||
return propertyGenerator as AdminPropertyGenerator<TProperty, TModel>;
|
||||
|
||||
var generator = Activator.CreateInstance(typeof(AdminPropertyGenerator<TProperty, TModel>), new { property.Name, property.PropertyType }) as AdminPropertyGenerator<TProperty, TModel>;
|
||||
generator?.ApplyConfigurationFromAttributes(this, property.GetCustomAttributes(false), property);
|
||||
_propertyGenerators.Add(property.Name, generator);
|
||||
|
||||
return generator;
|
||||
}
|
||||
|
||||
public IAdminPageGenerator<TModel> ListingProperty<TProperty>(Expression<Func<TModel, TProperty>> propertyExpression) {
|
||||
var property = GetPropertyInfo(propertyExpression);
|
||||
Page.ListingProperty = property.Name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AdminPage<TModel> Compile() {
|
||||
var properties = new List<AdminPageProperty>();
|
||||
|
||||
foreach (var generator in _propertyGenerators.Values) {
|
||||
var method = generator.GetType().GetMethod(nameof(AdminPropertyGenerator<object, object>.Compile));
|
||||
var prop = method?.Invoke(generator, []) as AdminPageProperty;
|
||||
properties.Add(prop);
|
||||
}
|
||||
|
||||
Page.Properties = properties;
|
||||
|
||||
return Page;
|
||||
}
|
||||
|
||||
public static PropertyInfo GetPropertyInfo<TSource, TProperty>(Expression<Func<TSource, TProperty>> propertyLambda) {
|
||||
if (propertyLambda.Body is not MemberExpression member) {
|
||||
throw new ArgumentException($"Expression '{propertyLambda}' refers to a method, not a property.");
|
||||
}
|
||||
|
||||
if (member.Member is not PropertyInfo propInfo) {
|
||||
throw new ArgumentException($"Expression '{propertyLambda}' refers to a field, not a property.");
|
||||
}
|
||||
|
||||
Type type = typeof(TSource);
|
||||
if (propInfo.ReflectedType != null && type != propInfo.ReflectedType &&
|
||||
!type.IsSubclassOf(propInfo.ReflectedType)) {
|
||||
throw new ArgumentException($"Expression '{propertyLambda}' refers to a property that is not from type {type}.");
|
||||
}
|
||||
|
||||
if (propInfo.Name is null)
|
||||
throw new ArgumentException($"Expression '{propertyLambda}' refers a not existing property.");
|
||||
|
||||
return propInfo;
|
||||
}
|
||||
|
||||
public void ApplyConfigurationFromAttributes(object[] attributes) {
|
||||
if (attributes.Any(a => a is AdminNameAttribute)) {
|
||||
var attribute = attributes.Single(a => a is AdminNameAttribute) as AdminNameAttribute;
|
||||
Title(attribute?.Name);
|
||||
}
|
||||
|
||||
if (attributes.Any(a => a is AdminDescriptionAttribute)) {
|
||||
var attribute = attributes.Single(a => a is AdminDescriptionAttribute) as AdminDescriptionAttribute;
|
||||
Description(attribute?.Description);
|
||||
}
|
||||
|
||||
if (attributes.Any(a => a is AdminPermissionsAttribute)) {
|
||||
var attribute = attributes.Single(a => a is AdminPermissionsAttribute) as AdminPermissionsAttribute;
|
||||
CreatePermission(attribute?.Permissions.Create);
|
||||
UpdatePermission(attribute?.Permissions.Update);
|
||||
ViewPermission(attribute?.Permissions.View);
|
||||
DeletePermission(attribute?.Permissions.Delete);
|
||||
}
|
||||
|
||||
if (attributes.Any(a => a is AdminButtonConfigAttribute)) {
|
||||
var attribute = attributes.Single(a => a is AdminButtonConfigAttribute) as AdminButtonConfigAttribute;
|
||||
ShowCreateButton(attribute?.ShowCreateButton == true);
|
||||
ShowUpdateButton(attribute?.ShowUpdateButton == true);
|
||||
ShowDeleteButton(attribute?.ShowDeleteButton == true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using HopFrame.Web.Admin.Attributes;
|
||||
using HopFrame.Web.Admin.Attributes.Members;
|
||||
using HopFrame.Web.Admin.Models;
|
||||
|
||||
namespace HopFrame.Web.Admin.Generators.Implementation;
|
||||
|
||||
internal sealed class AdminPropertyGenerator<TProperty, TModel>(string name, Type type) : IAdminPropertyGenerator<TProperty, TModel>, IGenerator<AdminPageProperty> {
|
||||
|
||||
private readonly AdminPageProperty _property = new() {
|
||||
Name = name,
|
||||
Type = type
|
||||
};
|
||||
|
||||
public IAdminPropertyGenerator<TProperty, TModel> Sortable(bool sortable) {
|
||||
_property.Sortable = sortable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IAdminPropertyGenerator<TProperty, TModel> Editable(bool editable) {
|
||||
_property.Editable = editable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IAdminPropertyGenerator<TProperty, TModel> DisplayValueWhileEditing(bool display) {
|
||||
_property.EditDisplayValue = display;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IAdminPropertyGenerator<TProperty, TModel> DisplayInListing(bool display = true) {
|
||||
_property.DisplayInListing = display;
|
||||
_property.Sortable = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IAdminPropertyGenerator<TProperty, TModel> Ignore(bool ignore = false) {
|
||||
_property.Ignore = ignore;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IAdminPropertyGenerator<TProperty, TModel> Generated(bool generated = true) {
|
||||
_property.Generated = generated;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IAdminPropertyGenerator<TProperty, TModel> Bold(bool bold = true) {
|
||||
_property.Bold = bold;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IAdminPropertyGenerator<TProperty, TModel> Unique(bool unique = true) {
|
||||
_property.Unique = unique;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IAdminPropertyGenerator<TProperty, TModel> DisplayName(string displayName) {
|
||||
_property.DisplayName = displayName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IAdminPropertyGenerator<TProperty, TModel> Prefix(string prefix) {
|
||||
_property.Prefix = prefix;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IAdminPropertyGenerator<TProperty, TModel> Validator(Func<TProperty, string> validator) {
|
||||
_property.Validator = o => validator.Invoke((TProperty)o);
|
||||
return this;
|
||||
}
|
||||
|
||||
public IAdminPropertyGenerator<TProperty, TModel> IsSelector(bool selector = true) {
|
||||
_property.Selector = selector;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IAdminPropertyGenerator<TProperty, TModel> IsSelector<TSelectorType>(bool selector = true) {
|
||||
_property.Selector = true;
|
||||
_property.SelectorType = typeof(TSelectorType);
|
||||
return this;
|
||||
}
|
||||
|
||||
public IAdminPropertyGenerator<TProperty, TModel> Parser(Func<TModel, string, TProperty> parser) {
|
||||
_property.Parser = (o, s) => parser.Invoke((TModel)o, s.ToString());
|
||||
return this;
|
||||
}
|
||||
|
||||
public IAdminPropertyGenerator<TProperty, TModel> Parser<TInput>(Func<TModel, TInput, TProperty> parser) {
|
||||
_property.Parser = (o, s) => parser.Invoke((TModel)o, (TInput)s);
|
||||
return this;
|
||||
}
|
||||
|
||||
public IAdminPropertyGenerator<TProperty, TModel> Parser<TInput, TInnerProperty>(Func<TModel, TInput, TInnerProperty> parser) {
|
||||
_property.Parser = (o, s) => parser.Invoke((TModel)o, (TInput)s);
|
||||
return this;
|
||||
}
|
||||
|
||||
public IAdminPropertyGenerator<TProperty, TModel> DisplayProperty(Expression<Func<TProperty, object>> propertyExpression) {
|
||||
var property = AdminPageGenerator<object>.GetPropertyInfo(propertyExpression);
|
||||
_property.DisplayPropertyName = property.Name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IAdminPropertyGenerator<TProperty, TModel> DisplayProperty<TInnerProperty>(Expression<Func<TInnerProperty, object>> propertyExpression) {
|
||||
var property = AdminPageGenerator<object>.GetPropertyInfo(propertyExpression);
|
||||
_property.DisplayPropertyName = property.Name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AdminPageProperty Compile() {
|
||||
_property.DisplayName ??= _property.Name;
|
||||
return _property;
|
||||
}
|
||||
|
||||
public void ApplyConfigurationFromAttributes<T>(AdminPageGenerator<T> pageGenerator, object[] attributes, PropertyInfo property) {
|
||||
if (attributes.Any(a => a is KeyAttribute)) {
|
||||
pageGenerator.Page.DefaultSortPropertyName = property.Name;
|
||||
Editable(false);
|
||||
Bold();
|
||||
}
|
||||
|
||||
if (attributes.Any(a => a is AdminUnsortableAttribute))
|
||||
Sortable(false);
|
||||
|
||||
if (attributes.Any(a => a is AdminUneditableAttribute))
|
||||
Editable(false);
|
||||
|
||||
if (attributes.Any(a => a is AdminUniqueAttribute))
|
||||
Unique();
|
||||
|
||||
if (attributes.Any(a => a is AdminIgnoreAttribute)) {
|
||||
var attribute = attributes.Single(a => a is AdminIgnoreAttribute) as AdminIgnoreAttribute;
|
||||
DisplayInListing(false);
|
||||
Sortable(false);
|
||||
Ignore(attribute?.OnlyForListing == false);
|
||||
}
|
||||
|
||||
if (attributes.Any(a => a is AdminHideValueAttribute))
|
||||
DisplayValueWhileEditing(false);
|
||||
|
||||
if (attributes.Any(a => a is DatabaseGeneratedAttribute))
|
||||
Generated();
|
||||
|
||||
if (attributes.Any(a => a is AdminNameAttribute)) {
|
||||
var attribute = attributes.Single(a => a is AdminNameAttribute) as AdminNameAttribute;
|
||||
DisplayName(attribute?.Name);
|
||||
}
|
||||
|
||||
if (attributes.Any(a => a is AdminBoldAttribute)) {
|
||||
var attribute = attributes.Single(a => a is AdminBoldAttribute) as AdminBoldAttribute;
|
||||
Bold(attribute?.Bold == true);
|
||||
}
|
||||
|
||||
if (attributes.Any(a => a is RequiredAttribute)) {
|
||||
_property.Required = true;
|
||||
}
|
||||
|
||||
if (attributes.Any(a => a is AdminPrefixAttribute)) {
|
||||
var attribute = attributes.Single(a => a is AdminPrefixAttribute) as AdminPrefixAttribute;
|
||||
Prefix(attribute?.Prefix);
|
||||
}
|
||||
|
||||
if (attributes.Any(a => a is ListingPropertyAttribute)) {
|
||||
var attribute = attributes.Single(a => a is ListingPropertyAttribute) as ListingPropertyAttribute;
|
||||
_property.DisplayPropertyName = property.Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/HopFrame.Web.Admin/HopFrame.Web.Admin.csproj
Normal file
23
src/HopFrame.Web.Admin/HopFrame.Web.Admin.csproj
Normal file
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
|
||||
<PackageId>HopFrame.Web.Admin</PackageId>
|
||||
<Version>2.0.0</Version>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<IsPackable>true</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="README.md" Pack="true" PackagePath="\"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
33
src/HopFrame.Web.Admin/ModelProvider.cs
Normal file
33
src/HopFrame.Web.Admin/ModelProvider.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
namespace HopFrame.Web.Admin;
|
||||
|
||||
public abstract class ModelProvider<TModel> : IModelProvider {
|
||||
public abstract Task<IEnumerable<TModel>> ReadAll();
|
||||
public abstract Task<TModel> Create(TModel model);
|
||||
public abstract Task<TModel> Update(TModel model);
|
||||
public abstract Task Delete(TModel model);
|
||||
|
||||
|
||||
public async Task<IEnumerable<object>> ReadAllO() {
|
||||
var models = await ReadAll();
|
||||
return models.Select(m => (object)m);
|
||||
}
|
||||
|
||||
public async Task<object> CreateO(object model) {
|
||||
return await Create((TModel)model);
|
||||
}
|
||||
|
||||
public async Task<object> UpdateO(object model) {
|
||||
return await Update((TModel)model);
|
||||
}
|
||||
|
||||
public Task DeleteO(object model) {
|
||||
return Delete((TModel)model);
|
||||
}
|
||||
}
|
||||
|
||||
public interface IModelProvider {
|
||||
Task<IEnumerable<object>> ReadAllO();
|
||||
Task<object> CreateO(object model);
|
||||
Task<object> UpdateO(object model);
|
||||
Task DeleteO(object model);
|
||||
}
|
||||
35
src/HopFrame.Web.Admin/Models/AdminPage.cs
Normal file
35
src/HopFrame.Web.Admin/Models/AdminPage.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System.ComponentModel;
|
||||
using HopFrame.Web.Admin.Generators.Implementation;
|
||||
|
||||
namespace HopFrame.Web.Admin.Models;
|
||||
|
||||
public sealed class AdminPage<TModel> : AdminPage;
|
||||
|
||||
public class AdminPage {
|
||||
public string Title { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Url { get; set; }
|
||||
public AdminPagePermissions Permissions { get; set; }
|
||||
public IList<AdminPageProperty> Properties { get; set; }
|
||||
public string ListingProperty { get; set; }
|
||||
|
||||
public Type RepositoryProvider { get; set; }
|
||||
|
||||
public Type ModelType { get; set; }
|
||||
|
||||
public string DefaultSortPropertyName { get; set; }
|
||||
public ListSortDirection DefaultSortDirection { get; set; }
|
||||
|
||||
public bool ShowCreateButton { get; set; } = true;
|
||||
public bool ShowDeleteButton { get; set; } = true;
|
||||
public bool ShowUpdateButton { get; set; } = true;
|
||||
|
||||
public IModelProvider LoadModelProvider(IServiceProvider provider) {
|
||||
if (RepositoryProvider is null) return null;
|
||||
var repoProvider = provider.GetService(RepositoryProvider);
|
||||
if (repoProvider != null) return repoProvider as IModelProvider;
|
||||
|
||||
var dependencies = AdminContextGenerator.ResolveDependencies(RepositoryProvider, provider);
|
||||
return Activator.CreateInstance(RepositoryProvider, dependencies) as IModelProvider;
|
||||
}
|
||||
}
|
||||
8
src/HopFrame.Web.Admin/Models/AdminPagePermissions.cs
Normal file
8
src/HopFrame.Web.Admin/Models/AdminPagePermissions.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace HopFrame.Web.Admin.Models;
|
||||
|
||||
public sealed class AdminPagePermissions {
|
||||
public string View { get; set; }
|
||||
public string Create { get; set; }
|
||||
public string Update { get; set; }
|
||||
public string Delete { get; set; }
|
||||
}
|
||||
37
src/HopFrame.Web.Admin/Models/AdminPageProperty.cs
Normal file
37
src/HopFrame.Web.Admin/Models/AdminPageProperty.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
namespace HopFrame.Web.Admin.Models;
|
||||
|
||||
public sealed class AdminPageProperty {
|
||||
public string Name { get; set; }
|
||||
public string DisplayName { get; set; }
|
||||
public string Prefix { get; set; }
|
||||
public string DisplayPropertyName { get; set; }
|
||||
|
||||
public bool DisplayInListing { get; set; } = true;
|
||||
public bool Sortable { get; set; } = true;
|
||||
public bool Editable { get; set; } = true;
|
||||
public bool EditDisplayValue { get; set; } = true;
|
||||
public bool Generated { get; set; }
|
||||
public bool Bold { get; set; }
|
||||
public bool Required { get; set; }
|
||||
public bool Ignore { get; set; }
|
||||
public bool Unique { get; set; }
|
||||
public bool Selector { get; set; }
|
||||
public Type SelectorType { get; set; }
|
||||
|
||||
public Type Type { get; set; }
|
||||
|
||||
public Func<object, string> Validator { get; set; }
|
||||
public Func<object, object, object> Parser { get; set; }
|
||||
|
||||
public object GetValue(object entry) {
|
||||
return entry.GetType().GetProperty(Name)?.GetValue(entry);
|
||||
}
|
||||
|
||||
public T GetValue<T>(object entry) {
|
||||
return (T)entry.GetType().GetProperty(Name)?.GetValue(entry);
|
||||
}
|
||||
|
||||
public void SetValue(object entry, object value) {
|
||||
entry.GetType().GetProperty(Name)?.SetValue(entry, value);
|
||||
}
|
||||
}
|
||||
11
src/HopFrame.Web.Admin/Providers/IAdminPagesProvider.cs
Normal file
11
src/HopFrame.Web.Admin/Providers/IAdminPagesProvider.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using HopFrame.Web.Admin.Models;
|
||||
|
||||
namespace HopFrame.Web.Admin.Providers;
|
||||
|
||||
public interface IAdminPagesProvider {
|
||||
|
||||
AdminPage LoadAdminPage(string url);
|
||||
IList<AdminPage> LoadRegisteredAdminPages();
|
||||
AdminPage HasPageFor(Type type);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using HopFrame.Web.Admin.Models;
|
||||
|
||||
namespace HopFrame.Web.Admin.Providers.Implementation;
|
||||
|
||||
public class AdminPagesProvider(IServiceProvider provider) : IAdminPagesProvider {
|
||||
private static readonly IDictionary<string, PageDataStore> Pages = new Dictionary<string, PageDataStore>();
|
||||
|
||||
public static void RegisterAdminPage<TContext>(string url, Type pageType) where TContext : AdminPagesContext {
|
||||
Pages.Add(url, new PageDataStore {
|
||||
ContextType = typeof(TContext),
|
||||
PageType = pageType
|
||||
});
|
||||
}
|
||||
|
||||
public AdminPage LoadAdminPage(string url) {
|
||||
if (!Pages.TryGetValue(url, out var data)) return null;
|
||||
|
||||
var context = provider.GetService(data.ContextType);
|
||||
var property = data.ContextType.GetProperties()
|
||||
.SingleOrDefault(prop => prop.PropertyType == data.PageType);
|
||||
|
||||
return property?.GetValue(context) as AdminPage;
|
||||
}
|
||||
|
||||
public IList<AdminPage> LoadRegisteredAdminPages() {
|
||||
return Pages
|
||||
.Select(pair => LoadAdminPage(pair.Key))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public AdminPage HasPageFor(Type type) {
|
||||
foreach (var (url, data) in Pages) {
|
||||
var innerType = data.PageType.GenericTypeArguments[0];
|
||||
if (innerType != type) continue;
|
||||
return LoadAdminPage(url);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
internal struct PageDataStore {
|
||||
public Type PageType { get; set; }
|
||||
public Type ContextType { get; set; }
|
||||
}
|
||||
4
src/HopFrame.Web.Admin/README.md
Normal file
4
src/HopFrame.Web.Admin/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# HopFrame admin pages module
|
||||
This module contains all necessary information to create and compile the generated admin pages.
|
||||
|
||||
For more information about the HopFrame visit the [docs](https://git.leon-hoppe.de/leon.hoppe/HopFrame).
|
||||
44
src/HopFrame.Web.Admin/ServiceCollectionExtensions.cs
Normal file
44
src/HopFrame.Web.Admin/ServiceCollectionExtensions.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using HopFrame.Web.Admin.Attributes;
|
||||
using HopFrame.Web.Admin.Generators.Implementation;
|
||||
using HopFrame.Web.Admin.Providers;
|
||||
using HopFrame.Web.Admin.Providers.Implementation;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace HopFrame.Web.Admin;
|
||||
|
||||
public static class ServiceCollectionExtensions {
|
||||
|
||||
public static IServiceCollection AddAdminContext<TContext>(this IServiceCollection services) where TContext : AdminPagesContext {
|
||||
services.TryAddSingleton<IAdminPagesProvider, AdminPagesProvider>();
|
||||
|
||||
services.AddSingleton(provider => {
|
||||
var generator = new AdminContextGenerator();
|
||||
var context = generator.CompileContext<TContext>(provider);
|
||||
return context;
|
||||
});
|
||||
|
||||
PreregisterPages<TContext>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static void PreregisterPages<TContext>() where TContext : AdminPagesContext {
|
||||
var contextType = typeof(TContext);
|
||||
var props = contextType.GetProperties();
|
||||
|
||||
foreach (var property in props) {
|
||||
var url = property.Name;
|
||||
|
||||
if (property.GetCustomAttributes(false).Any(a => a is AdminPageUrlAttribute)) {
|
||||
var attribute = property.GetCustomAttributes(false)
|
||||
.Single(a => a is AdminPageUrlAttribute) as AdminPageUrlAttribute;
|
||||
|
||||
url = attribute?.Url;
|
||||
}
|
||||
|
||||
AdminPagesProvider.RegisterAdminPage<TContext>(url, property.PropertyType);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
namespace HopFrame.Web;
|
||||
|
||||
[Obsolete("Use HopFrame.Security.AdminPermissions instead")]
|
||||
public static class AdminPermissions {
|
||||
public const string IsAdmin = Security.AdminPermissions.IsAdmin;
|
||||
|
||||
public const string ViewUsers = Security.AdminPermissions.ViewUsers;
|
||||
public const string EditUser = Security.AdminPermissions.EditUser;
|
||||
public const string DeleteUser = Security.AdminPermissions.DeleteUser;
|
||||
public const string AddUser = Security.AdminPermissions.AddUser;
|
||||
|
||||
public const string ViewGroups = Security.AdminPermissions.ViewGroups;
|
||||
public const string EditGroup = Security.AdminPermissions.EditGroup;
|
||||
public const string DeleteGroup = Security.AdminPermissions.DeleteGroup;
|
||||
public const string AddGroup = Security.AdminPermissions.AddGroup;
|
||||
}
|
||||
@@ -1,33 +1,35 @@
|
||||
using System.Security.Claims;
|
||||
using HopFrame.Database;
|
||||
using HopFrame.Database.Repositories;
|
||||
using HopFrame.Security.Authentication;
|
||||
using HopFrame.Security.Claims;
|
||||
using HopFrame.Security.Services;
|
||||
using HopFrame.Web.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace HopFrame.Web;
|
||||
|
||||
public sealed class AuthMiddleware(IAuthService auth, IPermissionService perms) : IMiddleware {
|
||||
/// <summary>
|
||||
/// Assures that the user stays logged in even if the access token is expired
|
||||
/// </summary>
|
||||
public sealed class AuthMiddleware(IAuthService auth, IPermissionRepository perms) : IMiddleware {
|
||||
public async Task InvokeAsync(HttpContext context, RequestDelegate next) {
|
||||
var loggedIn = await auth.IsLoggedIn();
|
||||
|
||||
if (!loggedIn) {
|
||||
var token = await auth.RefreshLogin();
|
||||
if (token is null) {
|
||||
await next.Invoke(context);
|
||||
next?.Invoke(context);
|
||||
return;
|
||||
}
|
||||
|
||||
var claims = new List<Claim> {
|
||||
new(HopFrameClaimTypes.AccessTokenId, token.Token),
|
||||
new(HopFrameClaimTypes.UserId, token.UserId)
|
||||
new(HopFrameClaimTypes.AccessTokenId, token.Content.ToString()),
|
||||
new(HopFrameClaimTypes.UserId, token.Owner.Id.ToString())
|
||||
};
|
||||
|
||||
var permissions = await perms.GetFullPermissions(token.UserId);
|
||||
var permissions = await perms.GetFullPermissions(token.Owner);
|
||||
claims.AddRange(permissions.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm)));
|
||||
|
||||
context.User.AddIdentity(new ClaimsIdentity(claims, HopFrameAuthentication<HopDbContextBase>.SchemeName));
|
||||
context.User.AddIdentity(new ClaimsIdentity(claims, HopFrameAuthentication.SchemeName));
|
||||
}
|
||||
|
||||
await next?.Invoke(context);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user