47 Commits

Author SHA1 Message Date
6c64497a08 Merge branch 'release/v2.0.0' into 'main'
Release/v2.0.0

See merge request leon.hoppe/HopFrame!13
2024-11-23 15:16:54 +00:00
b206ab65ce Changed dotnet sdk version 2024-11-23 16:12:15 +01:00
92a329ef41 Accidentally deleted ci file 2024-11-23 16:10:27 +01:00
92b7e9a8ae Renamed ci file 2024-11-23 16:07:22 +01:00
776b055cfb Added readme to HopFrame.Web.Admin.csproj + added ci scripts 2024-11-23 16:05:57 +01:00
bd7751b44b Updated versions + edited project readme's 2024-11-23 15:40:58 +01:00
f8995ca990 Renamed model repo to provider + provider registration check 2024-11-23 15:29:46 +01:00
beac2aa20c Merge branch 'feature/admincontext-dependencyinjection' into 'dev'
Resolve "Proper Admin Context dependency injection"

Closes #21

See merge request leon.hoppe/HopFrame!11
2024-11-22 17:57:39 +00:00
c00c30ea3f Made admin pages dependency injectable 2024-11-22 18:58:39 +01:00
e257e36b66 Merge branch 'feature/documentation' into 'dev'
Resolve "Documentation"

Closes #18

See merge request leon.hoppe/HopFrame!10
2024-11-22 13:17:35 +00:00
fee99c60b6 Finished documentation 2024-11-22 14:18:40 +01:00
0c2c02136d added docs for admin pages and blazor pages + fixed typos 2024-11-22 14:04:15 +01:00
Leon Hoppe
16ef41800d added endpoint documentation 2024-11-22 12:20:33 +01:00
Leon Hoppe
2bc8a5d70b working on various components of the documentation 2024-11-22 11:42:21 +01:00
Leon Hoppe
a531cd7a47 Reorganized files 2024-11-22 10:49:48 +01:00
e8c61dbc7f Fixed visual separation issues 2024-11-21 17:02:15 +01:00
01cd0e1590 Renamed Authentication to Authorization 2024-11-21 16:56:45 +01:00
eef03c152d finished documentation for installation, database usage and authentication 2024-11-21 16:55:56 +01:00
986c5cebde Added inline documentation + fixed small bugs 2024-11-21 16:15:18 +01:00
6d2f7051ee attempt to fix diagram display issue 2024-11-20 20:24:33 +01:00
6c5c5c9e9d Integrated models directly into docs 2024-11-20 20:20:48 +01:00
leonhoppe
53d214ed8b Merge pull request #9 from leonhoppe/feature/adminLogin
Feature/admin login
2024-11-19 18:19:34 +01:00
4801e790c0 finished admin login 2024-11-19 18:20:27 +01:00
61323f089d Redesigned admin login page 2024-11-14 16:20:09 +01:00
leonhoppe
94e7a41e59 Merge pull request #8 from leonhoppe/feature/generatedAdminPages
Feature/generated admin pages
2024-11-09 11:19:02 +01:00
89a3185c8b cleanup 2024-11-09 11:18:28 +01:00
bc7dfa8e6a Removed unnecessary files + added proper documentation to admin page generators 2024-11-09 11:17:50 +01:00
601b502c8c Improved developer experience for AdminContext's 2024-11-09 10:39:17 +01:00
0cc4eb44da Implemented selector properties for admin pages 2024-11-05 18:45:34 +01:00
d38cce6dc2 Added validation to admin pages 2024-10-27 15:26:25 +01:00
85a45ece55 finished list input + added proper prefix rendering 2024-10-27 11:33:50 +01:00
ce15717c7d Made AdminPageProperty generic + added dynamic display property selecting 2024-10-26 11:54:02 +02:00
599ce2bf43 Working on Admin page modal 2024-10-13 17:42:40 +02:00
d2729870e3 Started working on admin page modal 2024-10-08 21:34:00 +02:00
bc0651cb75 Added order, search and delete functionality to admin page 2024-10-08 19:21:46 +02:00
075ca2286f Worked on admin page listing 2024-10-07 17:39:05 +02:00
6a781990e4 Started creating generated admin page 2024-10-06 12:48:05 +02:00
dd67bba07d Moved populator methods to corresponding classes 2024-10-06 11:26:54 +02:00
6a110d5b8b Added AdminPages to admin dashboard and navigation + created 2.0 todo list 2024-10-06 11:09:00 +02:00
9cf818c55d Created static object provider + added some properties 2024-10-05 12:18:32 +02:00
66ddc22012 Created AdminContext handling 2024-09-30 19:01:39 +02:00
672f0fd2c3 Updated launchSettings.json of test projects 2024-09-28 21:23:42 +02:00
leonhoppe
8d280422cf Merge pull request #6 from leonhoppe/feature/relations
Feature/relations
2024-09-28 12:18:58 +02:00
cfda1bd053 Took Database layout changes into account in frontend pages 2024-09-28 12:17:07 +02:00
f71587d72e Rebuild data storage system so that database dependencies get taken into account 2024-09-26 21:06:48 +02:00
leonhoppe
1b3ffc82ff Merge pull request #5 from leonhoppe/release/v1.1.0
Release/v1.1.0
2024-09-26 12:36:52 +02:00
c3f8615eba Reverted to breaking changes 2024-09-26 12:35:53 +02:00
143 changed files with 4344 additions and 2615 deletions

