diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..add57be
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+bin/
+obj/
+/packages/
+riderModule.iml
+/_ReSharper.Caches/
\ No newline at end of file
diff --git a/.idea/.idea.HopFrame/.idea/.gitignore b/.idea/.idea.HopFrame/.idea/.gitignore
new file mode 100644
index 0000000..4806007
--- /dev/null
+++ b/.idea/.idea.HopFrame/.idea/.gitignore
@@ -0,0 +1,13 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Rider ignored files
+/contentModel.xml
+/.idea.HopFrame.iml
+/projectSettingsUpdater.xml
+/modules.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/.idea.HopFrame/.idea/indexLayout.xml b/.idea/.idea.HopFrame/.idea/indexLayout.xml
new file mode 100644
index 0000000..7b08163
--- /dev/null
+++ b/.idea/.idea.HopFrame/.idea/indexLayout.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.HopFrame/.idea/vcs.xml b/.idea/.idea.HopFrame/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/.idea.HopFrame/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/DatabaseTest/.gitignore b/DatabaseTest/.gitignore
new file mode 100644
index 0000000..a4bbe35
--- /dev/null
+++ b/DatabaseTest/.gitignore
@@ -0,0 +1,3 @@
+obj
+bin
+appsettings.Development.json
\ No newline at end of file
diff --git a/DatabaseTest/Controllers/TestController.cs b/DatabaseTest/Controllers/TestController.cs
new file mode 100644
index 0000000..0104631
--- /dev/null
+++ b/DatabaseTest/Controllers/TestController.cs
@@ -0,0 +1,9 @@
+using HopFrame.Api.Controller;
+using Microsoft.AspNetCore.Mvc;
+
+namespace DatabaseTest.Controllers;
+
+[ApiController]
+public class TestController(DatabaseContext context) : SecurityController(context) {
+
+}
\ No newline at end of file
diff --git a/DatabaseTest/DatabaseContext.cs b/DatabaseTest/DatabaseContext.cs
new file mode 100644
index 0000000..ac33081
--- /dev/null
+++ b/DatabaseTest/DatabaseContext.cs
@@ -0,0 +1,12 @@
+using HopFrame.Database;
+using Microsoft.EntityFrameworkCore;
+
+namespace DatabaseTest;
+
+public class DatabaseContext : HopDbContextBase {
+ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
+ base.OnConfiguring(optionsBuilder);
+
+ optionsBuilder.UseSqlite("Data Source=C:\\Users\\Remote\\Documents\\Projekte\\HopFrame\\DatabaseTest\\bin\\Debug\\net7.0\\test.db;Mode=ReadWrite;");
+ }
+}
\ No newline at end of file
diff --git a/DatabaseTest/DatabaseTest.csproj b/DatabaseTest/DatabaseTest.csproj
new file mode 100644
index 0000000..936e228
--- /dev/null
+++ b/DatabaseTest/DatabaseTest.csproj
@@ -0,0 +1,25 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/DatabaseTest/Migrations/20240712130909_Initial.Designer.cs b/DatabaseTest/Migrations/20240712130909_Initial.Designer.cs
new file mode 100644
index 0000000..385c9d8
--- /dev/null
+++ b/DatabaseTest/Migrations/20240712130909_Initial.Designer.cs
@@ -0,0 +1,75 @@
+//
+using System;
+using DatabaseTest;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace DatabaseTest.Migrations
+{
+ [DbContext(typeof(DatabaseContext))]
+ [Migration("20240712130909_Initial")]
+ partial class Initial
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "7.0.20");
+
+ modelBuilder.Entity("HopFrame.Database.Models.PermissionEntry", b =>
+ {
+ b.Property("RecordId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("GrantedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("OwnerId")
+ .HasColumnType("INTEGER");
+
+ b.Property("PermissionText")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.HasKey("RecordId");
+
+ b.ToTable("Permissions");
+ });
+
+ modelBuilder.Entity("HopFrame.Database.Models.UserEntry", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("Password")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("Username")
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("Users");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/DatabaseTest/Migrations/20240712130909_Initial.cs b/DatabaseTest/Migrations/20240712130909_Initial.cs
new file mode 100644
index 0000000..7cedbde
--- /dev/null
+++ b/DatabaseTest/Migrations/20240712130909_Initial.cs
@@ -0,0 +1,56 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace DatabaseTest.Migrations
+{
+ ///
+ public partial class Initial : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "Permissions",
+ columns: table => new
+ {
+ RecordId = table.Column(type: "INTEGER", nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ PermissionText = table.Column(type: "TEXT", maxLength: 255, nullable: false),
+ OwnerId = table.Column(type: "INTEGER", nullable: false),
+ GrantedAt = table.Column(type: "TEXT", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Permissions", x => x.RecordId);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Users",
+ columns: table => new
+ {
+ Id = table.Column(type: "INTEGER", nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ Username = table.Column(type: "TEXT", maxLength: 50, nullable: true),
+ Email = table.Column(type: "TEXT", maxLength: 50, nullable: false),
+ Password = table.Column(type: "TEXT", maxLength: 255, nullable: false),
+ CreatedAt = table.Column(type: "TEXT", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Users", x => x.Id);
+ });
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "Permissions");
+
+ migrationBuilder.DropTable(
+ name: "Users");
+ }
+ }
+}
diff --git a/DatabaseTest/Migrations/20240712143345_Tokens.Designer.cs b/DatabaseTest/Migrations/20240712143345_Tokens.Designer.cs
new file mode 100644
index 0000000..334bd32
--- /dev/null
+++ b/DatabaseTest/Migrations/20240712143345_Tokens.Designer.cs
@@ -0,0 +1,101 @@
+//
+using System;
+using DatabaseTest;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace DatabaseTest.Migrations
+{
+ [DbContext(typeof(DatabaseContext))]
+ [Migration("20240712143345_Tokens")]
+ partial class Tokens
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "7.0.20");
+
+ modelBuilder.Entity("HopFrame.Database.Models.Entries.PermissionEntry", b =>
+ {
+ b.Property("RecordId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("GrantedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("OwnerId")
+ .HasColumnType("INTEGER");
+
+ b.Property("PermissionText")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.HasKey("RecordId");
+
+ b.ToTable("Permissions");
+ });
+
+ modelBuilder.Entity("HopFrame.Database.Models.Entries.UserEntry", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("Password")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("Username")
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("HopFrame.Security.Models.TokenEntry", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("Token")
+ .IsRequired()
+ .HasMaxLength(36)
+ .HasColumnType("TEXT");
+
+ b.Property("Type")
+ .HasMaxLength(1)
+ .HasColumnType("INTEGER");
+
+ b.Property("UserId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("Tokens");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/DatabaseTest/Migrations/20240712143345_Tokens.cs b/DatabaseTest/Migrations/20240712143345_Tokens.cs
new file mode 100644
index 0000000..4aaf872
--- /dev/null
+++ b/DatabaseTest/Migrations/20240712143345_Tokens.cs
@@ -0,0 +1,38 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace DatabaseTest.Migrations
+{
+ ///
+ public partial class Tokens : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "Tokens",
+ columns: table => new
+ {
+ Id = table.Column(type: "INTEGER", nullable: false)
+ .Annotation("Sqlite:Autoincrement", true),
+ Type = table.Column(type: "INTEGER", maxLength: 1, nullable: false),
+ Token = table.Column(type: "TEXT", maxLength: 36, nullable: false),
+ UserId = table.Column(type: "INTEGER", nullable: false),
+ CreatedAt = table.Column(type: "TEXT", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Tokens", x => x.Id);
+ });
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "Tokens");
+ }
+ }
+}
diff --git a/DatabaseTest/Migrations/20240713083821_Security.Designer.cs b/DatabaseTest/Migrations/20240713083821_Security.Designer.cs
new file mode 100644
index 0000000..de4bfa3
--- /dev/null
+++ b/DatabaseTest/Migrations/20240713083821_Security.Designer.cs
@@ -0,0 +1,100 @@
+//
+using System;
+using DatabaseTest;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace DatabaseTest.Migrations
+{
+ [DbContext(typeof(DatabaseContext))]
+ [Migration("20240713083821_Security")]
+ partial class Security
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "8.0.7");
+
+ modelBuilder.Entity("HopFrame.Database.Models.Entries.PermissionEntry", b =>
+ {
+ b.Property("RecordId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("GrantedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("PermissionText")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasMaxLength(36)
+ .HasColumnType("TEXT");
+
+ b.HasKey("RecordId");
+
+ b.ToTable("Permissions");
+ });
+
+ modelBuilder.Entity("HopFrame.Database.Models.Entries.TokenEntry", b =>
+ {
+ b.Property("Token")
+ .HasMaxLength(36)
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("Type")
+ .HasMaxLength(1)
+ .HasColumnType("INTEGER");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasMaxLength(36)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Token");
+
+ b.ToTable("Tokens");
+ });
+
+ modelBuilder.Entity("HopFrame.Database.Models.Entries.UserEntry", b =>
+ {
+ b.Property("Id")
+ .HasMaxLength(36)
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("Password")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("Username")
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("Users");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/DatabaseTest/Migrations/20240713083821_Security.cs b/DatabaseTest/Migrations/20240713083821_Security.cs
new file mode 100644
index 0000000..9e581d8
--- /dev/null
+++ b/DatabaseTest/Migrations/20240713083821_Security.cs
@@ -0,0 +1,109 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace DatabaseTest.Migrations
+{
+ ///
+ public partial class Security : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropPrimaryKey(
+ name: "PK_Tokens",
+ table: "Tokens");
+
+ migrationBuilder.DropColumn(
+ name: "Id",
+ table: "Tokens");
+
+ migrationBuilder.DropColumn(
+ name: "OwnerId",
+ table: "Permissions");
+
+ migrationBuilder.AlterColumn(
+ name: "Id",
+ table: "Users",
+ type: "TEXT",
+ maxLength: 36,
+ nullable: false,
+ oldClrType: typeof(long),
+ oldType: "INTEGER")
+ .OldAnnotation("Sqlite:Autoincrement", true);
+
+ migrationBuilder.AlterColumn(
+ name: "UserId",
+ table: "Tokens",
+ type: "TEXT",
+ maxLength: 36,
+ nullable: false,
+ oldClrType: typeof(long),
+ oldType: "INTEGER");
+
+ migrationBuilder.AddColumn(
+ name: "UserId",
+ table: "Permissions",
+ type: "TEXT",
+ maxLength: 36,
+ nullable: false,
+ defaultValue: "");
+
+ migrationBuilder.AddPrimaryKey(
+ name: "PK_Tokens",
+ table: "Tokens",
+ column: "Token");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropPrimaryKey(
+ name: "PK_Tokens",
+ table: "Tokens");
+
+ migrationBuilder.DropColumn(
+ name: "UserId",
+ table: "Permissions");
+
+ migrationBuilder.AlterColumn(
+ name: "Id",
+ table: "Users",
+ type: "INTEGER",
+ nullable: false,
+ oldClrType: typeof(string),
+ oldType: "TEXT",
+ oldMaxLength: 36)
+ .Annotation("Sqlite:Autoincrement", true);
+
+ migrationBuilder.AlterColumn(
+ name: "UserId",
+ table: "Tokens",
+ type: "INTEGER",
+ nullable: false,
+ oldClrType: typeof(string),
+ oldType: "TEXT",
+ oldMaxLength: 36);
+
+ migrationBuilder.AddColumn(
+ name: "Id",
+ table: "Tokens",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: 0L)
+ .Annotation("Sqlite:Autoincrement", true);
+
+ migrationBuilder.AddColumn(
+ name: "OwnerId",
+ table: "Permissions",
+ type: "INTEGER",
+ nullable: false,
+ defaultValue: 0L);
+
+ migrationBuilder.AddPrimaryKey(
+ name: "PK_Tokens",
+ table: "Tokens",
+ column: "Id");
+ }
+ }
+}
diff --git a/DatabaseTest/Migrations/DatabaseContextModelSnapshot.cs b/DatabaseTest/Migrations/DatabaseContextModelSnapshot.cs
new file mode 100644
index 0000000..d4c5d10
--- /dev/null
+++ b/DatabaseTest/Migrations/DatabaseContextModelSnapshot.cs
@@ -0,0 +1,97 @@
+//
+using System;
+using DatabaseTest;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace DatabaseTest.Migrations
+{
+ [DbContext(typeof(DatabaseContext))]
+ partial class DatabaseContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "8.0.7");
+
+ modelBuilder.Entity("HopFrame.Database.Models.Entries.PermissionEntry", b =>
+ {
+ b.Property("RecordId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("GrantedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("PermissionText")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasMaxLength(36)
+ .HasColumnType("TEXT");
+
+ b.HasKey("RecordId");
+
+ b.ToTable("Permissions");
+ });
+
+ modelBuilder.Entity("HopFrame.Database.Models.Entries.TokenEntry", b =>
+ {
+ b.Property("Token")
+ .HasMaxLength(36)
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("Type")
+ .HasMaxLength(1)
+ .HasColumnType("INTEGER");
+
+ b.Property("UserId")
+ .IsRequired()
+ .HasMaxLength(36)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Token");
+
+ b.ToTable("Tokens");
+ });
+
+ modelBuilder.Entity("HopFrame.Database.Models.Entries.UserEntry", b =>
+ {
+ b.Property("Id")
+ .HasMaxLength(36)
+ .HasColumnType("TEXT");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.Property("Password")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("Username")
+ .HasMaxLength(50)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("Users");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/DatabaseTest/Program.cs b/DatabaseTest/Program.cs
new file mode 100644
index 0000000..8cc3933
--- /dev/null
+++ b/DatabaseTest/Program.cs
@@ -0,0 +1,60 @@
+using DatabaseTest;
+using HopFrame.Api;
+using HopFrame.Api.Controller;
+using HopFrame.Security.Authentication;
+using Microsoft.OpenApi.Models;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Add services to the container.
+
+builder.Services.AddControllers()
+ .AddController>();
+// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen();
+
+builder.Services.AddDbContext();
+builder.Services.AddHopFrameAuthentication();
+//builder.Logging.AddFilter>(options => options == LogLevel.None);
+
+builder.Services.AddSwaggerGen(c => {
+ c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme {
+ Description = @"JWT Authorization header using the Bearer scheme. \r\n\r\n
+ Enter 'Bearer' [space] and then your token in the text input below.",
+ Name = "Authorization",
+ In = ParameterLocation.Header,
+ Type = SecuritySchemeType.ApiKey,
+ Scheme = "Bearer"
+ });
+
+ c.AddSecurityRequirement(new OpenApiSecurityRequirement {{
+ new OpenApiSecurityScheme {
+ Reference = new OpenApiReference
+ {
+ Type = ReferenceType.SecurityScheme,
+ Id = "Bearer"
+ },
+ Scheme = "oauth2",
+ Name = "Bearer",
+ In = ParameterLocation.Header,
+ },
+ ArraySegment.Empty
+ }});
+});
+
+var app = builder.Build();
+
+// Configure the HTTP request pipeline.
+if (app.Environment.IsDevelopment()) {
+ app.UseSwagger();
+ app.UseSwaggerUI();
+}
+
+//app.UseHttpsRedirection();
+
+app.UseAuthorization();
+
+app.MapControllers();
+
+app.Run();
\ No newline at end of file
diff --git a/DatabaseTest/Properties/launchSettings.json b/DatabaseTest/Properties/launchSettings.json
new file mode 100644
index 0000000..6418a5c
--- /dev/null
+++ b/DatabaseTest/Properties/launchSettings.json
@@ -0,0 +1,41 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:19326",
+ "sslPort": 44320
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "http://localhost:5158",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "https://localhost:7283;http://localhost:5158",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/DatabaseTest/appsettings.json b/DatabaseTest/appsettings.json
new file mode 100644
index 0000000..470eccb
--- /dev/null
+++ b/DatabaseTest/appsettings.json
@@ -0,0 +1,10 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "HopFrame.Security.Authentication.HopFrameAuthentication": "None"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/HopFrame.Api/Controller/SecurityController.cs b/HopFrame.Api/Controller/SecurityController.cs
new file mode 100644
index 0000000..a9022b1
--- /dev/null
+++ b/HopFrame.Api/Controller/SecurityController.cs
@@ -0,0 +1,178 @@
+using System.Globalization;
+using System.Text;
+using HopFrame.Api.Logic;
+using HopFrame.Api.Models;
+using HopFrame.Database;
+using HopFrame.Database.Models.Entries;
+using HopFrame.Security.Authentication;
+using HopFrame.Security.Authorization;
+using HopFrame.Security.Claims;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+
+namespace HopFrame.Api.Controller;
+
+[ApiController]
+[Route("authentication")]
+public class SecurityController(TDbContext context) : ControllerBase where TDbContext : HopDbContextBase {
+
+ private const string RefreshTokenType = "HopFrame.Security.RefreshToken";
+
+ [HttpPut("login")]
+ public async Task>> Login([FromBody] UserLogin login) {
+ var user = await context.Users.SingleOrDefaultAsync(user => user.Email == login.Email);
+
+ if (user is null)
+ return LogicResult>.NotFound("The provided email address was not found");
+
+ var hashedPassword = EncryptionManager.Hash(login.Password, Encoding.Default.GetBytes(user.CreatedAt.ToString(CultureInfo.InvariantCulture)));
+ if (hashedPassword != user.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
+ };
+ var accessToken = new TokenEntry {
+ CreatedAt = DateTime.Now,
+ Token = Guid.NewGuid().ToString(),
+ Type = TokenEntry.AccessTokenType,
+ UserId = user.Id
+ };
+
+ HttpContext.Response.Cookies.Append(RefreshTokenType, refreshToken.Token, new CookieOptions {
+ MaxAge = HopFrameAuthentication.RefreshTokenTime,
+ HttpOnly = true,
+ Secure = true
+ });
+
+ await context.Tokens.AddRangeAsync(refreshToken, accessToken);
+ await context.SaveChangesAsync();
+
+ return LogicResult>.Ok(accessToken.Token);
+ }
+
+ [HttpPost("register")]
+ public async Task>> Register([FromBody] UserRegister register) {
+ //TODO: Validate Password requirements
+
+ if (await context.Users.AnyAsync(user => user.Username == register.Username || user.Email == register.Email))
+ return LogicResult>.Conflict("Username or Email is already registered");
+
+ var user = new UserEntry {
+ CreatedAt = DateTime.Now,
+ Email = register.Email,
+ Username = register.Username,
+ Id = Guid.NewGuid().ToString()
+ };
+ user.Password = EncryptionManager.Hash(register.Password, Encoding.Default.GetBytes(user.CreatedAt.ToString(CultureInfo.InvariantCulture)));
+
+ await context.Users.AddAsync(user);
+
+ var refreshToken = new TokenEntry {
+ CreatedAt = DateTime.Now,
+ Token = Guid.NewGuid().ToString(),
+ Type = TokenEntry.RefreshTokenType,
+ UserId = user.Id
+ };
+ var accessToken = new TokenEntry {
+ CreatedAt = DateTime.Now,
+ Token = Guid.NewGuid().ToString(),
+ Type = TokenEntry.AccessTokenType,
+ UserId = user.Id
+ };
+
+ HttpContext.Response.Cookies.Append(RefreshTokenType, refreshToken.Token, new CookieOptions {
+ MaxAge = HopFrameAuthentication.RefreshTokenTime,
+ HttpOnly = true,
+ Secure = true
+ });
+
+ await context.Tokens.AddRangeAsync(refreshToken, accessToken);
+ await context.SaveChangesAsync();
+
+ return LogicResult>.Ok(accessToken.Token);
+ }
+
+ [HttpGet("authenticate")]
+ public async Task>> Authenticate() {
+ var refreshToken = HttpContext.Request.Cookies[RefreshTokenType];
+
+ 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);
+
+ if (token is null)
+ return LogicResult>.NotFound("Refresh token not valid");
+
+ 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();
+
+ return LogicResult>.Ok(accessToken.Token);
+ }
+
+ [HttpDelete("logout"), Authorized]
+ public async Task Logout() {
+ var accessToken = HttpContext.User.GetAccessTokenId();
+ var refreshToken = HttpContext.Request.Cookies[RefreshTokenType];
+
+ 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();
+
+ HttpContext.Response.Cookies.Delete(RefreshTokenType);
+
+ return LogicResult.Ok();
+ }
+
+ [HttpDelete("delete"), Authorized]
+ public async Task Delete([FromBody] UserLogin login) {
+ var token = HttpContext.User.GetAccessTokenId();
+ var userId = (await context.Tokens.SingleOrDefaultAsync(t => t.Token == token && t.Type == TokenEntry.AccessTokenType))?.UserId;
+
+ if (string.IsNullOrEmpty(userId))
+ return LogicResult.NotFound("Access token does not match any user");
+
+ var user = await context.Users.SingleAsync(user => user.Id == userId);
+
+ var password = EncryptionManager.Hash(login.Password, Encoding.Default.GetBytes(user.CreatedAt.ToString(CultureInfo.InvariantCulture)));
+ if (user.Password != password)
+ return LogicResult.Forbidden("The provided password is not correct");
+
+ var tokens = await context.Tokens.Where(t => t.UserId == userId).ToArrayAsync();
+
+ context.Tokens.RemoveRange(tokens);
+ context.Users.Remove(user);
+ await context.SaveChangesAsync();
+
+ HttpContext.Response.Cookies.Delete(RefreshTokenType);
+
+ return LogicResult.Ok();
+ }
+
+}
\ No newline at end of file
diff --git a/HopFrame.Api/ControllerExtensions.cs b/HopFrame.Api/ControllerExtensions.cs
new file mode 100644
index 0000000..e8d3fb4
--- /dev/null
+++ b/HopFrame.Api/ControllerExtensions.cs
@@ -0,0 +1,12 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace HopFrame.Api;
+
+public static class ControllerExtensions {
+
+ public static IMvcBuilder AddController(this IMvcBuilder builder) where TController : ControllerBase {
+ return builder.AddApplicationPart(typeof(TController).Assembly);
+ }
+
+}
\ No newline at end of file
diff --git a/HopFrame.Api/EncryptionManager.cs b/HopFrame.Api/EncryptionManager.cs
new file mode 100644
index 0000000..8b29a57
--- /dev/null
+++ b/HopFrame.Api/EncryptionManager.cs
@@ -0,0 +1,17 @@
+using Microsoft.AspNetCore.Cryptography.KeyDerivation;
+
+namespace HopFrame.Api;
+
+public static class EncryptionManager {
+
+ public static string Hash(string input, byte[] salt, KeyDerivationPrf method = KeyDerivationPrf.HMACSHA256) {
+ return Convert.ToBase64String(KeyDerivation.Pbkdf2(
+ password: input,
+ salt: salt,
+ prf: method,
+ iterationCount: 100000,
+ numBytesRequested: 256 / 8
+ ));
+ }
+
+}
\ No newline at end of file
diff --git a/HopFrame.Api/HopFrame.Api.csproj b/HopFrame.Api/HopFrame.Api.csproj
new file mode 100644
index 0000000..a6269a1
--- /dev/null
+++ b/HopFrame.Api/HopFrame.Api.csproj
@@ -0,0 +1,15 @@
+
+
+
+ net8.0
+ latest
+ enable
+ disable
+
+
+
+
+
+
+
+
diff --git a/HopFrame.Api/Logic/ControllerBaseExtension.cs b/HopFrame.Api/Logic/ControllerBaseExtension.cs
new file mode 100644
index 0000000..27fedc9
--- /dev/null
+++ b/HopFrame.Api/Logic/ControllerBaseExtension.cs
@@ -0,0 +1,50 @@
+using System.Net;
+using Microsoft.AspNetCore.Mvc;
+
+namespace HopFrame.Api.Logic;
+
+public static class ControllerBaseExtension {
+ public static ActionResult FromLogicResult(this ControllerBase controller, ILogicResult result) {
+ switch (result.State) {
+ case LogicResultState.Ok:
+ return controller.Ok();
+
+ case LogicResultState.BadRequest:
+ return controller.StatusCode((int)HttpStatusCode.BadRequest, result.Message);
+
+ case LogicResultState.Forbidden:
+ return controller.StatusCode((int)HttpStatusCode.Forbidden, result.Message);
+
+ case LogicResultState.NotFound:
+ return controller.StatusCode((int)HttpStatusCode.NotFound, result.Message);
+
+ case LogicResultState.Conflict:
+ return controller.StatusCode((int)HttpStatusCode.Conflict, result.Message);
+
+ default:
+ throw new Exception("An unhandled result has occurred as a result of a service call.");
+ }
+ }
+
+ public static ActionResult FromLogicResult(this ControllerBase controller, ILogicResult result) {
+ switch (result.State) {
+ case LogicResultState.Ok:
+ return controller.Ok(result.Data);
+
+ case LogicResultState.BadRequest:
+ return controller.StatusCode((int)HttpStatusCode.BadRequest, result.Message);
+
+ case LogicResultState.Forbidden:
+ return controller.StatusCode((int)HttpStatusCode.Forbidden, result.Message);
+
+ case LogicResultState.NotFound:
+ return controller.StatusCode((int)HttpStatusCode.NotFound, result.Message);
+
+ case LogicResultState.Conflict:
+ return controller.StatusCode((int)HttpStatusCode.Conflict, result.Message);
+
+ default:
+ throw new Exception("An unhandled result has occurred as a result of a service call.");
+ }
+ }
+}
\ No newline at end of file
diff --git a/HopFrame.Api/Logic/ILogicResult.cs b/HopFrame.Api/Logic/ILogicResult.cs
new file mode 100644
index 0000000..6edb70a
--- /dev/null
+++ b/HopFrame.Api/Logic/ILogicResult.cs
@@ -0,0 +1,19 @@
+namespace HopFrame.Api.Logic;
+
+public interface ILogicResult {
+ LogicResultState State { get; set; }
+
+ string Message { get; set; }
+
+ bool IsSuccessful { get; }
+}
+
+public interface ILogicResult {
+ LogicResultState State { get; set; }
+
+ T Data { get; set; }
+
+ string Message { get; set; }
+
+ bool IsSuccessful { get; }
+}
\ No newline at end of file
diff --git a/HopFrame.Api/Logic/LogicResult.cs b/HopFrame.Api/Logic/LogicResult.cs
new file mode 100644
index 0000000..cc3254a
--- /dev/null
+++ b/HopFrame.Api/Logic/LogicResult.cs
@@ -0,0 +1,170 @@
+namespace HopFrame.Api.Logic;
+
+public class LogicResult : ILogicResult {
+ public LogicResultState State { get; set; }
+
+ public string Message { get; set; }
+
+ public bool IsSuccessful => State == LogicResultState.Ok;
+
+ public static LogicResult Ok() {
+ return new LogicResult() {
+ State = LogicResultState.Ok
+ };
+ }
+
+ public static LogicResult BadRequest() {
+ return new LogicResult() {
+ State = LogicResultState.BadRequest
+ };
+ }
+
+ public static LogicResult BadRequest(string message) {
+ return new LogicResult() {
+ State = LogicResultState.BadRequest,
+ Message = message
+ };
+ }
+
+ public static LogicResult Forbidden() {
+ return new LogicResult() {
+ State = LogicResultState.Forbidden
+ };
+ }
+
+ public static LogicResult Forbidden(string message) {
+ return new LogicResult() {
+ State = LogicResultState.Forbidden,
+ Message = message
+ };
+ }
+
+ public static LogicResult NotFound() {
+ return new LogicResult() {
+ State = LogicResultState.NotFound
+ };
+ }
+
+ public static LogicResult NotFound(string message) {
+ return new LogicResult() {
+ State = LogicResultState.NotFound,
+ Message = message
+ };
+ }
+
+ public static LogicResult Conflict() {
+ return new LogicResult() {
+ State = LogicResultState.Conflict
+ };
+ }
+
+ public static LogicResult Conflict(string message) {
+ return new LogicResult() {
+ State = LogicResultState.Conflict,
+ Message = message
+ };
+ }
+
+ public static LogicResult Forward(LogicResult result) {
+ return new LogicResult() {
+ State = result.State,
+ Message = result.Message
+ };
+ }
+
+ public static LogicResult Forward(ILogicResult result) {
+ return new LogicResult() {
+ State = result.State,
+ Message = result.Message
+ };
+ }
+}
+
+public class LogicResult : ILogicResult {
+ public LogicResultState State { get; set; }
+
+ public T Data { get; set; }
+
+ public string Message { get; set; }
+
+ public bool IsSuccessful => State == LogicResultState.Ok;
+
+ public static LogicResult Ok() {
+ return new LogicResult() {
+ State = LogicResultState.Ok
+ };
+ }
+
+ public static LogicResult Ok(T result) {
+ return new LogicResult() {
+ State = LogicResultState.Ok,
+ Data = result
+ };
+ }
+
+ public static LogicResult BadRequest() {
+ return new LogicResult() {
+ State = LogicResultState.BadRequest
+ };
+ }
+
+ public static LogicResult BadRequest(string message) {
+ return new LogicResult() {
+ State = LogicResultState.BadRequest,
+ Message = message
+ };
+ }
+
+ public static LogicResult Forbidden() {
+ return new LogicResult() {
+ State = LogicResultState.Forbidden
+ };
+ }
+
+ public static LogicResult Forbidden(string message) {
+ return new LogicResult() {
+ State = LogicResultState.Forbidden,
+ Message = message
+ };
+ }
+
+ public static LogicResult NotFound() {
+ return new LogicResult() {
+ State = LogicResultState.NotFound
+ };
+ }
+
+ public static LogicResult NotFound(string message) {
+ return new LogicResult() {
+ State = LogicResultState.NotFound,
+ Message = message
+ };
+ }
+
+ public static LogicResult Conflict() {
+ return new LogicResult() {
+ State = LogicResultState.Conflict
+ };
+ }
+
+ public static LogicResult Conflict(string message) {
+ return new LogicResult() {
+ State = LogicResultState.Conflict,
+ Message = message
+ };
+ }
+
+ public static LogicResult Forward(ILogicResult result) {
+ return new LogicResult() {
+ State = result.State,
+ Message = result.Message
+ };
+ }
+
+ public static LogicResult Forward(ILogicResult result) {
+ return new LogicResult() {
+ State = result.State,
+ Message = result.Message
+ };
+ }
+}
\ No newline at end of file
diff --git a/HopFrame.Api/Logic/LogicResultState.cs b/HopFrame.Api/Logic/LogicResultState.cs
new file mode 100644
index 0000000..69d5b90
--- /dev/null
+++ b/HopFrame.Api/Logic/LogicResultState.cs
@@ -0,0 +1,9 @@
+namespace HopFrame.Api.Logic;
+
+public enum LogicResultState {
+ Ok,
+ BadRequest,
+ Forbidden,
+ NotFound,
+ Conflict
+}
\ No newline at end of file
diff --git a/HopFrame.Api/Models/SingleValueResult.cs b/HopFrame.Api/Models/SingleValueResult.cs
new file mode 100644
index 0000000..b1eb551
--- /dev/null
+++ b/HopFrame.Api/Models/SingleValueResult.cs
@@ -0,0 +1,13 @@
+namespace HopFrame.Api.Models;
+
+public struct SingleValueResult(T value) {
+ public T Value { get; set; } = value;
+
+ public static implicit operator T(SingleValueResult v) {
+ return v.Value;
+ }
+
+ public static implicit operator SingleValueResult(T v) {
+ return new SingleValueResult(v);
+ }
+}
\ No newline at end of file
diff --git a/HopFrame.Api/Models/UserLogin.cs b/HopFrame.Api/Models/UserLogin.cs
new file mode 100644
index 0000000..ccaac7e
--- /dev/null
+++ b/HopFrame.Api/Models/UserLogin.cs
@@ -0,0 +1,6 @@
+namespace HopFrame.Api.Models;
+
+public struct UserLogin {
+ public string Email { get; set; }
+ public string Password { get; set; }
+}
\ No newline at end of file
diff --git a/HopFrame.Api/Models/UserRegister.cs b/HopFrame.Api/Models/UserRegister.cs
new file mode 100644
index 0000000..aacafdc
--- /dev/null
+++ b/HopFrame.Api/Models/UserRegister.cs
@@ -0,0 +1,7 @@
+namespace HopFrame.Api.Models;
+
+public struct UserRegister {
+ public string Username { get; set; }
+ public string Email { get; set; }
+ public string Password { get; set; }
+}
\ No newline at end of file
diff --git a/HopFrame.Database/HopDbContextBase.cs b/HopFrame.Database/HopDbContextBase.cs
new file mode 100644
index 0000000..e5872bf
--- /dev/null
+++ b/HopFrame.Database/HopDbContextBase.cs
@@ -0,0 +1,22 @@
+using HopFrame.Database.Models.Entries;
+using Microsoft.EntityFrameworkCore;
+
+namespace HopFrame.Database;
+
+public class HopDbContextBase : DbContext {
+
+ public HopDbContextBase() {}
+
+ public HopDbContextBase(DbContextOptions options) : base(options) {}
+
+ public virtual DbSet Users { get; set; }
+ public virtual DbSet Permissions { get; set; }
+ public virtual DbSet Tokens { get; set; }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder) {
+ base.OnModelCreating(modelBuilder);
+
+ modelBuilder.Entity();
+ modelBuilder.Entity();
+ }
+}
\ No newline at end of file
diff --git a/HopFrame.Database/HopFrame.Database.csproj b/HopFrame.Database/HopFrame.Database.csproj
new file mode 100644
index 0000000..b0a20a0
--- /dev/null
+++ b/HopFrame.Database/HopFrame.Database.csproj
@@ -0,0 +1,14 @@
+
+
+
+ net8.0
+ latest
+ enable
+ disable
+
+
+
+
+
+
+
diff --git a/HopFrame.Database/Models/Entries/PermissionEntry.cs b/HopFrame.Database/Models/Entries/PermissionEntry.cs
new file mode 100644
index 0000000..2f8bdae
--- /dev/null
+++ b/HopFrame.Database/Models/Entries/PermissionEntry.cs
@@ -0,0 +1,18 @@
+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/HopFrame.Database/Models/Entries/TokenEntry.cs b/HopFrame.Database/Models/Entries/TokenEntry.cs
new file mode 100644
index 0000000..d33b307
--- /dev/null
+++ b/HopFrame.Database/Models/Entries/TokenEntry.cs
@@ -0,0 +1,25 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace HopFrame.Database.Models.Entries;
+
+public class TokenEntry {
+ public const int RefreshTokenType = 0;
+ public const int AccessTokenType = 1;
+
+ ///
+ /// Defines the Type of the stored Token
+ /// 0: Refresh token
+ /// 1: Access token
+ ///
+ [Required, MinLength(1), MaxLength(1)]
+ public int Type { get; set; }
+
+ [Key, Required, MinLength(36), MaxLength(36)]
+ public string Token { get; set; }
+
+ [Required, MinLength(36), MaxLength(36)]
+ public string UserId { get; set; }
+
+ [Required]
+ public DateTime CreatedAt { get; set; }
+}
\ No newline at end of file
diff --git a/HopFrame.Database/Models/Entries/UserEntry.cs b/HopFrame.Database/Models/Entries/UserEntry.cs
new file mode 100644
index 0000000..2bc1a12
--- /dev/null
+++ b/HopFrame.Database/Models/Entries/UserEntry.cs
@@ -0,0 +1,20 @@
+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/HopFrame.Database/Models/ModelExtensions.cs b/HopFrame.Database/Models/ModelExtensions.cs
new file mode 100644
index 0000000..b0cc7a6
--- /dev/null
+++ b/HopFrame.Database/Models/ModelExtensions.cs
@@ -0,0 +1,23 @@
+using HopFrame.Database.Models.Entries;
+
+namespace HopFrame.Database.Models;
+
+public static class ModelExtensions {
+
+ 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.PermissionText)
+ .ToList();
+
+ return user;
+ }
+
+}
\ No newline at end of file
diff --git a/HopFrame.Database/Models/User.cs b/HopFrame.Database/Models/User.cs
new file mode 100644
index 0000000..cbedd0c
--- /dev/null
+++ b/HopFrame.Database/Models/User.cs
@@ -0,0 +1,9 @@
+namespace HopFrame.Database.Models;
+
+public class User {
+ public Guid Id { get; set; }
+ public string Username { get; set; }
+ public string Email { get; set; }
+ public DateTime CreatedAt { get; set; }
+ public IList Permissions { get; set; }
+}
\ No newline at end of file
diff --git a/HopFrame.Security/Authentication/HopFrameAuthentication.cs b/HopFrame.Security/Authentication/HopFrameAuthentication.cs
new file mode 100644
index 0000000..e021a0c
--- /dev/null
+++ b/HopFrame.Security/Authentication/HopFrameAuthentication.cs
@@ -0,0 +1,56 @@
+using System.Security.Claims;
+using System.Text.Encodings.Web;
+using HopFrame.Database;
+using HopFrame.Security.Claims;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
+
+namespace HopFrame.Security.Authentication;
+
+public class HopFrameAuthentication : AuthenticationHandler where TDbContext : HopDbContextBase {
+
+ public const string SchemeName = "HopCore.Authentication";
+ public static readonly TimeSpan AccessTokenTime = new(0, 0, 5, 0);
+ public static readonly TimeSpan RefreshTokenTime = new(30, 0, 0, 0);
+
+ private readonly TDbContext _context;
+
+ public HopFrameAuthentication(IOptionsMonitor options, ILoggerFactory logger,
+ UrlEncoder encoder, ISystemClock clock, TDbContext context) : base(options, logger, encoder, clock) {
+ _context = context;
+ }
+
+ protected override async Task HandleAuthenticateAsync() {
+ var accessToken = Request.Headers["Authorization"].ToString();
+ if (string.IsNullOrEmpty(accessToken)) return AuthenticateResult.Fail("No Access Token provided");
+
+ var tokenEntry = await _context.Tokens.SingleOrDefaultAsync(token => token.Token == 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)))
+ 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.ToString())
+ };
+
+ var permissions = await _context.Permissions
+ .Where(perm => perm.UserId == tokenEntry.UserId)
+ .Select(perm => perm.PermissionText)
+ .ToListAsync();
+
+ claims.AddRange(permissions.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm)));
+
+ var principal = new ClaimsPrincipal();
+ principal.AddIdentity(new ClaimsIdentity(claims, SchemeName));
+ return AuthenticateResult.Success(new AuthenticationTicket(principal, Scheme.Name));
+ }
+
+}
\ No newline at end of file
diff --git a/HopFrame.Security/Authentication/HopFrameAuthenticationExtensions.cs b/HopFrame.Security/Authentication/HopFrameAuthenticationExtensions.cs
new file mode 100644
index 0000000..e6faedc
--- /dev/null
+++ b/HopFrame.Security/Authentication/HopFrameAuthenticationExtensions.cs
@@ -0,0 +1,13 @@
+using HopFrame.Database;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace HopFrame.Security.Authentication;
+
+public static class HopFrameAuthenticationExtensions {
+
+ public static AuthenticationBuilder AddHopFrameAuthentication(this IServiceCollection service) where TDbContext : HopDbContextBase {
+ return service.AddAuthentication(HopFrameAuthentication.SchemeName).AddScheme>(HopFrameAuthentication.SchemeName, _ => {});
+ }
+
+}
\ No newline at end of file
diff --git a/HopFrame.Security/Authorization/AuthorizedAttribute.cs b/HopFrame.Security/Authorization/AuthorizedAttribute.cs
new file mode 100644
index 0000000..0180b52
--- /dev/null
+++ b/HopFrame.Security/Authorization/AuthorizedAttribute.cs
@@ -0,0 +1,9 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace HopFrame.Security.Authorization;
+
+public class AuthorizedAttribute : TypeFilterAttribute {
+ public AuthorizedAttribute(params string[] permission) : base(typeof(AuthorizedFilter)) {
+ Arguments = new object[] { permission };
+ }
+}
\ No newline at end of file
diff --git a/HopFrame.Security/Authorization/AuthorizedFilter.cs b/HopFrame.Security/Authorization/AuthorizedFilter.cs
new file mode 100644
index 0000000..e7f8859
--- /dev/null
+++ b/HopFrame.Security/Authorization/AuthorizedFilter.cs
@@ -0,0 +1,24 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Authorization;
+using Microsoft.AspNetCore.Mvc.Filters;
+
+namespace HopFrame.Security.Authorization;
+
+public class AuthorizedFilter : IAuthorizationFilter {
+ private readonly string[] _permissions;
+
+ public AuthorizedFilter(params string[] permissions) {
+ _permissions = permissions;
+ }
+
+ public void OnAuthorization(AuthorizationFilterContext context) {
+ if (context.Filters.Any(item => item is IAllowAnonymousFilter)) return;
+
+ if (context.HttpContext.User.Identity?.IsAuthenticated == false) {
+ context.Result = new UnauthorizedResult();
+ return;
+ }
+
+ //TODO: Check Permissions
+ }
+}
\ No newline at end of file
diff --git a/HopFrame.Security/Claims/HopFrameClaimTypes.cs b/HopFrame.Security/Claims/HopFrameClaimTypes.cs
new file mode 100644
index 0000000..595f76c
--- /dev/null
+++ b/HopFrame.Security/Claims/HopFrameClaimTypes.cs
@@ -0,0 +1,21 @@
+using System.Security.Claims;
+
+namespace HopFrame.Security.Claims;
+
+public static class HopFrameClaimTypes {
+ public const string AccessTokenId = "HopFrame.AccessTokenId";
+ public const string UserId = "HopFrame.UserId";
+ public const string Permission = "HopFrame.Permission";
+
+ public static string GetAccessTokenId(this ClaimsPrincipal principal) {
+ return principal.FindFirstValue(AccessTokenId);
+ }
+
+ public static string GetUserId(this ClaimsPrincipal principal) {
+ return principal.FindFirstValue(UserId);
+ }
+
+ public static string[] GetPermissions(this ClaimsPrincipal principal) {
+ return principal.FindAll(Permission).Select(claim => claim.Value).ToArray();
+ }
+}
\ No newline at end of file
diff --git a/HopFrame.Security/Claims/ITokenContextBase.cs b/HopFrame.Security/Claims/ITokenContextBase.cs
new file mode 100644
index 0000000..66788bc
--- /dev/null
+++ b/HopFrame.Security/Claims/ITokenContextBase.cs
@@ -0,0 +1,9 @@
+using HopFrame.Database.Models;
+
+namespace HopFrame.Security.Claims;
+
+public interface ITokenContextBase {
+ bool IsAuthenticated { get; }
+ User User { get; }
+ Guid AccessToken { get; }
+}
\ No newline at end of file
diff --git a/HopFrame.Security/Claims/TokenContextImplementor.cs b/HopFrame.Security/Claims/TokenContextImplementor.cs
new file mode 100644
index 0000000..a66d40b
--- /dev/null
+++ b/HopFrame.Security/Claims/TokenContextImplementor.cs
@@ -0,0 +1,23 @@
+using HopFrame.Database;
+using HopFrame.Database.Models;
+using Microsoft.AspNetCore.Http;
+
+namespace HopFrame.Security.Claims;
+
+public class TokenContextImplementor : ITokenContextBase {
+ private readonly IHttpContextAccessor _accessor;
+ private readonly HopDbContextBase _context;
+
+ public TokenContextImplementor(IHttpContextAccessor accessor, HopDbContextBase context) {
+ _accessor = accessor;
+ _context = context;
+ }
+
+ public bool IsAuthenticated => _accessor.HttpContext?.User.Identity?.IsAuthenticated == true;
+
+ public User User => _context.Users
+ .SingleOrDefault(user => user.Id == _accessor.HttpContext.User.GetUserId())?
+ .ToUserModel(_context);
+
+ public Guid AccessToken => Guid.Parse(_accessor.HttpContext?.User.GetAccessTokenId() ?? string.Empty);
+}
\ No newline at end of file
diff --git a/HopFrame.Security/HopFrame.Security.csproj b/HopFrame.Security/HopFrame.Security.csproj
new file mode 100644
index 0000000..c99bc9b
--- /dev/null
+++ b/HopFrame.Security/HopFrame.Security.csproj
@@ -0,0 +1,20 @@
+
+
+
+ net8.0
+ latest
+ enable
+ disable
+ HopFrame.Security
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/HopFrame.sln b/HopFrame.sln
new file mode 100644
index 0000000..8242697
--- /dev/null
+++ b/HopFrame.sln
@@ -0,0 +1,39 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Database", "HopFrame.Database\HopFrame.Database.csproj", "{003120AE-F38B-4632-8497-BE4505189627}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Testing", "Testing", "{58703056-8DAD-4221-BBE3-42425D2F4929}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DatabaseTest", "DatabaseTest\DatabaseTest.csproj", "{921159CE-AF75-44C3-A3F9-6B9B1A4E85CF}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Security", "HopFrame.Security\HopFrame.Security.csproj", "{7F82E1C6-4A42-4337-9E03-2EE6429D004F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HopFrame.Api", "HopFrame.Api\HopFrame.Api.csproj", "{1E821490-AEDC-4F55-B758-52F4FADAB53A}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {003120AE-F38B-4632-8497-BE4505189627}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {003120AE-F38B-4632-8497-BE4505189627}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {003120AE-F38B-4632-8497-BE4505189627}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {003120AE-F38B-4632-8497-BE4505189627}.Release|Any CPU.Build.0 = Release|Any CPU
+ {921159CE-AF75-44C3-A3F9-6B9B1A4E85CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {921159CE-AF75-44C3-A3F9-6B9B1A4E85CF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {921159CE-AF75-44C3-A3F9-6B9B1A4E85CF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {921159CE-AF75-44C3-A3F9-6B9B1A4E85CF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7F82E1C6-4A42-4337-9E03-2EE6429D004F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7F82E1C6-4A42-4337-9E03-2EE6429D004F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7F82E1C6-4A42-4337-9E03-2EE6429D004F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7F82E1C6-4A42-4337-9E03-2EE6429D004F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1E821490-AEDC-4F55-B758-52F4FADAB53A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1E821490-AEDC-4F55-B758-52F4FADAB53A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1E821490-AEDC-4F55-B758-52F4FADAB53A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1E821490-AEDC-4F55-B758-52F4FADAB53A}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {921159CE-AF75-44C3-A3F9-6B9B1A4E85CF} = {58703056-8DAD-4221-BBE3-42425D2F4929}
+ EndGlobalSection
+EndGlobal
diff --git a/HopFrame.sln.DotSettings.user b/HopFrame.sln.DotSettings.user
new file mode 100644
index 0000000..3b58ba2
--- /dev/null
+++ b/HopFrame.sln.DotSettings.user
@@ -0,0 +1,4 @@
+
+ <AssemblyExplorer>
+ <Assembly Path="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\7.0.16\ref\net7.0\System.ComponentModel.Annotations.dll" />
+</AssemblyExplorer>
\ No newline at end of file
diff --git a/global.json b/global.json
new file mode 100644
index 0000000..2ddda36
--- /dev/null
+++ b/global.json
@@ -0,0 +1,7 @@
+{
+ "sdk": {
+ "version": "8.0.0",
+ "rollForward": "latestMinor",
+ "allowPrerelease": false
+ }
+}
\ No newline at end of file