Finished database management and user authentication

This commit is contained in:
2024-07-13 16:37:36 +02:00
parent fe5b5d28eb
commit c1ac7f9972
47 changed files with 1620 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
bin/
obj/
/packages/
riderModule.iml
/_ReSharper.Caches/

13
.idea/.idea.HopFrame/.idea/.gitignore generated vendored Normal file
View File

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

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>

6
.idea/.idea.HopFrame/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

3
DatabaseTest/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
obj
bin
appsettings.Development.json

View File

@@ -0,0 +1,9 @@
using HopFrame.Api.Controller;
using Microsoft.AspNetCore.Mvc;
namespace DatabaseTest.Controllers;
[ApiController]
public class TestController(DatabaseContext context) : SecurityController<DatabaseContext>(context) {
}

View File

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

View File

@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.7" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\HopFrame.Api\HopFrame.Api.csproj" />
<ProjectReference Include="..\HopFrame.Security\HopFrame.Security.csproj" />
<ProjectReference Include="..\HopFrame.Database\HopFrame.Database.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,75 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<long>("RecordId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("GrantedAt")
.HasColumnType("TEXT");
b.Property<long>("OwnerId")
.HasColumnType("INTEGER");
b.Property<string>("PermissionText")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.HasKey("RecordId");
b.ToTable("Permissions");
});
modelBuilder.Entity("HopFrame.Database.Models.UserEntry", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("Password")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("Username")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Users");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,56 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DatabaseTest.Migrations
{
/// <inheritdoc />
public partial class Initial : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Permissions",
columns: table => new
{
RecordId = table.Column<long>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
PermissionText = table.Column<string>(type: "TEXT", maxLength: 255, nullable: false),
OwnerId = table.Column<long>(type: "INTEGER", nullable: false),
GrantedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Permissions", x => x.RecordId);
});
migrationBuilder.CreateTable(
name: "Users",
columns: table => new
{
Id = table.Column<long>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Username = table.Column<string>(type: "TEXT", maxLength: 50, nullable: true),
Email = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
Password = table.Column<string>(type: "TEXT", maxLength: 255, nullable: false),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Permissions");
migrationBuilder.DropTable(
name: "Users");
}
}
}

View File

@@ -0,0 +1,101 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<long>("RecordId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("GrantedAt")
.HasColumnType("TEXT");
b.Property<long>("OwnerId")
.HasColumnType("INTEGER");
b.Property<string>("PermissionText")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.HasKey("RecordId");
b.ToTable("Permissions");
});
modelBuilder.Entity("HopFrame.Database.Models.Entries.UserEntry", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("Password")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("Username")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("HopFrame.Security.Models.TokenEntry", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Token")
.IsRequired()
.HasMaxLength(36)
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasMaxLength(1)
.HasColumnType("INTEGER");
b.Property<long>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("Tokens");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,38 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DatabaseTest.Migrations
{
/// <inheritdoc />
public partial class Tokens : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Tokens",
columns: table => new
{
Id = table.Column<long>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Type = table.Column<int>(type: "INTEGER", maxLength: 1, nullable: false),
Token = table.Column<string>(type: "TEXT", maxLength: 36, nullable: false),
UserId = table.Column<long>(type: "INTEGER", nullable: false),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Tokens", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Tokens");
}
}
}

View File