34
.gitlab-ci.yml Normal file
View 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

View File

@@ -5,7 +5,7 @@
<driver-ref>sqlite.xerial</driver-ref> <driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize> <synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver> <jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$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> <jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" /> <property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
</jdbc-additional-properties> </jdbc-additional-properties>

1025
.idea/config/applicationhost.config generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Web", "src\HopFram
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrontendTest", "test\FrontendTest\FrontendTest.csproj", "{8F983A37-63CF-48D5-988D-58B78EF8AECD}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrontendTest", "test\FrontendTest\FrontendTest.csproj", "{8F983A37-63CF-48D5-988D-58B78EF8AECD}"
EndProject 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8F983A37-63CF-48D5-988D-58B78EF8AECD}.Release|Any CPU.ActiveCfg = Release|Any CPU {8F983A37-63CF-48D5-988D-58B78EF8AECD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8F983A37-63CF-48D5-988D-58B78EF8AECD}.Release|Any CPU.Build.0 = Release|Any CPU {8F983A37-63CF-48D5-988D-58B78EF8AECD}.Release|Any CPU.Build.0 = Release|Any CPU
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
EndGlobalSection EndGlobalSection

View File

@@ -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"> <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">&lt;AssemblyExplorer&gt;&#xD; <s:String x:Key="/Default/Environment/AssemblyExplorer/XmlDocument/@EntryValue">&lt;AssemblyExplorer&gt;&#xD;
&lt;Assembly Path="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\7.0.16\ref\net7.0\System.ComponentModel.Annotations.dll" /&gt;&#xD; &lt;Assembly Path="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\7.0.16\ref\net7.0\System.ComponentModel.Annotations.dll" /&gt;&#xD;
&lt;Assembly Path="C:\Users\Remote\.nuget\packages\blazorstrap\5.2.100.61524\lib\net7.0\BlazorStrap.dll" /&gt;&#xD; &lt;Assembly Path="C:\Users\Remote\.nuget\packages\blazorstrap\5.2.100.61524\lib\net7.0\BlazorStrap.dll" /&gt;&#xD;

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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.

View 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" />
```

View 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
View 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
View 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
```

View File

@@ -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 ## PermissionGroup
These are the models used by the various database services. ```csharp
public class PermissionGroup : IPermissionOwner {
public string Name { get; init; }
public bool IsDefaultGroup { get; set; }
public string Description { get; set; }
public DateTime CreatedAt { get; set; }
public virtual List<Permission> Permissions { get; set; }
}
```
![](./Diagrams/Models/img/BaseModels.svg) ## 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 ## UserLogin
These are the models used by the REST API and the Blazor API. ```csharp
public class UserLogin {
public string Email { get; set; }
public string Password { get; set; }
}
```
![](./Diagrams/Models/img/ApiModels.svg) ## UserRegister
```csharp
public class UserRegister {
public string Username { get; set; }
public string Email { get; set; }
public string Password { get; set; }
}
```
## IPermissionOwner
## Database Models ```csharp
These are the models that correspond to the scheme in the Database public interface IPermissionOwner;
```
![](./Diagrams/Models/img/DatabaseModels.svg)

25
docs/readme.md Normal file
View 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
View 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);
}
```

