Initial commit
This commit is contained in:
25
Backend/.dockerignore
Normal file
25
Backend/.dockerignore
Normal file
@@ -0,0 +1,25 @@
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/.idea
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/azds.yaml
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
||||
2
Backend/.gitignore
vendored
Normal file
2
Backend/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
obj
|
||||
bin
|
||||
14
Backend/Backend.csproj
Normal file
14
Backend/Backend.csproj
Normal file
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MySql.EntityFrameworkCore" Version="6.0.4" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
20
Backend/Controllers/StartupController.cs
Normal file
20
Backend/Controllers/StartupController.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Backend.Controllers;
|
||||
|
||||
[ApiController]
|
||||
public class TestController : ControllerBase {
|
||||
|
||||
private DatabaseContext _context;
|
||||
|
||||
public TestController(DatabaseContext context) {
|
||||
_context = context;
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
public IActionResult InitializeDb() {
|
||||
_context.ExecuteTableCreation();
|
||||
return Ok("OK");
|
||||
}
|
||||
|
||||
}
|
||||
98
Backend/Controllers/UserController.cs
Normal file
98
Backend/Controllers/UserController.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using Backend.Entitys;
|
||||
using Backend.Logic;
|
||||
using Backend.LogicResults;
|
||||
using Backend.Security;
|
||||
using Backend.Security.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Backend.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("users")]
|
||||
public class UserController : ControllerBase {
|
||||
|
||||
private readonly UserLogic _logic;
|
||||
private readonly ITokenContext _context;
|
||||
|
||||
public UserController(UserLogic logic, ITokenContext context) {
|
||||
_logic = logic;
|
||||
_context = context;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Authorized(Permissions.ShowUsers)]
|
||||
public ActionResult<IEnumerable<User>> GetUsers() {
|
||||
return this.FromLogicResult(_logic.GetUsers());
|
||||
}
|
||||
|
||||
[HttpPut("login")]
|
||||
public ActionResult<AccessToken> Login([FromBody] UserLogin login) {
|
||||
return this.FromLogicResult(_logic.Login(login));
|
||||
}
|
||||
|
||||
[HttpPost("register")]
|
||||
public ActionResult<AccessToken> Register([FromBody] UserEditor editor) {
|
||||
return this.FromLogicResult(_logic.Register(editor));
|
||||
}
|
||||
|
||||
[HttpGet("token")]
|
||||
[Authorized]
|
||||
public ActionResult<AccessToken> GetToken() {
|
||||
return this.FromLogicResult(_logic.GenerateToken(_logic.GetCurrentUserRefreshToken()));
|
||||
}
|
||||
|
||||
[HttpGet("{userId}")]
|
||||
[Authorized(Permissions.ShowUsers)]
|
||||
public ActionResult<User> GetUser(Guid userId) {
|
||||
return this.FromLogicResult(_logic.GetUser(userId));
|
||||
}
|
||||
|
||||
[HttpGet("self")]
|
||||
[Authorized]
|
||||
public ActionResult<User> GetOwnUser() {
|
||||
return this.FromLogicResult(_logic.GetUser(_context.UserId));
|
||||
}
|
||||
|
||||
[HttpPut("{userId}")]
|
||||
[Authorized(Permissions.EditUsers)]
|
||||
public ActionResult EditUser(Guid userId, [FromBody] UserEditor editor) {
|
||||
return this.FromLogicResult(_logic.EditUser(userId, editor));
|
||||
}
|
||||
|
||||
[HttpDelete("{userId}")]
|
||||
[Authorized(Permissions.DeleteUsers)]
|
||||
public ActionResult DeleteUser(Guid userId) {
|
||||
return this.FromLogicResult(_logic.DeleteUser(userId));
|
||||
}
|
||||
|
||||
[HttpGet("{userId}/permissions")]
|
||||
[Authorized(Permissions.ShowUserPermissions)]
|
||||
public ActionResult<IEnumerable<string>> GetUserPermissions(Guid userId) {
|
||||
return this.FromLogicResult(_logic.GetPermissions(userId));
|
||||
}
|
||||
|
||||
[HttpGet("{userId}/permissions/raw")]
|
||||
[Authorized(Permissions.ShowUserPermissions)]
|
||||
public ActionResult<IEnumerable<string>> GetUserPermissionsRaw(Guid userId) {
|
||||
return this.FromLogicResult(_logic.GetPermissionsRaw(userId));
|
||||
}
|
||||
|
||||
[HttpPost("{userId}/permissions")]
|
||||
[Authorized(Permissions.EditUserPermissions, Permissions.EditOwnPermissions)]
|
||||
public ActionResult AddUserPermissions(Guid userId, [FromBody] string[] permissions) {
|
||||
return this.FromLogicResult(_logic.AddPermissions(userId, permissions));
|
||||
}
|
||||
|
||||
[HttpPut("{userId}/permissions")]
|
||||
[Authorized(Permissions.EditUserPermissions, Permissions.EditOwnPermissions)]
|
||||
public ActionResult DeleteUserPermissions(Guid userId, [FromBody] string[] permissions) {
|
||||
return this.FromLogicResult(_logic.DeletePermissions(userId, permissions));
|
||||
}
|
||||
|
||||
[HttpDelete("{userId}/logout")]
|
||||
[Authorized(Permissions.LogoutUsers)]
|
||||
public ActionResult Logout(Guid userId) {
|
||||
return this.FromLogicResult(_logic.Logout(userId));
|
||||
}
|
||||
|
||||
}
|
||||
65
Backend/DatabaseContext.cs
Normal file
65
Backend/DatabaseContext.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using Backend.Entitys;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Backend;
|
||||
|
||||
public class DatabaseContext : DbContext {
|
||||
private string _connectionString;
|
||||
|
||||
public DbSet<User> Users { get; set; }
|
||||
public DbSet<RefreshToken> RefreshTokens { get; set; }
|
||||
public DbSet<AccessToken> AccessTokens { get; set; }
|
||||
public DbSet<Permission> Permissions { get; set; }
|
||||
|
||||
public DatabaseContext(IConfiguration configuration) {
|
||||
_connectionString = configuration.GetSection("MySQL").Get<string>();
|
||||
}
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
|
||||
if (string.IsNullOrEmpty(_connectionString))
|
||||
throw new ArgumentException("MySQL Connection String was not defined correctly in the Configuration!");
|
||||
|
||||
optionsBuilder.UseMySQL(_connectionString);
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder) {
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.Entity<User>(entry => {
|
||||
entry.HasKey(e => e.Id);
|
||||
entry.Property(e => e.FirstName);
|
||||
entry.Property(e => e.LastName);
|
||||
entry.Property(e => e.Email);
|
||||
entry.Property(e => e.Username);
|
||||
entry.Property(e => e.Password);
|
||||
entry.Property(e => e.Created);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<RefreshToken>(entry => {
|
||||
entry.HasKey(e => e.Id);
|
||||
entry.Property(e => e.UserId);
|
||||
entry.Property(e => e.ExpirationDate);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<AccessToken>(entry => {
|
||||
entry.HasKey(e => e.Id);
|
||||
entry.Property(e => e.RefreshTokenId);
|
||||
entry.Property(e => e.ExpirationDate);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<Permission>(entry => {
|
||||
entry.HasKey(e => e.Id);
|
||||
entry.Property(e => e.Id).ValueGeneratedOnAdd();
|
||||
|
||||
entry.Property(e => e.UserId);
|
||||
entry.Property(e => e.PermissionKey);
|
||||
});
|
||||
}
|
||||
|
||||
public void ExecuteTableCreation() {
|
||||
Database.ExecuteSqlRaw("CREATE TABLE IF NOT EXISTS Users (Id VARCHAR(50) PRIMARY KEY, FirstName VARCHAR(255), LastName VARCHAR(255), Email VARCHAR(255), Username VARCHAR(255), Password VARCHAR(255), Created TIMESTAMP)");
|
||||
Database.ExecuteSqlRaw("CREATE TABLE IF NOT EXISTS RefreshTokens (Id VARCHAR(50) PRIMARY KEY, UserId VARCHAR(50), ExpirationDate TIMESTAMP)");
|
||||
Database.ExecuteSqlRaw("CREATE TABLE IF NOT EXISTS AccessTokens (Id VARCHAR(50) PRIMARY KEY, RefreshTokenId VARCHAR(50), ExpirationDate TIMESTAMP)");
|
||||
Database.ExecuteSqlRaw("CREATE TABLE IF NOT EXISTS Permissions (Id INT PRIMARY KEY AUTO_INCREMENT, UserId VARCHAR(50), PermissionName VARCHAR(100))");
|
||||
}
|
||||
}
|
||||
20
Backend/Dockerfile
Normal file
20
Backend/Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 80
|
||||
EXPOSE 443
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
|
||||
WORKDIR /src
|
||||
COPY ["Backend.csproj", ""]
|
||||
RUN dotnet restore "Backend.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src"
|
||||
RUN dotnet build "Backend.csproj" -c Release -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
RUN dotnet publish "Backend.csproj" -c Release -o /app/publish
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "Backend.dll"]
|
||||
16
Backend/Entitys/Permission.cs
Normal file
16
Backend/Entitys/Permission.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
|
||||
namespace Backend.Entitys;
|
||||
|
||||
public class Permission {
|
||||
public int Id { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
public string PermissionKey { get; set; }
|
||||
}
|
||||
|
||||
public class PermissionGroup {
|
||||
public string Permission { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string[] Permissions { get; set; }
|
||||
public string[] Inherits { get; set; }
|
||||
}
|
||||
20
Backend/Entitys/Tokens.cs
Normal file
20
Backend/Entitys/Tokens.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
|
||||
namespace Backend.Entitys;
|
||||
|
||||
public class Tokens {
|
||||
public AccessToken AccessToken { get; set; }
|
||||
public RefreshToken RefreshToken { get; set; }
|
||||
}
|
||||
|
||||
public class RefreshToken {
|
||||
public Guid Id { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
public DateTime ExpirationDate { get; set; }
|
||||
}
|
||||
|
||||
public class AccessToken {
|
||||
public Guid Id { get; set; }
|
||||
public Guid RefreshTokenId { get; set; }
|
||||
public DateTime ExpirationDate { get; set; }
|
||||
}
|
||||
21
Backend/Entitys/User.cs
Normal file
21
Backend/Entitys/User.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
|
||||
namespace Backend.Entitys;
|
||||
|
||||
public class User : UserEditor {
|
||||
public Guid Id { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
}
|
||||
|
||||
public class UserLogin {
|
||||
public string UsernameOrEmail { get; set; }
|
||||
public string Password { get; set; }
|
||||
}
|
||||
|
||||
public class UserEditor {
|
||||
public string FirstName { get; set; }
|
||||
public string LastName { get; set; }
|
||||
public string Email { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
}
|
||||
213
Backend/Logic/UserLogic.cs
Normal file
213
Backend/Logic/UserLogic.cs
Normal file
@@ -0,0 +1,213 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Backend.Entitys;
|
||||
using Backend.LogicResults;
|
||||
using Backend.Options;
|
||||
using Backend.Repositorys;
|
||||
using Backend.Security;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Backend.Logic;
|
||||
|
||||
public class UserLogic {
|
||||
private UserRepository _users;
|
||||
private TokenRepository _tokens;
|
||||
private GroupRepository _groups;
|
||||
private UserMessageOptions _messages;
|
||||
private IHttpContextAccessor _contextAccessor;
|
||||
private ITokenContext _context;
|
||||
|
||||
public UserLogic(
|
||||
UserRepository users,
|
||||
TokenRepository tokens,
|
||||
GroupRepository groups,
|
||||
IOptions<UserMessageOptions> messages,
|
||||
IHttpContextAccessor contextAccessor,
|
||||
ITokenContext context) {
|
||||
_users = users;
|
||||
_tokens = tokens;
|
||||
_groups = groups;
|
||||
_messages = messages.Value;
|
||||
_contextAccessor = contextAccessor;
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public ILogicResult<IEnumerable<User>> GetUsers() {
|
||||
var users = _users.GetUsers()
|
||||
.Select(user => new User {
|
||||
Id = user.Id,
|
||||
Created = user.Created,
|
||||
Email = user.Email,
|
||||
FirstName = user.FirstName,
|
||||
LastName = user.LastName,
|
||||
Username = user.Username
|
||||
});
|
||||
|
||||
return LogicResult<IEnumerable<User>>.Ok(users);
|
||||
}
|
||||
|
||||
public ILogicResult<User> GetUser(Guid userId) {
|
||||
var user = _users.GetUser(userId);
|
||||
if (user is null) return LogicResult<User>.NotFound(_messages.NotFound);
|
||||
|
||||
return LogicResult<User>.Ok(new User {
|
||||
Id = user.Id,
|
||||
Created = user.Created,
|
||||
Email = user.Email,
|
||||
FirstName = user.FirstName,
|
||||
LastName = user.LastName,
|
||||
Username = user.Username
|
||||
});
|
||||
}
|
||||
|
||||
public ILogicResult EditUser(Guid userId, UserEditor editor) {
|
||||
if (!ValidateEdit(editor)) return LogicResult.BadRequest(_messages.InvalidEditData);
|
||||
if (_users.GetUser(userId) is null) return LogicResult.NotFound(_messages.NotFound);
|
||||
|
||||
_users.EditUser(userId, editor);
|
||||
return LogicResult.Ok();
|
||||
}
|
||||
|
||||
public ILogicResult DeleteUser(Guid userId) {
|
||||
if (_users.GetUser(userId) is null) return LogicResult.NotFound(_messages.NotFound);
|
||||
|
||||
_tokens.DeleteUserTokens(userId);
|
||||
_users.DeleteUser(userId);
|
||||
|
||||
return LogicResult.Ok();
|
||||
}
|
||||
|
||||
public ILogicResult<IEnumerable<string>> GetPermissions(Guid userId) {
|
||||
if (_users.GetUser(userId) is null) return LogicResult<IEnumerable<string>>.NotFound(_messages.NotFound);
|
||||
|
||||
return LogicResult<IEnumerable<string>>.Ok(_groups.GetUserPermissions(userId).Select(perm => perm.PermissionKey));
|
||||
}
|
||||
|
||||
public ILogicResult<IEnumerable<string>> GetPermissionsRaw(Guid userId) {
|
||||
if (_users.GetUser(userId) is null) return LogicResult<IEnumerable<string>>.NotFound(_messages.NotFound);
|
||||
|
||||
return LogicResult<IEnumerable<string>>.Ok(_groups.GetUserPermissionsRaw(userId).Select(perm => perm.PermissionKey));
|
||||
}
|
||||
|
||||
public ILogicResult AddPermissions(Guid userId, params string[] permissions) {
|
||||
if (_users.GetUser(userId) is null) return LogicResult.NotFound(_messages.NotFound);
|
||||
|
||||
_groups.AddPermissions(userId, permissions);
|
||||
return LogicResult.Ok();
|
||||
}
|
||||
|
||||
public ILogicResult DeletePermissions(Guid userId, params string[] permissions) {
|
||||
if (_users.GetUser(userId) is null) return LogicResult.NotFound(_messages.NotFound);
|
||||
|
||||
_groups.DeletePermissions(userId, permissions);
|
||||
return LogicResult.Ok();
|
||||
}
|
||||
|
||||
public ILogicResult<AccessToken> Login(UserLogin login) {
|
||||
var user = _users.GetUsers().SingleOrDefault(user =>
|
||||
user.Username == login.UsernameOrEmail || user.Email == login.UsernameOrEmail);
|
||||
|
||||
if (user is null) return LogicResult<AccessToken>.NotFound(_messages.NotFound);
|
||||
if (user.Password != TokenRepository.Hash128(login.Password, user.Email))
|
||||
return LogicResult<AccessToken>.BadRequest(_messages.WrongPassword);
|
||||
|
||||
_tokens.DeleteUserTokens(user.Id);
|
||||
var refreshToken = _tokens.CreateRefreshToken(user.Id);
|
||||
var accessToken = _tokens.CreateAccessToken(refreshToken.Id);
|
||||
SetRefreshToken(refreshToken);
|
||||
|
||||
return LogicResult<AccessToken>.Ok(accessToken);
|
||||
}
|
||||
|
||||
public ILogicResult<AccessToken> Register(UserEditor editor) {
|
||||
var users = _users.GetUsers();
|
||||
if (users.Any(user => user.Email == editor.Email || user.Username == editor.Username))
|
||||
return LogicResult<AccessToken>.Conflict(_messages.UsernameOrEmailExist);
|
||||
|
||||
if (!ValidateUserdata(editor))
|
||||
return LogicResult<AccessToken>.BadRequest(_messages.InvalidRegisterData);
|
||||
|
||||
var user = _users.CreateUser(editor);
|
||||
var refreshToken = _tokens.CreateRefreshToken(user.Id);
|
||||
var accessToken = _tokens.CreateAccessToken(refreshToken.Id);
|
||||
SetRefreshToken(refreshToken);
|
||||
|
||||
return LogicResult<AccessToken>.Ok(accessToken);
|
||||
}
|
||||
|
||||
public ILogicResult Logout(Guid userId) {
|
||||
if (_users.GetUser(userId) is null) return LogicResult.NotFound(_messages.NotFound);
|
||||
_tokens.DeleteUserTokens(userId);
|
||||
|
||||
if (_context.UserId == userId) DeleteRefreshToken();
|
||||
return LogicResult.Ok();
|
||||
}
|
||||
|
||||
public ILogicResult<AccessToken> GenerateToken(Guid refreshTokenId) {
|
||||
if (!_tokens.ValidateRefreshToken(refreshTokenId)) return LogicResult<AccessToken>.Conflict(_messages.InvalidRefreshToken);
|
||||
var token = _tokens.GetAccessTokens(refreshTokenId).ToArray().FirstOrDefault(token => _tokens.ValidateAccessToken(token.Id));
|
||||
if (token is not null) return LogicResult<AccessToken>.Ok(token);
|
||||
return LogicResult<AccessToken>.Ok(_tokens.CreateAccessToken(refreshTokenId));
|
||||
}
|
||||
|
||||
public Guid GetCurrentUserRefreshToken() {
|
||||
var token = _contextAccessor.HttpContext?.Request.Cookies["refresh_token"];
|
||||
if (token == null) return Guid.Empty;
|
||||
return Guid.Parse(token);
|
||||
}
|
||||
|
||||
private bool ValidateUserdata(UserEditor editor) {
|
||||
if (string.IsNullOrEmpty(editor.FirstName)) return false;
|
||||
if (string.IsNullOrEmpty(editor.LastName)) return false;
|
||||
if (string.IsNullOrEmpty(editor.Email)) return false;
|
||||
if (string.IsNullOrEmpty(editor.Username)) return false;
|
||||
if (string.IsNullOrEmpty(editor.Password)) return false;
|
||||
|
||||
if (editor.FirstName.Length > 255) return false;
|
||||
if (editor.LastName.Length > 255) return false;
|
||||
if (editor.Email.Length > 255) return false;
|
||||
if (editor.Username.Length > 255) return false;
|
||||
if (editor.Password.Length > 255) return false;
|
||||
|
||||
if (!editor.Email.Contains('@') || !editor.Email.Contains('.')) return false;
|
||||
if (editor.Username.Contains('@')) return false;
|
||||
if (editor.Password.Length < 8) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
private bool ValidateEdit(UserEditor editor) {
|
||||
if (editor.FirstName?.Length > 255) return false;
|
||||
if (editor.LastName?.Length > 255) return false;
|
||||
if (editor.Email?.Length > 255) return false;
|
||||
if (editor.Username?.Length > 255) return false;
|
||||
if (editor.Password?.Length > 255) return false;
|
||||
|
||||
if (!string.IsNullOrEmpty(editor.Email)) {
|
||||
if (!editor.Email.Contains('@') || !editor.Email.Contains('.')) return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(editor.Username)) {
|
||||
if (editor.Username.Contains('@')) return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(editor.Password)) {
|
||||
if (editor.Password.Length < 8) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private void DeleteRefreshToken() {
|
||||
_contextAccessor.HttpContext?.Response.Cookies.Delete("refresh_token");
|
||||
}
|
||||
private void SetRefreshToken(RefreshToken token) {
|
||||
_contextAccessor.HttpContext?.Response.Cookies.Append("refresh_token", token.Id.ToString(), new CookieOptions {
|
||||
MaxAge = token.ExpirationDate - DateTime.Now,
|
||||
HttpOnly = true,
|
||||
Secure = true
|
||||
});
|
||||
}
|
||||
}
|
||||
51
Backend/LogicResults/ControllerBaseExtention.cs
Normal file
51
Backend/LogicResults/ControllerBaseExtention.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Backend.LogicResults {
|
||||
public static class ControllerBaseExtention {
|
||||
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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
Backend/LogicResults/ILogicResult.cs
Normal file
19
Backend/LogicResults/ILogicResult.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace Backend.LogicResults {
|
||||
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; }
|
||||
}
|
||||
}
|
||||
170
Backend/LogicResults/LogicResult.cs
Normal file
170
Backend/LogicResults/LogicResult.cs
Normal file
@@ -0,0 +1,170 @@
|
||||
namespace Backend.LogicResults {
|
||||
internal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
internal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
9
Backend/LogicResults/LogicResultState.cs
Normal file
9
Backend/LogicResults/LogicResultState.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Backend.LogicResults {
|
||||
public enum LogicResultState {
|
||||
Ok,
|
||||
BadRequest,
|
||||
Forbidden,
|
||||
NotFound,
|
||||
Conflict
|
||||
}
|
||||
}
|
||||
12
Backend/Options/UserMessageOptions.cs
Normal file
12
Backend/Options/UserMessageOptions.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Backend.Options;
|
||||
|
||||
public class UserMessageOptions : OptionsFromConfiguration {
|
||||
public override string Position => "Messages:Users";
|
||||
|
||||
public string NotFound { get; set; }
|
||||
public string InvalidEditData { get; set; }
|
||||
public string InvalidRegisterData { get; set; }
|
||||
public string WrongPassword { get; set; }
|
||||
public string UsernameOrEmailExist { get; set; }
|
||||
public string InvalidRefreshToken { get; set; }
|
||||
}
|
||||
7
Backend/Options/UserOptions.cs
Normal file
7
Backend/Options/UserOptions.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Backend.Options;
|
||||
|
||||
public class UserOptions : OptionsFromConfiguration {
|
||||
public override string Position => "Users";
|
||||
|
||||
public string[] DefaultPermissions { get; set; }
|
||||
}
|
||||
79
Backend/Program.cs
Normal file
79
Backend/Program.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using Backend;
|
||||
using Backend.Logic;
|
||||
using Backend.Options;
|
||||
using Backend.Repositorys;
|
||||
using Backend.Security;
|
||||
using Backend.Security.Authentication;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.OpenApi.Models;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddDbContext<DatabaseContext>();
|
||||
builder.Services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||
builder.Services.AddScoped<ITokenContext, TokenContext>();
|
||||
|
||||
builder.Services.AddScoped<TokenRepository>();
|
||||
builder.Services.AddScoped<GroupRepository>();
|
||||
builder.Services.AddScoped<UserRepository>();
|
||||
|
||||
builder.Services.AddScoped<UserLogic>();
|
||||
|
||||
builder.Services.AddOptionsFromConfiguration<JwtTokenAuthenticationOptions>(builder.Configuration);
|
||||
builder.Services.AddOptionsFromConfiguration<UserOptions>(builder.Configuration);
|
||||
builder.Services.AddOptionsFromConfiguration<UserMessageOptions>(builder.Configuration);
|
||||
|
||||
builder.Services.AddCors();
|
||||
builder.Services.AddAuthentication(JwtTokenAuthentication.Scheme).AddJwtTokenAuthentication(builder.Configuration);
|
||||
builder.Services.AddControllers();
|
||||
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
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();
|
||||
|
||||
GroupRepository.CompileGroups(app.Configuration);
|
||||
|
||||
if (app.Environment.IsDevelopment()) {
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseCors(
|
||||
options => options
|
||||
.WithOrigins(app.Configuration.GetSection("Origins").Get<string[]>())
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader()
|
||||
.AllowCredentials()
|
||||
);
|
||||
|
||||
app.UseRouting();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.UseWebSockets();
|
||||
|
||||
app.MapControllers();
|
||||
app.Run();
|
||||
23
Backend/Properties/launchSettings.json
Normal file
23
Backend/Properties/launchSettings.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:42992",
|
||||
"sslPort": 44301
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"Backend": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "http://localhost:5142",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
94
Backend/Repositorys/GroupRepository.cs
Normal file
94
Backend/Repositorys/GroupRepository.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using Backend.Entitys;
|
||||
|
||||
namespace Backend.Repositorys;
|
||||
|
||||
public class GroupRepository {
|
||||
public static PermissionGroup[] Groups;
|
||||
|
||||
public static void CompileGroups(IConfiguration configuration) {
|
||||
var groupsSections = configuration.GetSection("Groups").GetChildren();
|
||||
List<PermissionGroup> groups = new List<PermissionGroup>();
|
||||
foreach (var section in groupsSections) {
|
||||
PermissionGroup group = new PermissionGroup();
|
||||
group.Name = section.GetValue<string>("Name");
|
||||
group.Permission = section.GetValue<string>("Permission");
|
||||
group.Permissions = section.GetSection("Permissions").Get<string[]>();
|
||||
group.Inherits = section.GetSection("Inherits").Get<string[]>();
|
||||
groups.Add(group);
|
||||
}
|
||||
Groups = groups.ToArray();
|
||||
}
|
||||
|
||||
private readonly DatabaseContext _context;
|
||||
private readonly PermissionGroup[] _groups;
|
||||
|
||||
public GroupRepository(DatabaseContext context) {
|
||||
_context = context;
|
||||
_groups = Groups;
|
||||
}
|
||||
|
||||
public PermissionGroup GetPermissionGroup(string name) {
|
||||
return _groups.SingleOrDefault(group => group.Permission.Equals(name));
|
||||
}
|
||||
|
||||
public PermissionGroup[] GetGroupsFromUser(Guid userId) {
|
||||
Permission[] permissions = GetUserPermissionsRaw(userId).ToArray();
|
||||
return ExtractGroups(permissions);
|
||||
}
|
||||
|
||||
public PermissionGroup[] ExtractGroups(Permission[] permissions) {
|
||||
List<PermissionGroup> permissionGroups = new List<PermissionGroup>();
|
||||
foreach (var permission in permissions) {
|
||||
if (permission.PermissionKey.StartsWith("group.")) {
|
||||
foreach (var permissionGroup in _groups) {
|
||||
if (permission.PermissionKey.Equals(permissionGroup.Permission)) {
|
||||
permissionGroups.Add(permissionGroup);
|
||||
|
||||
if (permissionGroup.Inherits is not null) {
|
||||
foreach (var inherit in permissionGroup.Inherits) {
|
||||
permissionGroups.Add(GetPermissionGroup(inherit));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return permissionGroups.ToArray();
|
||||
}
|
||||
|
||||
public IEnumerable<Permission> GetUserPermissions(Guid userId) {
|
||||
List<Permission> permissions = GetUserPermissionsRaw(userId).ToList();
|
||||
|
||||
PermissionGroup[] groups = ExtractGroups(permissions.ToArray());
|
||||
foreach (var group in groups) {
|
||||
if (group.Permissions is null) continue;
|
||||
permissions.AddRange(group.Permissions
|
||||
.Select(perm => new Permission { Id = -1, UserId = userId, PermissionKey = perm }));
|
||||
}
|
||||
|
||||
return permissions;
|
||||
}
|
||||
|
||||
public IEnumerable<Permission> GetUserPermissionsRaw(Guid userId) {
|
||||
return _context.Permissions.Where(permission => permission.UserId == userId);
|
||||
}
|
||||
|
||||
public void AddPermissions(Guid userId, params string[] permissions) {
|
||||
foreach (var permission in permissions) {
|
||||
_context.Permissions.Add(new Permission
|
||||
{ PermissionKey = permission, UserId = userId });
|
||||
}
|
||||
|
||||
_context.SaveChanges();
|
||||
}
|
||||
|
||||
public void DeletePermissions(Guid userId, params string[] permissions) {
|
||||
foreach (var permission in permissions) {
|
||||
_context.Permissions.RemoveRange(_context.Permissions.Where(perm =>
|
||||
perm.UserId == userId && perm.PermissionKey == permission));
|
||||
}
|
||||
|
||||
_context.SaveChanges();
|
||||
}
|
||||
}
|
||||
92
Backend/Repositorys/TokenRepository.cs
Normal file
92
Backend/Repositorys/TokenRepository.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
using System.Text;
|
||||
using Backend.Entitys;
|
||||
using Backend.Security.Authentication;
|
||||
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Backend.Repositorys;
|
||||
|
||||
public class TokenRepository {
|
||||
private readonly JwtTokenAuthenticationOptions _options;
|
||||
private readonly DatabaseContext _context;
|
||||
|
||||
public TokenRepository(IOptions<JwtTokenAuthenticationOptions> options, DatabaseContext context) {
|
||||
_options = options.Value;
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public RefreshToken GetRefreshToken(Guid refreshTokenId) {
|
||||
if (string.IsNullOrEmpty(refreshTokenId.ToString())) return null;
|
||||
return _context.RefreshTokens.SingleOrDefault(token => token.Id == refreshTokenId);
|
||||
}
|
||||
|
||||
public AccessToken GetAccessToken(Guid accessTokenId) {
|
||||
if (string.IsNullOrEmpty(accessTokenId.ToString())) return null;
|
||||
return _context.AccessTokens.SingleOrDefault(token => token.Id == accessTokenId);
|
||||
}
|
||||
|
||||
public IEnumerable<AccessToken> GetAccessTokens(Guid refreshTokenId) {
|
||||
if (string.IsNullOrEmpty(refreshTokenId.ToString())) return ArraySegment<AccessToken>.Empty;
|
||||
return _context.AccessTokens.Where(token => token.RefreshTokenId == refreshTokenId);
|
||||
}
|
||||
|
||||
public bool ValidateAccessToken(Guid accessTokenId) {
|
||||
AccessToken token = GetAccessToken(accessTokenId);
|
||||
if (token == null) return false;
|
||||
TimeSpan span = token.ExpirationDate - DateTime.Now;
|
||||
return span.TotalMilliseconds > 0;
|
||||
}
|
||||
|
||||
public bool ValidateRefreshToken(Guid refreshTokenId) {
|
||||
RefreshToken token = GetRefreshToken(refreshTokenId);
|
||||
if (token == null) return false;
|
||||
TimeSpan span = token.ExpirationDate - DateTime.Now;
|
||||
return span.TotalMilliseconds > 0;
|
||||
}
|
||||
|
||||
public RefreshToken CreateRefreshToken(Guid userId) {
|
||||
RefreshToken token = new RefreshToken {
|
||||
UserId = userId, Id = Guid.NewGuid(),
|
||||
ExpirationDate = DateTime.Now.Add(new TimeSpan(int.Parse(_options.RefreshTokenExpirationTimeInHours), 0, 0))
|
||||
};
|
||||
_context.RefreshTokens.Add(token);
|
||||
_context.SaveChanges();
|
||||
return token;
|
||||
}
|
||||
|
||||
public AccessToken CreateAccessToken(Guid refreshTokenId) {
|
||||
AccessToken token = new AccessToken {
|
||||
RefreshTokenId = refreshTokenId, Id = Guid.NewGuid(),
|
||||
ExpirationDate = DateTime.Now
|
||||
.Add(new TimeSpan(0, int.Parse(_options.AccessTokenExpirationTimeInMinutes), 0))
|
||||
};
|
||||
_context.AccessTokens.Add(token);
|
||||
_context.SaveChanges();
|
||||
return token;
|
||||
}
|
||||
|
||||
public void DeleteUserTokens(Guid userId) {
|
||||
List<RefreshToken> refreshTokens = _context.RefreshTokens.Where(token => token.UserId == userId).ToList();
|
||||
refreshTokens.ForEach(token => DeleteRefreshToken(token.Id));
|
||||
_context.SaveChanges();
|
||||
}
|
||||
|
||||
public void DeleteRefreshToken(Guid refreshTokenId) {
|
||||
_context.RefreshTokens.RemoveRange(_context.RefreshTokens.Where(token => token.Id == refreshTokenId));
|
||||
_context.AccessTokens.RemoveRange(_context.AccessTokens.Where(token => token.RefreshTokenId == refreshTokenId));
|
||||
}
|
||||
|
||||
public static string Hash128(string plainText, string salt) {
|
||||
try {
|
||||
string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2(
|
||||
password: plainText,
|
||||
salt: Encoding.Default.GetBytes(salt),
|
||||
prf: KeyDerivationPrf.HMACSHA256,
|
||||
iterationCount: 100000,
|
||||
numBytesRequested: 256 / 8
|
||||
));
|
||||
|
||||
return hashed;
|
||||
} catch (Exception) { return ""; }
|
||||
}
|
||||
}
|
||||
68
Backend/Repositorys/UserRepository.cs
Normal file
68
Backend/Repositorys/UserRepository.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using Backend.Entitys;
|
||||
using Backend.Options;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Backend.Repositorys;
|
||||
|
||||
public class UserRepository {
|
||||
private DatabaseContext _context;
|
||||
private TokenRepository _tokens;
|
||||
private GroupRepository _groups;
|
||||
private UserOptions _options;
|
||||
|
||||
public UserRepository(DatabaseContext context, TokenRepository tokens, GroupRepository groups, IOptions<UserOptions> options) {
|
||||
_context = context;
|
||||
_tokens = tokens;
|
||||
_groups = groups;
|
||||
_options = options.Value;
|
||||
}
|
||||
|
||||
public IEnumerable<User> GetUsers() => _context.Users.OrderBy(user => user.Created);
|
||||
|
||||
public User GetUser(Guid userId) => _context.Users.SingleOrDefault(user => user.Id == userId);
|
||||
|
||||
public User CreateUser(UserEditor editor) {
|
||||
var user = new User {
|
||||
Id = Guid.NewGuid(),
|
||||
Created = DateTime.Now,
|
||||
|
||||
Email = editor.Email,
|
||||
FirstName = editor.FirstName,
|
||||
LastName = editor.LastName,
|
||||
Password = TokenRepository.Hash128(editor.Password, editor.Email),
|
||||
Username = editor.Username
|
||||
};
|
||||
|
||||
_context.Users.Add(user);
|
||||
_context.SaveChanges();
|
||||
_groups.AddPermissions(user.Id, _options.DefaultPermissions);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
public void EditUser(Guid userId, UserEditor editor) {
|
||||
var user = GetUser(userId);
|
||||
|
||||
string SetValue(string orig, string input, string hashed = null) {
|
||||
if (!string.IsNullOrEmpty(input))
|
||||
return !string.IsNullOrEmpty(hashed) ? hashed : input;
|
||||
|
||||
return orig;
|
||||
}
|
||||
|
||||
user.Email = SetValue(user.Email, editor.Email);
|
||||
user.FirstName = SetValue(user.FirstName, editor.FirstName);
|
||||
user.LastName = SetValue(user.LastName, editor.LastName);
|
||||
user.Username = SetValue(user.Username, editor.Username);
|
||||
user.Password = SetValue(user.Password, editor.Password, TokenRepository.Hash128(editor.Password, editor.Email));
|
||||
|
||||
_context.SaveChanges();
|
||||
}
|
||||
|
||||
public void DeleteUser(Guid userId) {
|
||||
_context.Users.Remove(_context.Users.Single(user => user.Id == userId));
|
||||
_context.Permissions.RemoveRange(_context.Permissions.Where(perm => perm.UserId == userId));
|
||||
_tokens.DeleteUserTokens(userId);
|
||||
_context.SaveChanges();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace Backend.Security.Authentication {
|
||||
public static class JwtTokenAuthentication {
|
||||
public const string Scheme = "JwtTokenAuthentication";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Backend.Options;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
|
||||
namespace Backend.Security.Authentication {
|
||||
public static class JwtTokenAuthenticationExtensions {
|
||||
public static AuthenticationBuilder AddJwtTokenAuthentication(this AuthenticationBuilder builder,
|
||||
IConfiguration configuration) {
|
||||
builder.Services.AddOptionsFromConfiguration<JwtTokenAuthenticationOptions>(configuration);
|
||||
|
||||
return builder.AddScheme<JwtTokenAuthenticationHandlerOptions, JwtTokenAuthenticationHandler>(
|
||||
JwtTokenAuthentication.Scheme,
|
||||
_ => { });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Security.Claims;
|
||||
using Backend.Entitys;
|
||||
using Backend.Repositorys;
|
||||
using Backend.Security.Authorization;
|
||||
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
namespace Backend.Security.Authentication {
|
||||
public class JwtTokenAuthenticationHandler : AuthenticationHandler<JwtTokenAuthenticationHandlerOptions> {
|
||||
private readonly TokenRepository _tokens;
|
||||
private readonly GroupRepository _groups;
|
||||
private readonly JwtTokenAuthenticationOptions _options;
|
||||
|
||||
public JwtTokenAuthenticationHandler(
|
||||
IOptionsMonitor<JwtTokenAuthenticationHandlerOptions> options,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder,
|
||||
ISystemClock clock,
|
||||
IOptions<JwtTokenAuthenticationOptions> tokenOptions,
|
||||
TokenRepository tokens,
|
||||
GroupRepository groups)
|
||||
: base(options, logger, encoder, clock) {
|
||||
_options = tokenOptions.Value;
|
||||
_tokens = tokens;
|
||||
_groups = groups;
|
||||
}
|
||||
|
||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync() {
|
||||
if (Request.Headers["Authorization"].Equals(_options.DebugAccessToken))
|
||||
return AuthenticateResult.Success(GetAuthenticationTicket(null, null, "*"));
|
||||
|
||||
var accessToken = GetAccessToken();
|
||||
if (accessToken == null) return AuthenticateResult.Fail("Access Token invalid");
|
||||
var refreshToken = _tokens.GetRefreshToken(accessToken.RefreshTokenId);
|
||||
if (refreshToken == null) return AuthenticateResult.Fail("Refresh Token invalid");
|
||||
if (!_tokens.ValidateRefreshToken(refreshToken.Id)) return AuthenticateResult.Fail("Refresh Token invalid");
|
||||
bool valid = _tokens.ValidateAccessToken(accessToken.Id);
|
||||
return valid
|
||||
? AuthenticateResult.Success(GetAuthenticationTicket(accessToken, refreshToken))
|
||||
: AuthenticateResult.Fail("Access Token invalid");
|
||||
}
|
||||
|
||||
private AuthenticationTicket GetAuthenticationTicket(AccessToken accessToken, RefreshToken refreshToken, params string[] customPerms) {
|
||||
List<Claim> claims = GenerateClaims(accessToken, refreshToken, customPerms);
|
||||
ClaimsPrincipal principal = new ClaimsPrincipal();
|
||||
principal.AddIdentity(new ClaimsIdentity(claims, JwtTokenAuthentication.Scheme));
|
||||
AuthenticationTicket ticket = new AuthenticationTicket(principal, Scheme.Name);
|
||||
return ticket;
|
||||
}
|
||||
|
||||
private List<Claim> GenerateClaims(AccessToken accessToken, RefreshToken refreshToken, params string[] customPerms) {
|
||||
List<Claim> claims = new List<Claim>();
|
||||
if (accessToken is not null && refreshToken is not null) {
|
||||
claims.AddRange(new List<Claim> {
|
||||
new(CustomClaimTypes.AccessTokenId, accessToken.Id.ToString()),
|
||||
new(CustomClaimTypes.RefreshTokenId, refreshToken.Id.ToString()),
|
||||
new(CustomClaimTypes.UserId, refreshToken.UserId.ToString()),
|
||||
});
|
||||
|
||||
string[] permissions = _groups.GetUserPermissions(refreshToken.UserId).Select(perm => perm.PermissionKey).ToArray();
|
||||
claims.AddRange(permissions
|
||||
.Select(permission => new Claim(CustomClaimTypes.Permission, permission)));
|
||||
}
|
||||
|
||||
claims.AddRange(customPerms.Select(perm => new Claim(CustomClaimTypes.Permission, perm)));
|
||||
|
||||
return claims;
|
||||
}
|
||||
|
||||
private AccessToken GetAccessToken() {
|
||||
string key = Request.Headers["Authorization"];
|
||||
if (string.IsNullOrEmpty(key)) {
|
||||
key = Request.Query["token"];
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(key))
|
||||
return null;
|
||||
|
||||
AccessToken token = _tokens.GetAccessToken(Guid.Parse(key));
|
||||
return token;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
@@ -0,0 +1,8 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
|
||||
namespace Backend.Security.Authentication {
|
||||
public class JwtTokenAuthenticationHandlerOptions : AuthenticationSchemeOptions {
|
||||
// Options for the authentication handler.
|
||||
// Currently: None
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using Backend.Options;
|
||||
|
||||
namespace Backend.Security.Authentication {
|
||||
public class JwtTokenAuthenticationOptions : OptionsFromConfiguration {
|
||||
public override string Position => "Authentication";
|
||||
|
||||
public string RefreshTokenExpirationTimeInHours { get; set; }
|
||||
public string AccessTokenExpirationTimeInMinutes { get; set; }
|
||||
public string DebugAccessToken { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace Backend.Options {
|
||||
public abstract class OptionsFromConfiguration {
|
||||
public abstract string Position { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Backend.Options {
|
||||
public static class OptionsFromConfigurationExtensions {
|
||||
public static T AddOptionsFromConfiguration<T>(this IServiceCollection services, IConfiguration configuration)
|
||||
where T : OptionsFromConfiguration {
|
||||
T optionsInstance = (T)Activator.CreateInstance(typeof(T));
|
||||
if (optionsInstance == null) return null;
|
||||
string position = optionsInstance.Position;
|
||||
services.Configure((Action<T>)(options => {
|
||||
IConfigurationSection section = configuration.GetSection(position);
|
||||
if (section != null) {
|
||||
section.Bind(options);
|
||||
}
|
||||
}));
|
||||
return optionsInstance;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Backend.Security.Authorization {
|
||||
public sealed class AuthorizedAttribute : TypeFilterAttribute {
|
||||
public AuthorizedAttribute(params string[] permission) : base(typeof(AuthorizedFilter)) {
|
||||
Arguments = new object[] { permission };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace Backend.Security.Authorization {
|
||||
public class AuthorizedFilter : IAuthorizationFilter {
|
||||
private readonly string[] _permissions;
|
||||
|
||||
public AuthorizedFilter(params string[] permissions) {
|
||||
_permissions = permissions;
|
||||
}
|
||||
|
||||
public void OnAuthorization(AuthorizationFilterContext context) {
|
||||
if (EndpointHasAllowAnonymousFilter(context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsAuthenticated(context)) {
|
||||
context.Result = new UnauthorizedResult();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ContainsRequiredRole(context)) {
|
||||
context.Result = new ForbidResult();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool EndpointHasAllowAnonymousFilter(AuthorizationFilterContext context) {
|
||||
return context.Filters.Any(item => item is IAllowAnonymousFilter);
|
||||
}
|
||||
|
||||
private bool IsAuthenticated(AuthorizationFilterContext context) {
|
||||
return context.HttpContext.User.Identity.IsAuthenticated;
|
||||
}
|
||||
|
||||
private bool ContainsRequiredRole(AuthorizationFilterContext context) {
|
||||
if (context.HttpContext.User.HasClaim(CustomClaimTypes.Permission, "*"))
|
||||
return true;
|
||||
|
||||
var perms = context.HttpContext.User.Claims
|
||||
.Where(c => c.Type == CustomClaimTypes.Permission)
|
||||
.Select(c => c.Value).ToArray();
|
||||
|
||||
if (context.RouteData.Values.ContainsKey("userId")) {
|
||||
var accessedUser = context.RouteData.Values["userId"] as string;
|
||||
|
||||
if (accessedUser == context.HttpContext.User.GetUserId()) {
|
||||
var selfPerms = _permissions.Where(p => p.StartsWith("self.")).ToArray();
|
||||
|
||||
if (!selfPerms.Any())
|
||||
return true;
|
||||
|
||||
if (CheckPermission(selfPerms, perms))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (CheckPermission(_permissions, perms.Where(p => !p.StartsWith("self.")).ToArray()))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
|
||||
bool CheckPermission(string[] permissions, string[] permission) {
|
||||
if (permissions.Length == 0)
|
||||
return true;
|
||||
|
||||
if (permission.Contains("*"))
|
||||
return true;
|
||||
|
||||
foreach (var perm in permissions) {
|
||||
if (permission.Contains(perm))
|
||||
return true;
|
||||
|
||||
string[] splice = perm.Split(".");
|
||||
string cache = "";
|
||||
foreach (var s in splice) {
|
||||
cache += s + ".";
|
||||
if (permission.Contains(cache + "*"))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Backend.Security.Authorization {
|
||||
public static class ClaimsPrincipalExtensions {
|
||||
public static string GetAccessTokenId(this ClaimsPrincipal principal) =>
|
||||
principal.FindFirstValue(CustomClaimTypes.AccessTokenId);
|
||||
|
||||
public static string GetRefreshTokenId(this ClaimsPrincipal principal) =>
|
||||
principal.FindFirstValue(CustomClaimTypes.RefreshTokenId);
|
||||
|
||||
public static string GetUserId(this ClaimsPrincipal principal) =>
|
||||
principal.FindFirstValue(CustomClaimTypes.UserId);
|
||||
|
||||
public static string[] GetPermissions(this ClaimsPrincipal principal) => principal.Claims
|
||||
.Where(claim => claim.Type.Equals(CustomClaimTypes.Permission))
|
||||
.Select(claim => claim.Value)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Backend.Security.Authorization {
|
||||
public static class CustomClaimTypes {
|
||||
public const string AccessTokenId = "WebDesktop.AccessTokenId";
|
||||
public const string RefreshTokenId = "WebDesktop.RefreshTokenId";
|
||||
public const string UserId = "WebDesktop.UserId";
|
||||
public const string Permission = "WebDesktop.Permission";
|
||||
}
|
||||
}
|
||||
9
Backend/Security/ITokenContext.cs
Normal file
9
Backend/Security/ITokenContext.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Backend.Security {
|
||||
public interface ITokenContext {
|
||||
bool IsAuthenticated { get; }
|
||||
Guid UserId { get; }
|
||||
Guid AccessTokenId { get; }
|
||||
Guid RefreshTokenId { get; }
|
||||
string[] Permissions { get; }
|
||||
}
|
||||
}
|
||||
14
Backend/Security/Permissions.cs
Normal file
14
Backend/Security/Permissions.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace Backend.Security;
|
||||
|
||||
public static class Permissions {
|
||||
|
||||
public const string ShowUsers = "users.see";
|
||||
public const string EditUsers = "users.edit";
|
||||
public const string DeleteUsers = "users.delete";
|
||||
public const string LogoutUsers = "users.logout";
|
||||
public const string EditUserPermissions = "users.permissions.edit";
|
||||
public const string ShowUserPermissions = "users.permissions.show";
|
||||
|
||||
public const string EditOwnPermissions = "self.permissions.edit";
|
||||
|
||||
}
|
||||
26
Backend/Security/TokenContext.cs
Normal file
26
Backend/Security/TokenContext.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using Backend.Security.Authorization;
|
||||
|
||||
namespace Backend.Security {
|
||||
internal class TokenContext : ITokenContext {
|
||||
private readonly IHttpContextAccessor _accessor;
|
||||
|
||||
public TokenContext(IHttpContextAccessor accessor) {
|
||||
_accessor = accessor;
|
||||
}
|
||||
|
||||
public bool IsAuthenticated => _accessor.HttpContext?.User.Identity?.IsAuthenticated == true;
|
||||
|
||||
public Guid UserId => CreateGuild(_accessor.HttpContext?.User.GetUserId());
|
||||
|
||||
public Guid AccessTokenId => CreateGuild(_accessor.HttpContext?.User.GetAccessTokenId());
|
||||
|
||||
public Guid RefreshTokenId => CreateGuild(_accessor.HttpContext?.User.GetRefreshTokenId());
|
||||
|
||||
public string[] Permissions => _accessor.HttpContext?.User.GetPermissions();
|
||||
|
||||
private static Guid CreateGuild(string id) {
|
||||
if (string.IsNullOrEmpty(id)) return Guid.Empty;
|
||||
return Guid.Parse(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
6
Backend/appsettings.Development.json
Normal file
6
Backend/appsettings.Development.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"Origins": ["http://localhost:4200", "http://localhost:9876"],
|
||||
"Authentication": {
|
||||
"DebugAccessToken": "474a0461-37ec-4b11-aefe-00c423d1511e"
|
||||
}
|
||||
}
|
||||
44
Backend/appsettings.json
Normal file
44
Backend/appsettings.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Backend.Security.Authentication.JwtTokenAuthenticationHandler": "None"
|
||||
}
|
||||
},
|
||||
"MySQL": "SERVER=213.136.89.237;DATABASE=WebDesktop;UID=WebDesktop;PASSWORD=Hft6bP@V3IkYvqS1",
|
||||
"Origins": ["https://desktop.leon-hoppe.de"],
|
||||
"AllowedHosts": "*",
|
||||
"Authentication": {
|
||||
"RefreshTokenExpirationTimeInHours": 12,
|
||||
"AccessTokenExpirationTimeInMinutes": 5,
|
||||
"DebugAccessToken": null
|
||||
},
|
||||
"Users": {
|
||||
"DefaultPermissions": ["group.user"]
|
||||
},
|
||||
"Groups": [
|
||||
{
|
||||
"Permission": "group.admin",
|
||||
"Name": "Admin",
|
||||
"Inherits": [],
|
||||
"Permissions": ["*"]
|
||||
},
|
||||
{
|
||||
"Permission": "group.user",
|
||||
"Name": "User",
|
||||
"Inherits": [],
|
||||
"Permissions": []
|
||||
}
|
||||
],
|
||||
"Messages": {
|
||||
"Users": {
|
||||
"NotFound": "This user does not exist",
|
||||
"InvalidEditData": "Userdata does not match security rules",
|
||||
"InvalidRegisterData": "Userdata does not match security rules",
|
||||
"WrongPassword": "Wrong password",
|
||||
"UsernameOrEmailExist": "This username or email already exist",
|
||||
"InvalidRefreshToken": "Invalid RefreshToken"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user