@@ -0,0 +1,100 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<long>("RecordId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("GrantedAt")
.HasColumnType("TEXT");
b.Property<string>("PermissionText")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasMaxLength(36)
.HasColumnType("TEXT");
b.HasKey("RecordId");
b.ToTable("Permissions");
});
modelBuilder.Entity("HopFrame.Database.Models.Entries.TokenEntry", b =>
{
b.Property<string>("Token")
.HasMaxLength(36)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasMaxLength(1)
.HasColumnType("INTEGER");
b.Property<string>("UserId")
.IsRequired()
.HasMaxLength(36)
.HasColumnType("TEXT");
b.HasKey("Token");
b.ToTable("Tokens");
});
modelBuilder.Entity("HopFrame.Database.Models.Entries.UserEntry", b =>
{
b.Property<string>("Id")
.HasMaxLength(36)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("Password")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("Username")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Users");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,109 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DatabaseTest.Migrations
{
/// <inheritdoc />
public partial class Security : Migration
{
/// <inheritdoc />
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<string>(
name: "Id",
table: "Users",
type: "TEXT",
maxLength: 36,
nullable: false,
oldClrType: typeof(long),
oldType: "INTEGER")
.OldAnnotation("Sqlite:Autoincrement", true);
migrationBuilder.AlterColumn<string>(
name: "UserId",
table: "Tokens",
type: "TEXT",
maxLength: 36,
nullable: false,
oldClrType: typeof(long),
oldType: "INTEGER");
migrationBuilder.AddColumn<string>(
name: "UserId",
table: "Permissions",
type: "TEXT",
maxLength: 36,
nullable: false,
defaultValue: "");
migrationBuilder.AddPrimaryKey(
name: "PK_Tokens",
table: "Tokens",
column: "Token");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropPrimaryKey(
name: "PK_Tokens",
table: "Tokens");
migrationBuilder.DropColumn(
name: "UserId",
table: "Permissions");
migrationBuilder.AlterColumn<long>(
name: "Id",
table: "Users",
type: "INTEGER",
nullable: false,
oldClrType: typeof(string),
oldType: "TEXT",
oldMaxLength: 36)
.Annotation("Sqlite:Autoincrement", true);
migrationBuilder.AlterColumn<long>(
name: "UserId",
table: "Tokens",
type: "INTEGER",
nullable: false,
oldClrType: typeof(string),
oldType: "TEXT",
oldMaxLength: 36);
migrationBuilder.AddColumn<long>(
name: "Id",
table: "Tokens",
type: "INTEGER",
nullable: false,
defaultValue: 0L)
.Annotation("Sqlite:Autoincrement", true);
migrationBuilder.AddColumn<long>(
name: "OwnerId",
table: "Permissions",
type: "INTEGER",
nullable: false,
defaultValue: 0L);
migrationBuilder.AddPrimaryKey(
name: "PK_Tokens",
table: "Tokens",
column: "Id");
}
}
}

View File

@@ -0,0 +1,97 @@
// <auto-generated />
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<long>("RecordId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("GrantedAt")
.HasColumnType("TEXT");
b.Property<string>("PermissionText")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasMaxLength(36)
.HasColumnType("TEXT");
b.HasKey("RecordId");
b.ToTable("Permissions");
});
modelBuilder.Entity("HopFrame.Database.Models.Entries.TokenEntry", b =>
{
b.Property<string>("Token")
.HasMaxLength(36)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasMaxLength(1)
.HasColumnType("INTEGER");
b.Property<string>("UserId")
.IsRequired()
.HasMaxLength(36)
.HasColumnType("TEXT");
b.HasKey("Token");
b.ToTable("Tokens");
});
modelBuilder.Entity("HopFrame.Database.Models.Entries.UserEntry", b =>
{
b.Property<string>("Id")
.HasMaxLength(36)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("Password")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("Username")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Users");
});
#pragma warning restore 612, 618
}
}
}

60
DatabaseTest/Program.cs Normal file
View File

@@ -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<SecurityController<DatabaseContext>>();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddDbContext<DatabaseContext>();
builder.Services.AddHopFrameAuthentication<DatabaseContext>();
//builder.Logging.AddFilter<HopFrameAuthentication<DatabaseContext>>(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<string>.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();

View File

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

View File

@@ -0,0 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"HopFrame.Security.Authentication.HopFrameAuthentication": "None"
}
},
"AllowedHosts": "*"
}

View File