View File

@@ -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();
}
```

View File

@@ -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();
```

View File

@@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Mvc;
namespace HopFrame.Api.Controller; namespace HopFrame.Api.Controller;
[ApiController] [ApiController]
[Route("authentication")] [Route("api/v1/authentication")]
public class SecurityController(IAuthLogic auth) : ControllerBase { public class SecurityController(IAuthLogic auth) : ControllerBase {
[HttpPut("login")] [HttpPut("login")]

View File

@@ -27,10 +27,11 @@ public static class ServiceCollectionExtensions {
/// <param name="services">The service provider to add the services to</param> /// <param name="services">The service provider to add the services to</param>
/// <typeparam name="TDbContext">The data source for all HopFrame entities</typeparam> /// <typeparam name="TDbContext">The data source for all HopFrame entities</typeparam>
public static void AddHopFrameNoEndpoints<TDbContext>(this IServiceCollection services) where TDbContext : HopDbContextBase { public static void AddHopFrameNoEndpoints<TDbContext>(this IServiceCollection services) where TDbContext : HopDbContextBase {
services.AddHopFrameRepositories<TDbContext>();
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>(); services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<IAuthLogic, AuthLogic<TDbContext>>(); services.AddScoped<IAuthLogic, AuthLogic>();
services.AddHopFrameAuthentication<TDbContext>(); services.AddHopFrameAuthentication();
} }
} }

View File

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

View File

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

View File

@@ -1,16 +1,14 @@
using HopFrame.Api.Models; using HopFrame.Api.Models;
using HopFrame.Database; using HopFrame.Database.Models;
using HopFrame.Database.Models.Entries; using HopFrame.Database.Repositories;
using HopFrame.Security.Authentication; using HopFrame.Security.Authentication;
using HopFrame.Security.Claims; using HopFrame.Security.Claims;
using HopFrame.Security.Models; using HopFrame.Security.Models;
using HopFrame.Security.Services;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
namespace HopFrame.Api.Logic.Implementation; namespace HopFrame.Api.Logic.Implementation;
public class AuthLogic<TDbContext>(TDbContext context, IUserService users, ITokenContext tokenContext, IHttpContextAccessor accessor) : IAuthLogic where TDbContext : HopDbContextBase { public class AuthLogic(IUserRepository users, ITokenRepository tokens, ITokenContext tokenContext, IHttpContextAccessor accessor) : IAuthLogic {
public async Task<LogicResult<SingleValueResult<string>>> Login(UserLogin login) { public async Task<LogicResult<SingleValueResult<string>>> Login(UserLogin login) {
var user = await users.GetUserByEmail(login.Email); 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)) if (!await users.CheckUserPassword(user, login.Password))
return LogicResult<SingleValueResult<string>>.Forbidden("The provided password is not correct"); return LogicResult<SingleValueResult<string>>.Forbidden("The provided password is not correct");
var refreshToken = new TokenEntry { var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user);
CreatedAt = DateTime.Now, var accessToken = await tokens.CreateToken(Token.AccessTokenType, user);
Token = Guid.NewGuid().ToString(),
Type = TokenEntry.RefreshTokenType,
UserId = user.Id.ToString()
};
var accessToken = new TokenEntry {
CreatedAt = DateTime.Now,
Token = Guid.NewGuid().ToString(),
Type = TokenEntry.AccessTokenType,
UserId = user.Id.ToString()
};
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Token, new CookieOptions { accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions {
MaxAge = HopFrameAuthentication<TDbContext>.RefreshTokenTime, MaxAge = HopFrameAuthentication.RefreshTokenTime,
HttpOnly = true, HttpOnly = true,
Secure = true Secure = true
}); });
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Token, new CookieOptions { accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
MaxAge = HopFrameAuthentication<TDbContext>.AccessTokenTime, MaxAge = HopFrameAuthentication.AccessTokenTime,
HttpOnly = true, HttpOnly = true,
Secure = true Secure = true
}); });
await context.Tokens.AddRangeAsync(refreshToken, accessToken); return LogicResult<SingleValueResult<string>>.Ok(accessToken.Content.ToString());
await context.SaveChangesAsync();
return LogicResult<SingleValueResult<string>>.Ok(accessToken.Token);
} }
public async Task<LogicResult<SingleValueResult<string>>> Register(UserRegister register) { public async Task<LogicResult<SingleValueResult<string>>> Register(UserRegister register) {
@@ -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)) if (allUsers.Any(user => user.Username == register.Username || user.Email == register.Email))
return LogicResult<SingleValueResult<string>>.Conflict("Username or Email is already registered"); return LogicResult<SingleValueResult<string>>.Conflict("Username or Email is already registered");
var user = await users.AddUser(register); var user = await users.AddUser(new User {
Username = register.Username,
Email = register.Email,
Password = register.Password
});
var refreshToken = new TokenEntry { var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user);
CreatedAt = DateTime.Now, var accessToken = await tokens.CreateToken(Token.AccessTokenType, user);
Token = Guid.NewGuid().ToString(),
Type = TokenEntry.RefreshTokenType,
UserId = user.Id.ToString()
};
var accessToken = new TokenEntry {
CreatedAt = DateTime.Now,
Token = Guid.NewGuid().ToString(),
Type = TokenEntry.AccessTokenType,
UserId = user.Id.ToString()
};
await context.Tokens.AddRangeAsync(refreshToken, accessToken); accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions {
await context.SaveChangesAsync(); MaxAge = HopFrameAuthentication.RefreshTokenTime,
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Token, new CookieOptions {
MaxAge = HopFrameAuthentication<TDbContext>.RefreshTokenTime,
HttpOnly = true, HttpOnly = true,
Secure = true Secure = true
}); });
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Token, new CookieOptions { accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
MaxAge = HopFrameAuthentication<TDbContext>.AccessTokenTime, MaxAge = HopFrameAuthentication.AccessTokenTime,
HttpOnly = false, HttpOnly = false,
Secure = true Secure = true
}); });
return LogicResult<SingleValueResult<string>>.Ok(accessToken.Token); return LogicResult<SingleValueResult<string>>.Ok(accessToken.Content.ToString());
} }
public async Task<LogicResult<SingleValueResult<string>>> Authenticate() { public async Task<LogicResult<SingleValueResult<string>>> Authenticate() {
@@ -97,31 +73,26 @@ public class AuthLogic<TDbContext>(TDbContext context, IUserService users, IToke
if (string.IsNullOrEmpty(refreshToken)) if (string.IsNullOrEmpty(refreshToken))
return LogicResult<SingleValueResult<string>>.Conflict("Refresh token not provided"); 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) if (token is null)
return LogicResult<SingleValueResult<string>>.NotFound("Refresh token not valid"); return LogicResult<SingleValueResult<string>>.NotFound("Refresh token not valid");
if (token.CreatedAt + HopFrameAuthentication<TDbContext>.RefreshTokenTime < DateTime.Now) if (token.CreatedAt + HopFrameAuthentication.RefreshTokenTime < DateTime.Now)
return LogicResult<SingleValueResult<string>>.Conflict("Refresh token is expired"); return LogicResult<SingleValueResult<string>>.Conflict("Refresh token is expired");
var accessToken = new TokenEntry { var accessToken = await tokens.CreateToken(Token.AccessTokenType, token.Owner);
CreatedAt = DateTime.Now,
Token = Guid.NewGuid().ToString(),
Type = TokenEntry.AccessTokenType,
UserId = token.UserId
};
await context.Tokens.AddAsync(accessToken);
await context.SaveChangesAsync();
accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Token, new CookieOptions { accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions {
MaxAge = HopFrameAuthentication<TDbContext>.AccessTokenTime, MaxAge = HopFrameAuthentication.AccessTokenTime,
HttpOnly = false, HttpOnly = false,
Secure = true Secure = true
}); });
return LogicResult<SingleValueResult<string>>.Ok(accessToken.Token); return LogicResult<SingleValueResult<string>>.Ok(accessToken.Content.ToString());
} }
public async Task<LogicResult> Logout() { 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)) if (string.IsNullOrEmpty(accessToken) || string.IsNullOrEmpty(refreshToken))
return LogicResult.Conflict("access or refresh token not provided"); return LogicResult.Conflict("access or refresh token not provided");
var tokenEntries = await context.Tokens.Where(token => await tokens.DeleteUserTokens(tokenContext.User);
(token.Token == accessToken && token.Type == TokenEntry.AccessTokenType) ||
(token.Token == refreshToken && token.Type == TokenEntry.RefreshTokenType))
.ToArrayAsync();
if (tokenEntries.Length != 2)
return LogicResult.NotFound("One or more of the provided tokens was not found");
context.Tokens.Remove(tokenEntries[0]);
context.Tokens.Remove(tokenEntries[1]);
await context.SaveChangesAsync();
accessor.HttpContext?.Response.Cookies.Delete(ITokenContext.RefreshTokenType); accessor.HttpContext?.Response.Cookies.Delete(ITokenContext.RefreshTokenType);
accessor.HttpContext?.Response.Cookies.Delete(ITokenContext.AccessTokenType); accessor.HttpContext?.Response.Cookies.Delete(ITokenContext.AccessTokenType);

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
using HopFrame.Database.Models.Entries; using HopFrame.Database.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace HopFrame.Database; namespace HopFrame.Database;
@@ -8,25 +8,27 @@ namespace HopFrame.Database;
/// </summary> /// </summary>
public abstract class HopDbContextBase : DbContext { public abstract class HopDbContextBase : DbContext {
public virtual DbSet<UserEntry> Users { get; set; } public virtual DbSet<User> Users { get; set; }
public virtual DbSet<PermissionEntry> Permissions { get; set; } public virtual DbSet<Permission> Permissions { get; set; }
public virtual DbSet<TokenEntry> Tokens { get; set; } public virtual DbSet<Token> Tokens { get; set; }
public virtual DbSet<GroupEntry> Groups { get; set; } public virtual DbSet<PermissionGroup> Groups { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) { protected override void OnModelCreating(ModelBuilder modelBuilder) {
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
modelBuilder.Entity<UserEntry>(); modelBuilder.Entity<User>()
modelBuilder.Entity<PermissionEntry>(); .HasMany(u => u.Tokens)
modelBuilder.Entity<TokenEntry>(); .WithOne(t => t.Owner)
modelBuilder.Entity<GroupEntry>(); .OnDelete(DeleteBehavior.Cascade);
}
/// <summary> modelBuilder.Entity<User>()
/// Gets executed when a user is deleted through the IUserService from the .HasMany(u => u.Permissions)
/// HopFrame.Security package. You can override this method to also delete .WithOne(p => p.User)
/// related user specific entries in the database .OnDelete(DeleteBehavior.Cascade);
/// </summary>
/// <param name="user"></param> modelBuilder.Entity<PermissionGroup>()
public virtual void OnUserDelete(UserEntry user) {} .HasMany(g => g.Permissions)
.WithOne(p => p.Group)
.OnDelete(DeleteBehavior.Cascade);
}
} }

