diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..6287fe9 --- /dev/null +++ b/.gitlab-ci.yml @@ -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 diff --git a/.idea/.idea.HopFrame/.idea/dataSources.xml b/.idea/.idea.HopFrame/.idea/dataSources.xml index 56f170e..ded00e9 100644 --- a/.idea/.idea.HopFrame/.idea/dataSources.xml +++ b/.idea/.idea.HopFrame/.idea/dataSources.xml @@ -5,7 +5,7 @@ sqlite.xerial true org.sqlite.JDBC - jdbc:sqlite:$PROJECT_DIR$/RestApiTest/bin/Debug/net8.0/test.db + jdbc:sqlite:$PROJECT_DIR$/test/RestApiTest/bin/Debug/net8.0/test.db diff --git a/.idea/config/applicationhost.config b/.idea/config/applicationhost.config new file mode 100644 index 0000000..7410bf3 --- /dev/null +++ b/.idea/config/applicationhost.config @@ -0,0 +1,1025 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/HopFrame.sln b/HopFrame.sln index f9217e8..2e65007 100644 --- a/HopFrame.sln +++ b/HopFrame.sln @@ -12,6 +12,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Web", "src\HopFram EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrontendTest", "test\FrontendTest\FrontendTest.csproj", "{8F983A37-63CF-48D5-988D-58B78EF8AECD}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Web.Admin", "src\HopFrame.Web.Admin\HopFrame.Web.Admin.csproj", "{02D9F10A-664A-4EF7-BF19-310C26FF4DEB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -42,6 +44,10 @@ Global {8F983A37-63CF-48D5-988D-58B78EF8AECD}.Debug|Any CPU.Build.0 = Debug|Any CPU {8F983A37-63CF-48D5-988D-58B78EF8AECD}.Release|Any CPU.ActiveCfg = Release|Any CPU {8F983A37-63CF-48D5-988D-58B78EF8AECD}.Release|Any CPU.Build.0 = Release|Any CPU + {02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {02D9F10A-664A-4EF7-BF19-310C26FF4DEB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution EndGlobalSection diff --git a/HopFrame.sln.DotSettings.user b/HopFrame.sln.DotSettings.user index f66ed72..a38eed3 100644 --- a/HopFrame.sln.DotSettings.user +++ b/HopFrame.sln.DotSettings.user @@ -1,4 +1,7 @@ + ForceIncluded + ForceIncluded + ForceIncluded <AssemblyExplorer> <Assembly Path="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\7.0.16\ref\net7.0\System.ComponentModel.Annotations.dll" /> <Assembly Path="C:\Users\Remote\.nuget\packages\blazorstrap\5.2.100.61524\lib\net7.0\BlazorStrap.dll" /> diff --git a/README.md b/README.md index b7ed1b7..4d53003 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,15 @@ A simple backend management api for ASP.NET Core Web APIs - [x] Database management - [x] User authentication - [x] Permission management -- [x] Frontend dashboards +- [x] Generated frontend administration boards # Usage There are two different versions of HopFrame, either the Web API version or the full Blazor web version. ## Ho to use the Web API version +> **Hint:** For more information about the HopFrame installation and usage go to the [docs](./docs). + 1. Add the HopFrame.Api library to your project: ``` diff --git a/docs/Diagrams/Models/ApiModels.puml b/docs/Diagrams/Models/ApiModels.puml deleted file mode 100644 index bb5d25a..0000000 --- a/docs/Diagrams/Models/ApiModels.puml +++ /dev/null @@ -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 { - +Value: TValue - } - - class UserPasswordValidation { - +Password: string - } -} - -@enduml \ No newline at end of file diff --git a/docs/Diagrams/Models/BaseModels.puml b/docs/Diagrams/Models/BaseModels.puml deleted file mode 100644 index 62706fc..0000000 --- a/docs/Diagrams/Models/BaseModels.puml +++ /dev/null @@ -1,37 +0,0 @@ -@startuml BaseModels -set namespaceSeparator none - -namespace HopFrame.Database { - class User { - +Id: Guid - +Username: string - +Email: string - +CreatedAt: DateTime - +Permissions: IList - } - - class Permission { - +Id: long - +PermissionName: string - +Owner: Guid - +GrantedAt: DateTime - } - - class PermissionGroup { - +Name: string - +IsDefaultGroup: bool - +Description: string - +CreatedAt: DateTime - +Permissions: IList - } - - interface IPermissionOwner {} -} - -IPermissionOwner <|-- User -IPermissionOwner <|-- PermissionGroup - -User .. Permission -PermissionGroup .. Permission - -@enduml \ No newline at end of file diff --git a/docs/Diagrams/Models/DatabaseModels.puml b/docs/Diagrams/Models/DatabaseModels.puml deleted file mode 100644 index 2e47b5b..0000000 --- a/docs/Diagrams/Models/DatabaseModels.puml +++ /dev/null @@ -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 \ No newline at end of file diff --git a/docs/Diagrams/Models/img/ApiModels.svg b/docs/Diagrams/Models/img/ApiModels.svg deleted file mode 100644 index 76a19b4..0000000 --- a/docs/Diagrams/Models/img/ApiModels.svg +++ /dev/null @@ -1 +0,0 @@ -HopFrameSecurityApiUserLoginEmail: stringPassword: stringUserRegisterUsername: stringEmail: stringPassword: stringSingleValueResultTValueValue: TValueUserPasswordValidationPassword: string \ No newline at end of file diff --git a/docs/Diagrams/Models/img/BaseModels.svg b/docs/Diagrams/Models/img/BaseModels.svg deleted file mode 100644 index 89ed8d2..0000000 --- a/docs/Diagrams/Models/img/BaseModels.svg +++ /dev/null @@ -1 +0,0 @@ -HopFrame.DatabaseUserId: GuidUsername: stringEmail: stringCreatedAt: DateTimePermissions: IList<Permission>PermissionId: longPermissionName: stringOwner: GuidGrantedAt: DateTimePermissionGroupName: stringIsDefaultGroup: boolDescription: stringCreatedAt: DateTimePermissions: IList<Permission>IPermissionOwner \ No newline at end of file diff --git a/docs/Diagrams/Models/img/DatabaseModels.svg b/docs/Diagrams/Models/img/DatabaseModels.svg deleted file mode 100644 index fedbd56..0000000 --- a/docs/Diagrams/Models/img/DatabaseModels.svg +++ /dev/null @@ -1 +0,0 @@ -HopFrame.DatabaseUserEntryId: stringUsername: stringEmail: stringPassword: stringCreatedAt: DateTimeTokenEntryType: intToken: stringUserId: stringCreatedAt: DateTimePermissionEntryRecordId: longPermissionText: stringUserId: stringGrantedAt: DateTimeGroupEntryName: stringDefault: boolDescription: stringCreatedAt: DateTime \ No newline at end of file diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index eb4fa47..0000000 --- a/docs/README.md +++ /dev/null @@ -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. diff --git a/docs/api/authorization.md b/docs/api/authorization.md new file mode 100644 index 0000000..8fe422f --- /dev/null +++ b/docs/api/authorization.md @@ -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 HelloWorld() { + return "Hello, World!"; +} +``` + +```csharp +// Only logged-in users can access this endpoint +[HttpGet("hello"), Authorized] +public ActionResult 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 HelloWorld() { + return "Hello, World!"; +} +``` diff --git a/docs/api/endpoints.md b/docs/api/endpoints.md new file mode 100644 index 0000000..c02a3bc --- /dev/null +++ b/docs/api/endpoints.md @@ -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) | | diff --git a/docs/api/installation.md b/docs/api/installation.md new file mode 100644 index 0000000..66021eb --- /dev/null +++ b/docs/api/installation.md @@ -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(); + ``` diff --git a/docs/api/logicresults.md b/docs/api/logicresults.md new file mode 100644 index 0000000..ad2fe43 --- /dev/null +++ b/docs/api/logicresults.md @@ -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 Hello() { + return new ActionResult("Hello, World!"); + } + ``` +2. Now instead of directly returning the `ActionResult`, return a `LogicResult` + + ```csharp + [HttpGet("hello")] + public ActionResult Hello() { + return LogicResult.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 Hello() { + if (!Auth.IsLoggedIn) + return LogicResult.Forbidden(); + + return LogicResult.Ok("Hello, World!"); + } + ``` + > **Hint:** You can also provide an error message for status codes that are not in the 200 range. \ No newline at end of file diff --git a/docs/api/models.md b/docs/api/models.md new file mode 100644 index 0000000..c102452 --- /dev/null +++ b/docs/api/models.md @@ -0,0 +1,16 @@ +# HopFrame Models +All models used by the RestAPI are listed below + +## SingleValueResult +```csharp +public struct SingleValueResult(TValue value) { + public TValue Value { get; set; } = value; +} +``` + +## UserPasswordValidation +```csharp +public sealed class UserPasswordValidation { + public string Password { get; set; } +} +``` diff --git a/docs/blazor/admin.md b/docs/blazor/admin.md new file mode 100644 index 0000000..e89a8f4 --- /dev/null +++ b/docs/blazor/admin.md @@ -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 Addresses { get; set; } + + public AdminPage 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 Addresses { get; set; } + public AdminPage Employees { get; set; } + + public override void OnModelCreating(IAdminContextGenerator generator) { + base.OnModelCreating(generator); + + generator.Page() + .Property(e => e.Address) + .IsSelector(); + + generator.Page() + .Property(a => a.Employee) + .Ignore(); + + generator.Page() + .Property(a => a.AddressId) + .IsSelector() + .Parser((model, e) => model.AddressId = e.EmployeeId); + + generator.Page() + .ConfigureRepository() + .ListingProperty(e => e.Name); + + generator.Page() + .ConfigureRepository() + .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; + ``` diff --git a/docs/blazor/auth.md b/docs/blazor/auth.md new file mode 100644 index 0000000..a759486 --- /dev/null +++ b/docs/blazor/auth.md @@ -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 Login(UserLogin login); + Task Logout(); + + Task RefreshLogin(); + Task 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. diff --git a/docs/blazor/authorization.md b/docs/blazor/authorization.md new file mode 100644 index 0000000..952de9c --- /dev/null +++ b/docs/blazor/authorization.md @@ -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 + + + This paragraph is only visible if the user is logged-in and has the required permission + +``` + +```html + + +``` diff --git a/docs/blazor/installation.md b/docs/blazor/installation.md new file mode 100644 index 0000000..f740b7b --- /dev/null +++ b/docs/blazor/installation.md @@ -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(); + ``` + +4. **Optional:** You can also add your [AdminContext](./admin.md) + + ```csharp + builder.Services.AddAdminContext(); + ``` + +5. Add the authentication middleware to your app + + ```csharp + app.UseMiddleware(); + ``` + +6. Add the HopFrame pages to your Razor components + + ```csharp + app.MapRazorComponents() + .AddHopFrameAdminPages() + .AddInteractiveServerRenderMode(); + ``` diff --git a/docs/blazor/pages.md b/docs/blazor/pages.md new file mode 100644 index 0000000..214b771 --- /dev/null +++ b/docs/blazor/pages.md @@ -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. + diff --git a/docs/database.md b/docs/database.md new file mode 100644 index 0000000..c695a8f --- /dev/null +++ b/docs/database.md @@ -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(); + ``` + +3. Create a database migration + + ```bash + dotnet ef migrations add Initial + ``` + +4. Apply the migration to the data source + + ```bash + dotnet ef database update + ``` diff --git a/docs/models.md b/docs/models.md index 6af77a4..39ecc99 100644 --- a/docs/models.md +++ b/docs/models.md @@ -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 Permissions { get; set; } + public virtual List Tokens { get; set; } +} +``` -## Base Models -These are the models used by the various database services. +## PermissionGroup +```csharp +public class PermissionGroup : IPermissionOwner { + public string Name { get; init; } + public bool IsDefaultGroup { get; set; } + public string Description { get; set; } + public DateTime CreatedAt { get; set; } + public virtual List Permissions { get; set; } +} +``` - +## Permission +```csharp +public class Permission { + public long Id { get; init; } + public string PermissionName { get; set; } + public DateTime GrantedAt { get; set; } + public virtual User User { get; set; } + public virtual PermissionGroup Group { get; set; } +} +``` +## Token +```csharp +public class Token { + public int Type { get; set; } + public Guid Content { get; set; } + public DateTime CreatedAt { get; set; } + public virtual User Owner { get; set; } +} +``` -## API Models -These are the models used by the REST API and the Blazor API. +## UserLogin +```csharp +public class UserLogin { + public string Email { get; set; } + public string Password { get; set; } +} +``` - +## UserRegister +```csharp +public class UserRegister { + public string Username { get; set; } + public string Email { get; set; } + public string Password { get; set; } +} +``` - -## Database Models -These are the models that correspond to the scheme in the Database - - +## IPermissionOwner +```csharp +public interface IPermissionOwner; +``` diff --git a/docs/readme.md b/docs/readme.md new file mode 100644 index 0000000..289a64c --- /dev/null +++ b/docs/readme.md @@ -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) diff --git a/docs/repositories.md b/docs/repositories.md new file mode 100644 index 0000000..25cb4ac --- /dev/null +++ b/docs/repositories.md @@ -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> GetUsers(); + + Task GetUser(Guid userId); + + Task GetUserByEmail(string email); + + Task GetUserByUsername(string username); + + Task AddUser(User user); + + Task UpdateUser(User user); + + Task DeleteUser(User user); + + Task CheckUserPassword(User user, string password); + + Task ChangePassword(User user, string password); +} +``` + +### Group Repository + +```csharp +public interface IGroupRepository { + Task> GetPermissionGroups(); + + Task> GetDefaultGroups(); + + Task> GetUserGroups(User user); + + Task GetPermissionGroup(string name); + + Task EditPermissionGroup(PermissionGroup group); + + Task CreatePermissionGroup(PermissionGroup group); + + Task DeletePermissionGroup(PermissionGroup group); +} +``` + +### Permission Repository + +```csharp +public interface IPermissionRepository { + Task HasPermission(IPermissionOwner owner, params string[] permissions); + + Task AddPermission(IPermissionOwner owner, string permission); + + Task RemovePermission(IPermissionOwner owner, string permission); + + Task> GetFullPermissions(IPermissionOwner owner); +} +``` + +### Token Repository + +```csharp +public interface ITokenRepository { + Task GetToken(string content); + + Task CreateToken(int type, User owner); + + Task DeleteUserTokens(User owner); +} +``` diff --git a/docs/services.md b/docs/services.md deleted file mode 100644 index 81a7312..0000000 --- a/docs/services.md +++ /dev/null @@ -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> GetUsers(); - - Task GetUser(Guid userId); - - Task GetUserByEmail(string email); - - Task GetUserByUsername(string username); - - Task AddUser(UserRegister user); - - Task UpdateUser(User user); - - Task DeleteUser(User user); - - Task 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 HasPermission(string permission, Guid user); - - Task> GetPermissionGroups(); - - Task GetPermissionGroup(string name); - - Task EditPermissionGroup(PermissionGroup group); - - Task> GetUserPermissionGroups(User user); - - Task RemoveGroupFromUser(User user, PermissionGroup group); - - Task CreatePermissionGroup(string name, bool isDefault = false, string description = null); - - Task DeletePermissionGroup(PermissionGroup group); - - Task GetPermission(string name, IPermissionOwner owner); - - Task AddPermission(IPermissionOwner owner, string permission); - - Task RemovePermission(Permission permission); - - Task 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(ILogicResult result); - - public static implicit operator ActionResult(LogicResult v); -} - -public class LogicResult : ILogicResult { - public static LogicResult Ok(); - - public static LogicResult Ok(T result); - - ... -} -``` - -### IAuthLogic -This service handles all logic needed to provide the authentication endpoints by using the LogicResults. - -```csharp -public interface IAuthLogic { - Task>> Login(UserLogin login); - - Task>> Register(UserRegister register); - - Task>> Authenticate(); - - Task Logout(); - - Task 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 Login(UserLogin login); - Task Logout(); - - Task RefreshLogin(); - Task IsLoggedIn(); -} -``` diff --git a/docs/usage.md b/docs/usage.md deleted file mode 100644 index 2023531..0000000 --- a/docs/usage.md +++ /dev/null @@ -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(); - builder.Services.AddHopFrame(); - ``` - -## 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(); - builder.Services.AddHopFrame(); - ``` - -4. Add the authentication middleware to your app - - ```csharp - app.UseMiddleware(); - ``` - -5. Add the HopFrame pages to your Razor components - - ```csharp - app.MapRazorComponents() - .AddHopFrameAdminPages() - .AddInteractiveServerRenderMode(); - ``` diff --git a/src/HopFrame.Api/Controller/SecurityController.cs b/src/HopFrame.Api/Controller/SecurityController.cs index fcbed7f..d9c1128 100644 --- a/src/HopFrame.Api/Controller/SecurityController.cs +++ b/src/HopFrame.Api/Controller/SecurityController.cs @@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Mvc; namespace HopFrame.Api.Controller; [ApiController] -[Route("authentication")] +[Route("api/v1/authentication")] public class SecurityController(IAuthLogic auth) : ControllerBase { [HttpPut("login")] diff --git a/src/HopFrame.Api/Extensions/ServiceCollectionExtensions.cs b/src/HopFrame.Api/Extensions/ServiceCollectionExtensions.cs index 29feb38..618a437 100644 --- a/src/HopFrame.Api/Extensions/ServiceCollectionExtensions.cs +++ b/src/HopFrame.Api/Extensions/ServiceCollectionExtensions.cs @@ -27,10 +27,11 @@ public static class ServiceCollectionExtensions { /// The service provider to add the services to /// The data source for all HopFrame entities public static void AddHopFrameNoEndpoints(this IServiceCollection services) where TDbContext : HopDbContextBase { + services.AddHopFrameRepositories(); services.TryAddSingleton(); - services.AddScoped>(); + services.AddScoped(); - services.AddHopFrameAuthentication(); + services.AddHopFrameAuthentication(); } } diff --git a/src/HopFrame.Api/HopFrame.Api.csproj b/src/HopFrame.Api/HopFrame.Api.csproj index 614e37a..744a466 100644 --- a/src/HopFrame.Api/HopFrame.Api.csproj +++ b/src/HopFrame.Api/HopFrame.Api.csproj @@ -7,7 +7,7 @@ disable HopFrame.Api - 1.1.0 + 2.0.0 README.md MIT true diff --git a/src/HopFrame.Api/Logic/IAuthLogic.cs b/src/HopFrame.Api/Logic/IAuthLogic.cs index 7dc5b78..3dd5b5b 100644 --- a/src/HopFrame.Api/Logic/IAuthLogic.cs +++ b/src/HopFrame.Api/Logic/IAuthLogic.cs @@ -8,9 +8,18 @@ public interface IAuthLogic { Task>> Register(UserRegister register); + /// + /// Reassures that the user has a valid refresh token and generates a new access token + /// + /// The newly generated access token Task>> Authenticate(); Task Logout(); + /// + /// Deletes the user account that called the endpoint if the provided password is correct + /// + /// The password od the user + /// Task Delete(UserPasswordValidation validation); } \ No newline at end of file diff --git a/src/HopFrame.Api/Logic/Implementation/AuthLogic.cs b/src/HopFrame.Api/Logic/Implementation/AuthLogic.cs index 405c2cf..e792add 100644 --- a/src/HopFrame.Api/Logic/Implementation/AuthLogic.cs +++ b/src/HopFrame.Api/Logic/Implementation/AuthLogic.cs @@ -1,16 +1,14 @@ using HopFrame.Api.Models; -using HopFrame.Database; -using HopFrame.Database.Models.Entries; +using HopFrame.Database.Models; +using HopFrame.Database.Repositories; using HopFrame.Security.Authentication; using HopFrame.Security.Claims; using HopFrame.Security.Models; -using HopFrame.Security.Services; using Microsoft.AspNetCore.Http; -using Microsoft.EntityFrameworkCore; namespace HopFrame.Api.Logic.Implementation; -public class AuthLogic(TDbContext 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>> Login(UserLogin login) { var user = await users.GetUserByEmail(login.Email); @@ -21,34 +19,21 @@ public class AuthLogic(TDbContext context, IUserService users, IToke if (!await users.CheckUserPassword(user, login.Password)) return LogicResult>.Forbidden("The provided password is not correct"); - var refreshToken = new TokenEntry { - CreatedAt = DateTime.Now, - Token = Guid.NewGuid().ToString(), - Type = TokenEntry.RefreshTokenType, - UserId = user.Id.ToString() - }; - var accessToken = new TokenEntry { - CreatedAt = DateTime.Now, - Token = Guid.NewGuid().ToString(), - Type = TokenEntry.AccessTokenType, - UserId = user.Id.ToString() - }; + var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user); + var accessToken = await tokens.CreateToken(Token.AccessTokenType, user); - accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Token, new CookieOptions { - MaxAge = HopFrameAuthentication.RefreshTokenTime, + accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions { + MaxAge = HopFrameAuthentication.RefreshTokenTime, HttpOnly = true, Secure = true }); - accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Token, new CookieOptions { - MaxAge = HopFrameAuthentication.AccessTokenTime, + accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions { + MaxAge = HopFrameAuthentication.AccessTokenTime, HttpOnly = true, Secure = true }); - await context.Tokens.AddRangeAsync(refreshToken, accessToken); - await context.SaveChangesAsync(); - - return LogicResult>.Ok(accessToken.Token); + return LogicResult>.Ok(accessToken.Content.ToString()); } public async Task>> Register(UserRegister register) { @@ -59,36 +44,27 @@ public class AuthLogic(TDbContext context, IUserService users, IToke if (allUsers.Any(user => user.Username == register.Username || user.Email == register.Email)) return LogicResult>.Conflict("Username or Email is already registered"); - var user = await users.AddUser(register); + var user = await users.AddUser(new User { + Username = register.Username, + Email = register.Email, + Password = register.Password + }); - var refreshToken = new TokenEntry { - CreatedAt = DateTime.Now, - Token = Guid.NewGuid().ToString(), - Type = TokenEntry.RefreshTokenType, - UserId = user.Id.ToString() - }; - var accessToken = new TokenEntry { - CreatedAt = DateTime.Now, - Token = Guid.NewGuid().ToString(), - Type = TokenEntry.AccessTokenType, - UserId = user.Id.ToString() - }; + var refreshToken = await tokens.CreateToken(Token.RefreshTokenType, user); + var accessToken = await tokens.CreateToken(Token.AccessTokenType, user); - await context.Tokens.AddRangeAsync(refreshToken, accessToken); - await context.SaveChangesAsync(); - - accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Token, new CookieOptions { - MaxAge = HopFrameAuthentication.RefreshTokenTime, + accessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Content.ToString(), new CookieOptions { + MaxAge = HopFrameAuthentication.RefreshTokenTime, HttpOnly = true, Secure = true }); - accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Token, new CookieOptions { - MaxAge = HopFrameAuthentication.AccessTokenTime, + accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions { + MaxAge = HopFrameAuthentication.AccessTokenTime, HttpOnly = false, Secure = true }); - return LogicResult>.Ok(accessToken.Token); + return LogicResult>.Ok(accessToken.Content.ToString()); } public async Task>> Authenticate() { @@ -97,31 +73,26 @@ public class AuthLogic(TDbContext context, IUserService users, IToke if (string.IsNullOrEmpty(refreshToken)) return LogicResult>.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>.BadRequest("The provided token is not a refresh token"); + if (token is null) return LogicResult>.NotFound("Refresh token not valid"); - if (token.CreatedAt + HopFrameAuthentication.RefreshTokenTime < DateTime.Now) + if (token.CreatedAt + HopFrameAuthentication.RefreshTokenTime < DateTime.Now) return LogicResult>.Conflict("Refresh token is expired"); - var accessToken = new TokenEntry { - CreatedAt = DateTime.Now, - Token = Guid.NewGuid().ToString(), - Type = TokenEntry.AccessTokenType, - UserId = token.UserId - }; - - await context.Tokens.AddAsync(accessToken); - await context.SaveChangesAsync(); + var accessToken = await tokens.CreateToken(Token.AccessTokenType, token.Owner); - accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Token, new CookieOptions { - MaxAge = HopFrameAuthentication.AccessTokenTime, + accessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Content.ToString(), new CookieOptions { + MaxAge = HopFrameAuthentication.AccessTokenTime, HttpOnly = false, Secure = true }); - return LogicResult>.Ok(accessToken.Token); + return LogicResult>.Ok(accessToken.Content.ToString()); } public async Task Logout() { @@ -131,17 +102,7 @@ public class AuthLogic(TDbContext context, IUserService users, IToke if (string.IsNullOrEmpty(accessToken) || string.IsNullOrEmpty(refreshToken)) return LogicResult.Conflict("access or refresh token not provided"); - var tokenEntries = await context.Tokens.Where(token => - (token.Token == accessToken && token.Type == TokenEntry.AccessTokenType) || - (token.Token == refreshToken && token.Type == TokenEntry.RefreshTokenType)) - .ToArrayAsync(); - - if (tokenEntries.Length != 2) - return LogicResult.NotFound("One or more of the provided tokens was not found"); - - context.Tokens.Remove(tokenEntries[0]); - context.Tokens.Remove(tokenEntries[1]); - await context.SaveChangesAsync(); + await tokens.DeleteUserTokens(tokenContext.User); accessor.HttpContext?.Response.Cookies.Delete(ITokenContext.RefreshTokenType); accessor.HttpContext?.Response.Cookies.Delete(ITokenContext.AccessTokenType); diff --git a/src/HopFrame.Api/Models/SingleValueResult.cs b/src/HopFrame.Api/Models/SingleValueResult.cs index c09fdb6..81cbdb4 100644 --- a/src/HopFrame.Api/Models/SingleValueResult.cs +++ b/src/HopFrame.Api/Models/SingleValueResult.cs @@ -1,5 +1,10 @@ namespace HopFrame.Api.Models; +/// +/// Useful for endpoints that only return a single int or string +/// +/// The value of the result +/// The type of the result public struct SingleValueResult(TValue value) { public TValue Value { get; set; } = value; diff --git a/src/HopFrame.Api/README.md b/src/HopFrame.Api/README.md index 67b946b..94966e4 100644 --- a/src/HopFrame.Api/README.md +++ b/src/HopFrame.Api/README.md @@ -1,100 +1,4 @@ # HopFrame API module This module contains some useful endpoints for user login / register management. -## Ho to use the Web API version - -1. Add the HopFrame.Api library to your project: - - ``` - dotnet add package HopFrame.Api - ``` - -2. Create a DbContext that inherits the ``HopDbContext`` and add a data source - - ```csharp - public class DatabaseContext : HopDbContextBase { - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { - base.OnConfiguring(optionsBuilder); - - optionsBuilder.UseSqlite("..."); - } - } - ``` - -3. Add the DbContext and HopFrame to your services - - ```csharp - builder.Services.AddDbContext(); - builder.Services.AddHopFrame(); - ``` - -# 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(); -//builder.Services.AddHopFrame(); -services.AddHopFrameNoEndpoints(); -``` - -# 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(ILogicResult result); - - public static implicit operator ActionResult(LogicResult v); -} - -public class LogicResult : ILogicResult { - public static LogicResult Ok(); - - public static LogicResult Ok(T result); - - ... -} -``` - -## IAuthLogic -This service handles all logic needed to provide the authentication endpoints by using the LogicResults. - -```csharp -public interface IAuthLogic { - Task>> Login(UserLogin login); - - Task>> Register(UserRegister register); - - Task>> Authenticate(); - - Task Logout(); - - Task Delete(UserPasswordValidation validation); -} -``` \ No newline at end of file +For more information about the HopFrame visit the [docs](https://git.leon-hoppe.de/leon.hoppe/HopFrame). \ No newline at end of file diff --git a/src/HopFrame.Security/EncryptionManager.cs b/src/HopFrame.Database/EncryptionManager.cs similarity index 96% rename from src/HopFrame.Security/EncryptionManager.cs rename to src/HopFrame.Database/EncryptionManager.cs index 8f5037b..32bb2d5 100644 --- a/src/HopFrame.Security/EncryptionManager.cs +++ b/src/HopFrame.Database/EncryptionManager.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Cryptography.KeyDerivation; -namespace HopFrame.Security; +namespace HopFrame.Database; public static class EncryptionManager { diff --git a/src/HopFrame.Database/HopDbContextBase.cs b/src/HopFrame.Database/HopDbContextBase.cs index 2ae1fe6..21342ea 100644 --- a/src/HopFrame.Database/HopDbContextBase.cs +++ b/src/HopFrame.Database/HopDbContextBase.cs @@ -1,4 +1,4 @@ -using HopFrame.Database.Models.Entries; +using HopFrame.Database.Models; using Microsoft.EntityFrameworkCore; namespace HopFrame.Database; @@ -8,25 +8,27 @@ namespace HopFrame.Database; /// public abstract class HopDbContextBase : DbContext { - public virtual DbSet Users { get; set; } - public virtual DbSet Permissions { get; set; } - public virtual DbSet Tokens { get; set; } - public virtual DbSet Groups { get; set; } + public virtual DbSet Users { get; set; } + public virtual DbSet Permissions { get; set; } + public virtual DbSet Tokens { get; set; } + public virtual DbSet Groups { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); - modelBuilder.Entity(); - modelBuilder.Entity(); - modelBuilder.Entity(); - modelBuilder.Entity(); - } + modelBuilder.Entity() + .HasMany(u => u.Tokens) + .WithOne(t => t.Owner) + .OnDelete(DeleteBehavior.Cascade); - /// - /// Gets executed when a user is deleted through the IUserService from the - /// HopFrame.Security package. You can override this method to also delete - /// related user specific entries in the database - /// - /// - public virtual void OnUserDelete(UserEntry user) {} + modelBuilder.Entity() + .HasMany(u => u.Permissions) + .WithOne(p => p.User) + .OnDelete(DeleteBehavior.Cascade); + + modelBuilder.Entity() + .HasMany(g => g.Permissions) + .WithOne(p => p.Group) + .OnDelete(DeleteBehavior.Cascade); + } } \ No newline at end of file diff --git a/src/HopFrame.Database/HopFrame.Database.csproj b/src/HopFrame.Database/HopFrame.Database.csproj index d88b757..de55cd5 100644 --- a/src/HopFrame.Database/HopFrame.Database.csproj +++ b/src/HopFrame.Database/HopFrame.Database.csproj @@ -7,13 +7,14 @@ disable HopFrame.Database - 1.1.0 + 2.0.0 README.md MIT true + diff --git a/src/HopFrame.Database/Models/Entries/GroupEntry.cs b/src/HopFrame.Database/Models/Entries/GroupEntry.cs deleted file mode 100644 index 830d466..0000000 --- a/src/HopFrame.Database/Models/Entries/GroupEntry.cs +++ /dev/null @@ -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; } -} \ No newline at end of file diff --git a/src/HopFrame.Database/Models/Entries/PermissionEntry.cs b/src/HopFrame.Database/Models/Entries/PermissionEntry.cs deleted file mode 100644 index 2f8bdae..0000000 --- a/src/HopFrame.Database/Models/Entries/PermissionEntry.cs +++ /dev/null @@ -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; } -} \ No newline at end of file diff --git a/src/HopFrame.Database/Models/Entries/UserEntry.cs b/src/HopFrame.Database/Models/Entries/UserEntry.cs deleted file mode 100644 index 2bc1a12..0000000 --- a/src/HopFrame.Database/Models/Entries/UserEntry.cs +++ /dev/null @@ -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; } -} \ No newline at end of file diff --git a/src/HopFrame.Database/Models/ModelExtensions.cs b/src/HopFrame.Database/Models/ModelExtensions.cs deleted file mode 100644 index 4600afd..0000000 --- a/src/HopFrame.Database/Models/ModelExtensions.cs +++ /dev/null @@ -1,56 +0,0 @@ -using HopFrame.Database.Models.Entries; - -namespace HopFrame.Database.Models; - -public static class ModelExtensions { - - /// - /// Converts the database model to a friendly user model - /// - /// the database model - /// the data source for the permissions and users - /// - 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; - } - -} \ No newline at end of file diff --git a/src/HopFrame.Database/Models/Permission.cs b/src/HopFrame.Database/Models/Permission.cs index e6fbe14..db111ba 100644 --- a/src/HopFrame.Database/Models/Permission.cs +++ b/src/HopFrame.Database/Models/Permission.cs @@ -1,10 +1,26 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; + namespace HopFrame.Database.Models; -public sealed class Permission { +public class Permission { + + [Key, Required, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public long Id { get; init; } + + [Required, MaxLength(255)] public string PermissionName { get; set; } - public Guid Owner { get; set; } + + [Required] public DateTime GrantedAt { get; set; } + + [ForeignKey("UserId"), JsonIgnore] + public virtual User User { get; set; } + + [ForeignKey("GroupName"), JsonIgnore] + public virtual PermissionGroup Group { get; set; } + } -public interface IPermissionOwner {} +public interface IPermissionOwner; diff --git a/src/HopFrame.Database/Models/PermissionGroup.cs b/src/HopFrame.Database/Models/PermissionGroup.cs index 3472e39..aa0c92c 100644 --- a/src/HopFrame.Database/Models/PermissionGroup.cs +++ b/src/HopFrame.Database/Models/PermissionGroup.cs @@ -1,9 +1,22 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + namespace HopFrame.Database.Models; public class PermissionGroup : IPermissionOwner { + + [Key, Required, MaxLength(50)] public string Name { get; init; } + + [Required, DefaultValue(false)] public bool IsDefaultGroup { get; set; } + + [MaxLength(500)] public string Description { get; set; } + + [Required] public DateTime CreatedAt { get; set; } - public IList Permissions { get; set; } + + public virtual List Permissions { get; set; } + } \ No newline at end of file diff --git a/src/HopFrame.Database/Models/Entries/TokenEntry.cs b/src/HopFrame.Database/Models/Token.cs similarity index 62% rename from src/HopFrame.Database/Models/Entries/TokenEntry.cs rename to src/HopFrame.Database/Models/Token.cs index d33b307..a42d367 100644 --- a/src/HopFrame.Database/Models/Entries/TokenEntry.cs +++ b/src/HopFrame.Database/Models/Token.cs @@ -1,8 +1,10 @@ using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; -namespace HopFrame.Database.Models.Entries; +namespace HopFrame.Database.Models; -public class TokenEntry { +public class Token { public const int RefreshTokenType = 0; public const int AccessTokenType = 1; @@ -15,11 +17,11 @@ public class TokenEntry { public int Type { get; set; } [Key, Required, MinLength(36), MaxLength(36)] - public string Token { get; set; } - - [Required, MinLength(36), MaxLength(36)] - public string UserId { get; set; } + public Guid Content { get; set; } [Required] public DateTime CreatedAt { get; set; } + + [ForeignKey("UserId"), JsonIgnore] + public virtual User Owner { get; set; } } \ No newline at end of file diff --git a/src/HopFrame.Database/Models/User.cs b/src/HopFrame.Database/Models/User.cs index e97d720..feec39c 100644 --- a/src/HopFrame.Database/Models/User.cs +++ b/src/HopFrame.Database/Models/User.cs @@ -1,9 +1,28 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + namespace HopFrame.Database.Models; -public sealed class User : IPermissionOwner { +public class User : IPermissionOwner { + + [Key, Required, MinLength(36), MaxLength(36)] public Guid Id { get; init; } + + [Required, MaxLength(50)] public string Username { get; set; } + + [Required, MaxLength(50), EmailAddress] public string Email { get; set; } + + [Required, MinLength(8), MaxLength(255), JsonIgnore] + public string Password { get; set; } + + [Required] public DateTime CreatedAt { get; set; } - public IList Permissions { get; set; } + + public virtual List Permissions { get; set; } + + [JsonIgnore] + public virtual List Tokens { get; set; } + } \ No newline at end of file diff --git a/src/HopFrame.Security/Authorization/PermissionValidator.cs b/src/HopFrame.Database/PermissionValidator.cs similarity index 94% rename from src/HopFrame.Security/Authorization/PermissionValidator.cs rename to src/HopFrame.Database/PermissionValidator.cs index d3a844b..de19dfc 100644 --- a/src/HopFrame.Security/Authorization/PermissionValidator.cs +++ b/src/HopFrame.Database/PermissionValidator.cs @@ -1,4 +1,4 @@ -namespace HopFrame.Security.Authorization; +namespace HopFrame.Database; public static class PermissionValidator { diff --git a/src/HopFrame.Database/README.md b/src/HopFrame.Database/README.md index 0aacfa2..25d0bda 100644 --- a/src/HopFrame.Database/README.md +++ b/src/HopFrame.Database/README.md @@ -1,2 +1,4 @@ # HopFrame Database module -This module contains all the logic for the database communication +This module contains all the logic for the database communication. + +For more information about the HopFrame visit the [docs](https://git.leon-hoppe.de/leon.hoppe/HopFrame). diff --git a/src/HopFrame.Database/Repositories/IGroupRepository.cs b/src/HopFrame.Database/Repositories/IGroupRepository.cs new file mode 100644 index 0000000..259d7c5 --- /dev/null +++ b/src/HopFrame.Database/Repositories/IGroupRepository.cs @@ -0,0 +1,21 @@ +using HopFrame.Database.Models; + +namespace HopFrame.Database.Repositories; + +public interface IGroupRepository { + Task> GetPermissionGroups(); + + Task> GetDefaultGroups(); + + Task> GetUserGroups(User user); + + Task GetPermissionGroup(string name); + + Task EditPermissionGroup(PermissionGroup group); + + Task CreatePermissionGroup(PermissionGroup group); + + Task DeletePermissionGroup(PermissionGroup group); + + internal Task> GetFullGroupPermissions(string group); +} \ No newline at end of file diff --git a/src/HopFrame.Database/Repositories/IPermissionRepository.cs b/src/HopFrame.Database/Repositories/IPermissionRepository.cs new file mode 100644 index 0000000..07680ce --- /dev/null +++ b/src/HopFrame.Database/Repositories/IPermissionRepository.cs @@ -0,0 +1,23 @@ +using HopFrame.Database.Models; + +namespace HopFrame.Database.Repositories; + +public interface IPermissionRepository { + Task HasPermission(IPermissionOwner owner, params string[] permissions); + + /// + /// permission system: + /// - "*" -> all rights + /// - "group.[name]" -> group member + /// - "[namespace].[name]" -> single permission + /// - "[namespace].*" -> all permissions in the namespace + /// + /// + /// + /// + Task AddPermission(IPermissionOwner owner, string permission); + + Task RemovePermission(IPermissionOwner owner, string permission); + + Task> GetFullPermissions(IPermissionOwner owner); +} \ No newline at end of file diff --git a/src/HopFrame.Database/Repositories/ITokenRepository.cs b/src/HopFrame.Database/Repositories/ITokenRepository.cs new file mode 100644 index 0000000..bec3963 --- /dev/null +++ b/src/HopFrame.Database/Repositories/ITokenRepository.cs @@ -0,0 +1,9 @@ +using HopFrame.Database.Models; + +namespace HopFrame.Database.Repositories; + +public interface ITokenRepository { + Task GetToken(string content); + Task CreateToken(int type, User owner); + Task DeleteUserTokens(User owner); +} \ No newline at end of file diff --git a/src/HopFrame.Security/Services/IUserService.cs b/src/HopFrame.Database/Repositories/IUserRepository.cs similarity index 53% rename from src/HopFrame.Security/Services/IUserService.cs rename to src/HopFrame.Database/Repositories/IUserRepository.cs index 5109dea..e847d61 100644 --- a/src/HopFrame.Security/Services/IUserService.cs +++ b/src/HopFrame.Database/Repositories/IUserRepository.cs @@ -1,9 +1,8 @@ using HopFrame.Database.Models; -using HopFrame.Security.Models; -namespace HopFrame.Security.Services; +namespace HopFrame.Database.Repositories; -public interface IUserService { +public interface IUserRepository { Task> GetUsers(); Task GetUser(Guid userId); @@ -12,13 +11,8 @@ public interface IUserService { Task GetUserByUsername(string username); - Task AddUser(UserRegister user); - - /// - /// IMPORTANT: - /// This function does not add or remove any permissions to the user. - /// For that please use - /// + Task AddUser(User user); + Task UpdateUser(User user); Task DeleteUser(User user); diff --git a/src/HopFrame.Database/Repositories/Implementation/GroupRepository.cs b/src/HopFrame.Database/Repositories/Implementation/GroupRepository.cs new file mode 100644 index 0000000..547e193 --- /dev/null +++ b/src/HopFrame.Database/Repositories/Implementation/GroupRepository.cs @@ -0,0 +1,79 @@ +using HopFrame.Database.Models; +using Microsoft.EntityFrameworkCore; + +namespace HopFrame.Database.Repositories.Implementation; + +internal sealed class GroupRepository(TDbContext context) : IGroupRepository where TDbContext : HopDbContextBase { + public async Task> GetPermissionGroups() { + return await context.Groups + .Include(g => g.Permissions) + .ToListAsync(); + } + + public async Task> GetDefaultGroups() { + return await context.Groups + .Include(g => g.Permissions) + .Where(g => g.IsDefaultGroup) + .ToListAsync(); + } + + public Task> GetUserGroups(User user) { + return Task.FromResult((IList) context.Groups + .Include(g => g.Permissions) + .AsEnumerable() + .Where(g => user.Permissions.Any(p => p.PermissionName == g.Name)) + .ToList()); + } + + public async Task 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 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> 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; + } +} \ No newline at end of file diff --git a/src/HopFrame.Database/Repositories/Implementation/PermissionRepository.cs b/src/HopFrame.Database/Repositories/Implementation/PermissionRepository.cs new file mode 100644 index 0000000..45bcfd8 --- /dev/null +++ b/src/HopFrame.Database/Repositories/Implementation/PermissionRepository.cs @@ -0,0 +1,89 @@ +using HopFrame.Database.Models; +using Microsoft.EntityFrameworkCore; + +namespace HopFrame.Database.Repositories.Implementation; + +internal sealed class PermissionRepository(TDbContext context, IGroupRepository groupRepository) : IPermissionRepository where TDbContext : HopDbContextBase { + public async Task 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 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> GetFullPermissions(IPermissionOwner owner) { + var permissions = new List(); + + 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; + } +} \ No newline at end of file diff --git a/src/HopFrame.Database/Repositories/Implementation/TokenRepository.cs b/src/HopFrame.Database/Repositories/Implementation/TokenRepository.cs new file mode 100644 index 0000000..70f727a --- /dev/null +++ b/src/HopFrame.Database/Repositories/Implementation/TokenRepository.cs @@ -0,0 +1,41 @@ +using HopFrame.Database.Models; +using Microsoft.EntityFrameworkCore; + +namespace HopFrame.Database.Repositories.Implementation; + +internal sealed class TokenRepository(TDbContext context) : ITokenRepository where TDbContext : HopDbContextBase { + + public async Task 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 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(); + } +} \ No newline at end of file diff --git a/src/HopFrame.Database/Repositories/Implementation/UserRepository.cs b/src/HopFrame.Database/Repositories/Implementation/UserRepository.cs new file mode 100644 index 0000000..c642466 --- /dev/null +++ b/src/HopFrame.Database/Repositories/Implementation/UserRepository.cs @@ -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 context, IGroupRepository groupRepository) : IUserRepository where TDbContext : HopDbContextBase { + + private IIncludableQueryable> IncludeReferences() { + return context.Users + .Include(u => u.Permissions) + .Include(u => u.Tokens); + } + + public async Task> GetUsers() { + return await IncludeReferences() + .ToListAsync(); + } + + public async Task GetUser(Guid userId) { + return await IncludeReferences() + .Where(u => u.Id == userId) + .SingleOrDefaultAsync(); + } + + public async Task GetUserByEmail(string email) { + return await IncludeReferences() + .Where(u => u.Email == email) + .SingleOrDefaultAsync(); + } + + public async Task GetUserByUsername(string username) { + return await IncludeReferences() + .Where(u => u.Username == username) + .SingleOrDefaultAsync(); + } + + public async Task 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(), + 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 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(); + } + +} \ No newline at end of file diff --git a/src/HopFrame.Database/ServiceCollectionExtensions.cs b/src/HopFrame.Database/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..ab07d5a --- /dev/null +++ b/src/HopFrame.Database/ServiceCollectionExtensions.cs @@ -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(this IServiceCollection services) where TDbContext : HopDbContextBase { + services.AddScoped>(); + services.AddScoped>(); + services.AddScoped>(); + services.AddScoped>(); + + return services; + } + +} \ No newline at end of file diff --git a/src/HopFrame.Security/Authentication/HopFrameAuthentication.cs b/src/HopFrame.Security/Authentication/HopFrameAuthentication.cs index 8232d4c..9c65b14 100644 --- a/src/HopFrame.Security/Authentication/HopFrameAuthentication.cs +++ b/src/HopFrame.Security/Authentication/HopFrameAuthentication.cs @@ -1,10 +1,8 @@ using System.Security.Claims; using System.Text.Encodings.Web; -using HopFrame.Database; +using HopFrame.Database.Repositories; using HopFrame.Security.Claims; -using HopFrame.Security.Services; using Microsoft.AspNetCore.Authentication; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -13,15 +11,15 @@ using Microsoft.Extensions.Options; namespace HopFrame.Security.Authentication; -public class HopFrameAuthentication( +public class HopFrameAuthentication( IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, - TDbContext context, - IPermissionService perms) - : AuthenticationHandler(options, logger, encoder, clock) - where TDbContext : HopDbContextBase { + ITokenRepository tokens, + IUserRepository users, + IPermissionRepository perms) + : AuthenticationHandler(options, logger, encoder, clock) { public const string SchemeName = "HopCore.Authentication"; public static readonly TimeSpan AccessTokenTime = new(0, 0, 5, 0); @@ -32,21 +30,21 @@ public class HopFrameAuthentication( if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Headers[SchemeName]; if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Headers["Token"]; if (string.IsNullOrEmpty(accessToken)) return AuthenticateResult.Fail("No Access Token provided"); - - var tokenEntry = await context.Tokens.SingleOrDefaultAsync(token => token.Token == accessToken); + + var tokenEntry = await tokens.GetToken(accessToken); if (tokenEntry is null) return AuthenticateResult.Fail("The provided Access Token does not exist"); if (tokenEntry.CreatedAt + AccessTokenTime < DateTime.Now) return AuthenticateResult.Fail("The provided Access Token is expired"); - if (!await context.Users.AnyAsync(user => user.Id == tokenEntry.UserId)) + if (tokenEntry.Owner is null) return AuthenticateResult.Fail("The provided Access Token does not match any user"); var claims = new List { new(HopFrameClaimTypes.AccessTokenId, accessToken), - new(HopFrameClaimTypes.UserId, tokenEntry.UserId) + new(HopFrameClaimTypes.UserId, tokenEntry.Owner.Id.ToString()) }; - var permissions = await perms.GetFullPermissions(tokenEntry.UserId); + var permissions = await perms.GetFullPermissions(tokenEntry.Owner); claims.AddRange(permissions.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm))); var principal = new ClaimsPrincipal(); diff --git a/src/HopFrame.Security/Authentication/HopFrameAuthenticationExtensions.cs b/src/HopFrame.Security/Authentication/HopFrameAuthenticationExtensions.cs index f604c34..cf87810 100644 --- a/src/HopFrame.Security/Authentication/HopFrameAuthenticationExtensions.cs +++ b/src/HopFrame.Security/Authentication/HopFrameAuthenticationExtensions.cs @@ -1,7 +1,4 @@ -using HopFrame.Database; using HopFrame.Security.Claims; -using HopFrame.Security.Services; -using HopFrame.Security.Services.Implementation; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; @@ -17,13 +14,11 @@ public static class HopFrameAuthenticationExtensions { /// The service provider to add the services to /// The database object that saves all entities that are important for the security api /// - public static IServiceCollection AddHopFrameAuthentication(this IServiceCollection service) where TDbContext : HopDbContextBase { + public static IServiceCollection AddHopFrameAuthentication(this IServiceCollection service) { service.TryAddSingleton(); - service.AddScoped>(); - service.AddScoped>(); - service.AddScoped>(); + service.AddScoped(); - service.AddAuthentication(HopFrameAuthentication.SchemeName).AddScheme>(HopFrameAuthentication.SchemeName, _ => {}); + service.AddAuthentication(HopFrameAuthentication.SchemeName).AddScheme(HopFrameAuthentication.SchemeName, _ => {}); service.AddAuthorization(); return service; diff --git a/src/HopFrame.Security/Authorization/AuthorizedFilter.cs b/src/HopFrame.Security/Authorization/AuthorizedFilter.cs index 13f5932..f78cdc0 100644 --- a/src/HopFrame.Security/Authorization/AuthorizedFilter.cs +++ b/src/HopFrame.Security/Authorization/AuthorizedFilter.cs @@ -1,3 +1,4 @@ +using HopFrame.Database; using HopFrame.Security.Claims; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Authorization; diff --git a/src/HopFrame.Security/Claims/ITokenContext.cs b/src/HopFrame.Security/Claims/ITokenContext.cs index 3b4f5e9..6b5a590 100644 --- a/src/HopFrame.Security/Claims/ITokenContext.cs +++ b/src/HopFrame.Security/Claims/ITokenContext.cs @@ -20,5 +20,5 @@ public interface ITokenContext { /// /// The access token the user provided /// - Guid AccessToken { get; } + Token AccessToken { get; } } \ No newline at end of file diff --git a/src/HopFrame.Security/Claims/TokenContextImplementor.cs b/src/HopFrame.Security/Claims/TokenContextImplementor.cs index dbdae9e..dd50a08 100644 --- a/src/HopFrame.Security/Claims/TokenContextImplementor.cs +++ b/src/HopFrame.Security/Claims/TokenContextImplementor.cs @@ -1,15 +1,13 @@ -using HopFrame.Database; using HopFrame.Database.Models; +using HopFrame.Database.Repositories; using Microsoft.AspNetCore.Http; namespace HopFrame.Security.Claims; -internal sealed class TokenContextImplementor(IHttpContextAccessor accessor, TDbContext context) : ITokenContext where TDbContext : HopDbContextBase { +internal sealed class TokenContextImplementor(IHttpContextAccessor accessor, IUserRepository users, ITokenRepository tokens) : ITokenContext { public bool IsAuthenticated => !string.IsNullOrEmpty(accessor.HttpContext?.User.GetAccessTokenId()); - public User User => context.Users - .SingleOrDefault(user => user.Id == accessor.HttpContext.User.GetUserId())? - .ToUserModel(context); - - public Guid AccessToken => Guid.Parse(accessor.HttpContext?.User.GetAccessTokenId() ?? Guid.Empty.ToString()); + public User User => users.GetUser(Guid.Parse(accessor.HttpContext?.User.GetUserId() ?? Guid.Empty.ToString())).GetAwaiter().GetResult(); + + public Token AccessToken => tokens.GetToken(accessor.HttpContext?.User.GetAccessTokenId()).GetAwaiter().GetResult(); } \ No newline at end of file diff --git a/src/HopFrame.Security/HopFrame.Security.csproj b/src/HopFrame.Security/HopFrame.Security.csproj index b8a814b..74c1d50 100644 --- a/src/HopFrame.Security/HopFrame.Security.csproj +++ b/src/HopFrame.Security/HopFrame.Security.csproj @@ -8,7 +8,7 @@ HopFrame.Security HopFrame.Security - 1.1.0 + 2.0.0 README.md MIT true diff --git a/src/HopFrame.Security/README.md b/src/HopFrame.Security/README.md index cc4b5d0..9dfdbb4 100644 --- a/src/HopFrame.Security/README.md +++ b/src/HopFrame.Security/README.md @@ -1,74 +1,4 @@ # HopFrame Security module this module contains all handlers for the login and register validation. It also checks the user permissions. -# Services added in this module -You can use these services by specifying them as a dependency. All of them are scoped dependencies. - -## ITokenContext -This service provides the information given by the current request - -```csharp -public interface ITokenContext { - bool IsAuthenticated { get; } - - User User { get; } - - Guid AccessToken { get; } -} -``` - -## IUserService -This service simplifies the data access of the user table in the database. - -```csharp -public interface IUserService { - Task> GetUsers(); - - Task GetUser(Guid userId); - - Task GetUserByEmail(string email); - - Task GetUserByUsername(string username); - - Task AddUser(UserRegister user); - - Task UpdateUser(User user); - - Task DeleteUser(User user); - - Task 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 HasPermission(string permission, Guid user); - - Task> GetPermissionGroups(); - - Task GetPermissionGroup(string name); - - Task EditPermissionGroup(PermissionGroup group); - - Task> GetUserPermissionGroups(User user); - - Task RemoveGroupFromUser(User user, PermissionGroup group); - - Task CreatePermissionGroup(string name, bool isDefault = false, string description = null); - - Task DeletePermissionGroup(PermissionGroup group); - - Task GetPermission(string name, IPermissionOwner owner); - - Task AddPermission(IPermissionOwner owner, string permission); - - Task RemovePermission(Permission permission); - - Task GetFullPermissions(string user); -} -``` +For more information about the HopFrame visit the [docs](https://git.leon-hoppe.de/leon.hoppe/HopFrame). diff --git a/src/HopFrame.Security/Services/IPermissionService.cs b/src/HopFrame.Security/Services/IPermissionService.cs deleted file mode 100644 index 38a9000..0000000 --- a/src/HopFrame.Security/Services/IPermissionService.cs +++ /dev/null @@ -1,48 +0,0 @@ -using HopFrame.Database.Models; - -namespace HopFrame.Security.Services; - -/// -/// permission system: -/// - "*" -> all rights -/// - "group.[name]" -> group member -/// - "[namespace].[name]" -> single permission -/// - "[namespace].*" -> all permissions in the namespace -/// -public interface IPermissionService { - - Task HasPermission(string permission, Guid user); - - Task> GetPermissionGroups(); - - Task GetPermissionGroup(string name); - - Task EditPermissionGroup(PermissionGroup group); - - Task> GetUserPermissionGroups(User user); - - Task RemoveGroupFromUser(User user, PermissionGroup group); - - Task CreatePermissionGroup(string name, bool isDefault = false, string description = null); - - Task DeletePermissionGroup(PermissionGroup group); - - Task GetPermission(string name, IPermissionOwner owner); - - /// - /// permission system: - /// - "*" -> all rights - /// - "group.[name]" -> group member - /// - "[namespace].[name]" -> single permission - /// - "[namespace].*" -> all permissions in the namespace - /// - /// - /// - /// - Task AddPermission(IPermissionOwner owner, string permission); - - Task RemovePermission(Permission permission); - - Task GetFullPermissions(string user); - -} \ No newline at end of file diff --git a/src/HopFrame.Security/Services/Implementation/PermissionService.cs b/src/HopFrame.Security/Services/Implementation/PermissionService.cs deleted file mode 100644 index ac0e156..0000000 --- a/src/HopFrame.Security/Services/Implementation/PermissionService.cs +++ /dev/null @@ -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 context, ITokenContext current) : IPermissionService where TDbContext : HopDbContextBase { - public async Task HasPermission(string permission) { - return await HasPermission(permission, current.User.Id); - } - - public async Task 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 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 HasPermission(string permission, Guid user) { - var permissions = await GetFullPermissions(user.ToString()); - - return PermissionValidator.IncludesPermission(permission, permissions); - } - - public async Task> GetPermissionGroups() { - return await context.Groups - .Select(group => group.ToPermissionGroup(context)) - .ToListAsync(); - } - - public Task 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
This paragraph is only visible if the user is logged-in and has the required permission