@@ -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>(TDbContext context) : ControllerBase where TDbContext : HopDbContextBase {
private const string RefreshTokenType = "HopFrame.Security.RefreshToken";
[HttpPut("login")]
public async Task<ILogicResult<SingleValueResult<string>>> Login([FromBody] UserLogin login) {
var user = await context.Users.SingleOrDefaultAsync(user => user.Email == login.Email);
if (user is null)
return LogicResult<SingleValueResult<string>>.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<SingleValueResult<string>>.Forbidden("The provided password is not correct");
var refreshToken = new TokenEntry {
CreatedAt = DateTime.Now,
Token = Guid.NewGuid().ToString(),
Type = TokenEntry.RefreshTokenType,
UserId = user.Id
};
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<TDbContext>.RefreshTokenTime,
HttpOnly = true,
Secure = true
});
await context.Tokens.AddRangeAsync(refreshToken, accessToken);
await context.SaveChangesAsync();
return LogicResult<SingleValueResult<string>>.Ok(accessToken.Token);
}
[HttpPost("register")]
public async Task<ILogicResult<SingleValueResult<string>>> Register([FromBody] UserRegister register) {
//TODO: Validate Password requirements
if (await context.Users.AnyAsync(user => user.Username == register.Username || user.Email == register.Email))
return LogicResult<SingleValueResult<string>>.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<TDbContext>.RefreshTokenTime,
HttpOnly = true,
Secure = true
});
await context.Tokens.AddRangeAsync(refreshToken, accessToken);
await context.SaveChangesAsync();
return LogicResult<SingleValueResult<string>>.Ok(accessToken.Token);
}
[HttpGet("authenticate")]
public async Task<ILogicResult<SingleValueResult<string>>> Authenticate() {
var refreshToken = HttpContext.Request.Cookies[RefreshTokenType];
if (string.IsNullOrEmpty(refreshToken))
return LogicResult<SingleValueResult<string>>.Conflict("Refresh token not provided");
var token = await context.Tokens.SingleOrDefaultAsync(token => token.Token == refreshToken && token.Type == TokenEntry.RefreshTokenType);
if (token is null)
return LogicResult<SingleValueResult<string>>.NotFound("Refresh token not valid");
if (token.CreatedAt + HopFrameAuthentication<TDbContext>.RefreshTokenTime < DateTime.Now)
return LogicResult<SingleValueResult<string>>.Conflict("Refresh token is expired");
var accessToken = new TokenEntry {
CreatedAt = DateTime.Now,
Token = Guid.NewGuid().ToString(),
Type = TokenEntry.AccessTokenType,
UserId = token.UserId
};
await context.Tokens.AddAsync(accessToken);
await context.SaveChangesAsync();
return LogicResult<SingleValueResult<string>>.Ok(accessToken.Token);
}
[HttpDelete("logout"), Authorized]
public async Task<ILogicResult> 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<ILogicResult> 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();
}
}

View File

@@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
namespace HopFrame.Api;
public static class ControllerExtensions {
public static IMvcBuilder AddController<TController>(this IMvcBuilder builder) where TController : ControllerBase {
return builder.AddApplicationPart(typeof(TController).Assembly);
}
}

View File

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

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\HopFrame.Database\HopFrame.Database.csproj" />
<ProjectReference Include="..\HopFrame.Security\HopFrame.Security.csproj" />
</ItemGroup>
</Project>

View File

@@ -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<T>(this ControllerBase controller, ILogicResult<T> 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.");
}
}
}

View File

@@ -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<T> {
LogicResultState State { get; set; }
T Data { get; set; }
string Message { get; set; }
bool IsSuccessful { get; }
}

View File

@@ -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<T>(ILogicResult<T> result) {
return new LogicResult() {
State = result.State,
Message = result.Message
};
}
}
public class LogicResult<T> : ILogicResult<T> {
public LogicResultState State { get; set; }
public T Data { get; set; }
public string Message { get; set; }
public bool IsSuccessful => State == LogicResultState.Ok;
public static LogicResult<T> Ok() {
return new LogicResult<T>() {
State = LogicResultState.Ok
};
}
public static LogicResult<T> Ok(T result) {
return new LogicResult<T>() {
State = LogicResultState.Ok,
Data = result
};
}
public static LogicResult<T> BadRequest() {
return new LogicResult<T>() {
State = LogicResultState.BadRequest
};
}
public static LogicResult<T> BadRequest(string message) {
return new LogicResult<T>() {
State = LogicResultState.BadRequest,
Message = message
};
}
public static LogicResult<T> Forbidden() {
return new LogicResult<T>() {
State = LogicResultState.Forbidden
};
}
public static LogicResult<T> Forbidden(string message) {
return new LogicResult<T>() {
State = LogicResultState.Forbidden,
Message = message
};
}
public static LogicResult<T> NotFound() {
return new LogicResult<T>() {
State = LogicResultState.NotFound
};
}
public static LogicResult<T> NotFound(string message) {
return new LogicResult<T>() {
State = LogicResultState.NotFound,
Message = message
};
}
public static LogicResult<T> Conflict() {
return new LogicResult<T>() {
State = LogicResultState.Conflict
};
}
public static LogicResult<T> Conflict(string message) {
return new LogicResult<T>() {
State = LogicResultState.Conflict,
Message = message
};
}
public static LogicResult<T> Forward(ILogicResult result) {
return new LogicResult<T>() {
State = result.State,
Message = result.Message
};
}
public static LogicResult<T> Forward<T2>(ILogicResult<T2> result) {
return new LogicResult<T>() {
State = result.State,
Message = result.Message
};
}
}