View File

@@ -7,13 +7,14 @@
<Nullable>disable</Nullable> <Nullable>disable</Nullable>
<PackageId>HopFrame.Database</PackageId> <PackageId>HopFrame.Database</PackageId>
<Version>1.1.0</Version> <Version>2.0.0</Version>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<IsPackable>true</IsPackable> <IsPackable>true</IsPackable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="8.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7" />
</ItemGroup> </ItemGroup>

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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;
}
}

View File

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

View File

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

View File

@@ -1,8 +1,10 @@
using System.ComponentModel.DataAnnotations; 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 RefreshTokenType = 0;
public const int AccessTokenType = 1; public const int AccessTokenType = 1;
@@ -15,11 +17,11 @@ public class TokenEntry {
public int Type { get; set; } public int Type { get; set; }
[Key, Required, MinLength(36), MaxLength(36)] [Key, Required, MinLength(36), MaxLength(36)]
public string Token { get; set; } public Guid Content { get; set; }
[Required, MinLength(36), MaxLength(36)]
public string UserId { get; set; }
[Required] [Required]
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
[ForeignKey("UserId"), JsonIgnore]
public virtual User Owner { get; set; }
} }

View File

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

View File

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

View File

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

View 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);
}

View 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);
}

View 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);
}

View File

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

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View 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;
}
}

View File

@@ -1,10 +1,8 @@
using System.Security.Claims; using System.Security.Claims;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using HopFrame.Database; using HopFrame.Database.Repositories;
using HopFrame.Security.Claims; using HopFrame.Security.Claims;
using HopFrame.Security.Services;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@@ -13,15 +11,15 @@ using Microsoft.Extensions.Options;
namespace HopFrame.Security.Authentication; namespace HopFrame.Security.Authentication;
public class HopFrameAuthentication<TDbContext>( public class HopFrameAuthentication(
IOptionsMonitor<AuthenticationSchemeOptions> options, IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, ILoggerFactory logger,
UrlEncoder encoder, UrlEncoder encoder,
ISystemClock clock, ISystemClock clock,
TDbContext context, ITokenRepository tokens,
IPermissionService perms) IUserRepository users,
: AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder, clock) IPermissionRepository perms)
where TDbContext : HopDbContextBase { : AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder, clock) {
public const string SchemeName = "HopCore.Authentication"; public const string SchemeName = "HopCore.Authentication";
public static readonly TimeSpan AccessTokenTime = new(0, 0, 5, 0); public static readonly TimeSpan AccessTokenTime = new(0, 0, 5, 0);
@@ -32,21 +30,21 @@ public class HopFrameAuthentication<TDbContext>(
if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Headers[SchemeName]; if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Headers[SchemeName];
if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Headers["Token"]; if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Headers["Token"];
if (string.IsNullOrEmpty(accessToken)) return AuthenticateResult.Fail("No Access Token provided"); if (string.IsNullOrEmpty(accessToken)) return AuthenticateResult.Fail("No Access Token provided");
var tokenEntry = await context.Tokens.SingleOrDefaultAsync(token => token.Token == accessToken); var tokenEntry = await tokens.GetToken(accessToken);
if (tokenEntry is null) return AuthenticateResult.Fail("The provided Access Token does not exist"); if (tokenEntry is null) return AuthenticateResult.Fail("The provided Access Token does not exist");
if (tokenEntry.CreatedAt + AccessTokenTime < DateTime.Now) return AuthenticateResult.Fail("The provided Access Token is expired"); if (tokenEntry.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"); return AuthenticateResult.Fail("The provided Access Token does not match any user");
var claims = new List<Claim> { var claims = new List<Claim> {
new(HopFrameClaimTypes.AccessTokenId, accessToken), 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))); claims.AddRange(permissions.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm)));
var principal = new ClaimsPrincipal(); var principal = new ClaimsPrincipal();