View File

@@ -0,0 +1,9 @@
namespace HopFrame.Api.Logic;
public enum LogicResultState {
Ok,
BadRequest,
Forbidden,
NotFound,
Conflict
}

View File

@@ -0,0 +1,13 @@
namespace HopFrame.Api.Models;
public struct SingleValueResult<T>(T value) {
public T Value { get; set; } = value;
public static implicit operator T(SingleValueResult<T> v) {
return v.Value;
}
public static implicit operator SingleValueResult<T>(T v) {
return new SingleValueResult<T>(v);
}
}

View File

@@ -0,0 +1,6 @@
namespace HopFrame.Api.Models;
public struct UserLogin {
public string Email { get; set; }
public string Password { get; set; }
}

View File

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

View File

@@ -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<UserEntry> Users { get; set; }
public virtual DbSet<PermissionEntry> Permissions { get; set; }
public virtual DbSet<TokenEntry> Tokens { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) {
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<UserEntry>();
modelBuilder.Entity<PermissionEntry>();
}
}

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7" />
</ItemGroup>
</Project>

View File

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

View File

@@ -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;
/// <summary>
/// Defines the Type of the stored Token
/// 0: Refresh token
/// 1: Access token
/// </summary>
[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; }
}

View File

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

View File

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

View File

@@ -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<string> Permissions { get; set; }
}

View File

@@ -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<TDbContext> : AuthenticationHandler<AuthenticationSchemeOptions> 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<AuthenticationSchemeOptions> options, ILoggerFactory logger,
UrlEncoder encoder, ISystemClock clock, TDbContext context) : base(options, logger, encoder, clock) {
_context = context;
}
protected override async Task<AuthenticateResult> 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<Claim> {
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));
}
}

View File

@@ -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<TDbContext>(this IServiceCollection service) where TDbContext : HopDbContextBase {
return service.AddAuthentication(HopFrameAuthentication<TDbContext>.SchemeName).AddScheme<AuthenticationSchemeOptions, HopFrameAuthentication<TDbContext>>(HopFrameAuthentication<TDbContext>.SchemeName, _ => {});
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,9 @@
using HopFrame.Database.Models;
namespace HopFrame.Security.Claims;
public interface ITokenContextBase {
bool IsAuthenticated { get; }
User User { get; }
Guid AccessToken { get; }
}

View File

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

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
<RootNamespace>HopFrame.Security</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.7" />
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\HopFrame.Database\HopFrame.Database.csproj" />
</ItemGroup>
</Project>

39
HopFrame.sln Normal file
View File

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

View File

@@ -0,0 +1,4 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/Environment/AssemblyExplorer/XmlDocument/@EntryValue">&lt;AssemblyExplorer&gt;&#xD;
&lt;Assembly Path="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\7.0.16\ref\net7.0\System.ComponentModel.Annotations.dll" /&gt;&#xD;
&lt;/AssemblyExplorer&gt;</s:String></wpf:ResourceDictionary>

7
global.json Normal file
View File

@@ -0,0 +1,7 @@
{
"sdk": {
"version": "8.0.0",
"rollForward": "latestMinor",
"allowPrerelease": false
}
}