View File

@@ -1,7 +1,4 @@
using HopFrame.Database;
using HopFrame.Security.Claims; using HopFrame.Security.Claims;
using HopFrame.Security.Services;
using HopFrame.Security.Services.Implementation;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@@ -17,13 +14,11 @@ public static class HopFrameAuthenticationExtensions {
/// <param name="service">The service provider to add the services to</param> /// <param name="service">The service provider to add the services to</param>
/// <typeparam name="TDbContext">The database object that saves all entities that are important for the security api</typeparam> /// <typeparam name="TDbContext">The database object that saves all entities that are important for the security api</typeparam>
/// <returns></returns> /// <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.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
service.AddScoped<ITokenContext, TokenContextImplementor<TDbContext>>(); service.AddScoped<ITokenContext, TokenContextImplementor>();
service.AddScoped<IPermissionService, PermissionService<TDbContext>>();
service.AddScoped<IUserService, UserService<TDbContext>>();
service.AddAuthentication(HopFrameAuthentication<TDbContext>.SchemeName).AddScheme<AuthenticationSchemeOptions, HopFrameAuthentication<TDbContext>>(HopFrameAuthentication<TDbContext>.SchemeName, _ => {}); service.AddAuthentication(HopFrameAuthentication.SchemeName).AddScheme<AuthenticationSchemeOptions, HopFrameAuthentication>(HopFrameAuthentication.SchemeName, _ => {});
service.AddAuthorization(); service.AddAuthorization();
return service; return service;

View File

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

View File

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

View File

@@ -1,15 +1,13 @@
using HopFrame.Database;
using HopFrame.Database.Models; using HopFrame.Database.Models;
using HopFrame.Database.Repositories;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
namespace HopFrame.Security.Claims; namespace HopFrame.Security.Claims;
internal sealed class TokenContextImplementor<TDbContext>(IHttpContextAccessor accessor, TDbContext context) : ITokenContext where TDbContext : HopDbContextBase { internal sealed class TokenContextImplementor(IHttpContextAccessor accessor, IUserRepository users, ITokenRepository tokens) : ITokenContext {
public bool IsAuthenticated => !string.IsNullOrEmpty(accessor.HttpContext?.User.GetAccessTokenId()); public bool IsAuthenticated => !string.IsNullOrEmpty(accessor.HttpContext?.User.GetAccessTokenId());
public User User => context.Users public User User => users.GetUser(Guid.Parse(accessor.HttpContext?.User.GetUserId() ?? Guid.Empty.ToString())).GetAwaiter().GetResult();
.SingleOrDefault(user => user.Id == accessor.HttpContext.User.GetUserId())?
.ToUserModel(context); public Token AccessToken => tokens.GetToken(accessor.HttpContext?.User.GetAccessTokenId()).GetAwaiter().GetResult();
public Guid AccessToken => Guid.Parse(accessor.HttpContext?.User.GetAccessTokenId() ?? Guid.Empty.ToString());
} }

View File

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

View File

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

View File

@@ -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);
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,9 @@
using HopFrame.Web.Admin.Generators;
namespace HopFrame.Web.Admin;
public abstract class AdminPagesContext {
public virtual void OnModelCreating(IAdminContextGenerator generator) {}
}

View 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;
}

View 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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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
};
}

View File

@@ -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;
}

View File

@@ -0,0 +1,4 @@
namespace HopFrame.Web.Admin.Attributes.Members;
[AttributeUsage(AttributeTargets.Property)]
public sealed class AdminHideValueAttribute : Attribute;

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,4 @@
namespace HopFrame.Web.Admin.Attributes.Members;
[AttributeUsage(AttributeTargets.Property)]
public sealed class AdminUneditableAttribute : Attribute;

View File

@@ -0,0 +1,4 @@
namespace HopFrame.Web.Admin.Attributes.Members;
[AttributeUsage(AttributeTargets.Property)]
public class AdminUniqueAttribute : Attribute;

View File

@@ -0,0 +1,4 @@
namespace HopFrame.Web.Admin.Attributes.Members;
[AttributeUsage(AttributeTargets.Property)]
public sealed class AdminUnsortableAttribute : Attribute;

View File

@@ -0,0 +1,4 @@
namespace HopFrame.Web.Admin.Attributes.Members;
[AttributeUsage(AttributeTargets.Property)]
public sealed class ListingPropertyAttribute : Attribute;

View 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>();
}

View 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);
}

View 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);
}

View 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();
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}

View 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>

View 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);
}

View 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;
}
}

View 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; }
}

View 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);
}
}

View 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);
}

View File

@@ -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; }
}

View 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).

View 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);
}
}
}

View File

@@ -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;
}

View File

@@ -1,33 +1,35 @@
using System.Security.Claims; using System.Security.Claims;
using HopFrame.Database; using HopFrame.Database.Repositories;
using HopFrame.Security.Authentication; using HopFrame.Security.Authentication;
using HopFrame.Security.Claims; using HopFrame.Security.Claims;
using HopFrame.Security.Services;
using HopFrame.Web.Services; using HopFrame.Web.Services;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
namespace HopFrame.Web; 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) { public async Task InvokeAsync(HttpContext context, RequestDelegate next) {
var loggedIn = await auth.IsLoggedIn(); var loggedIn = await auth.IsLoggedIn();
if (!loggedIn) { if (!loggedIn) {
var token = await auth.RefreshLogin(); var token = await auth.RefreshLogin();
if (token is null) { if (token is null) {
await next.Invoke(context); next?.Invoke(context);
return; return;
} }
var claims = new List<Claim> { var claims = new List<Claim> {
new(HopFrameClaimTypes.AccessTokenId, token.Token), new(HopFrameClaimTypes.AccessTokenId, token.Content.ToString()),
new(HopFrameClaimTypes.UserId, token.UserId) 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))); 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); await next?.Invoke(context);

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