Added login/register function. Implemented authentication. Started authorization
This commit is contained in:
44
Moonlight.ApiServer/Http/Controllers/Auth/AuthController.cs
Normal file
44
Moonlight.ApiServer/Http/Controllers/Auth/AuthController.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Moonlight.ApiServer.Services;
|
||||
using Moonlight.Shared.Http.Requests.Auth;
|
||||
using Moonlight.Shared.Http.Responses.Auth;
|
||||
|
||||
namespace Moonlight.ApiServer.Http.Controllers.Auth;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/auth")]
|
||||
public class AuthController : Controller
|
||||
{
|
||||
private readonly AuthService AuthService;
|
||||
|
||||
public AuthController(AuthService authService)
|
||||
{
|
||||
AuthService = authService;
|
||||
}
|
||||
|
||||
[HttpPost("login")]
|
||||
public async Task<LoginResponse> Login([FromBody] LoginRequest request)
|
||||
{
|
||||
var user = await AuthService.Login(request.Email, request.Password);
|
||||
|
||||
return new LoginResponse()
|
||||
{
|
||||
Token = await AuthService.GenerateToken(user)
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost("register")]
|
||||
public async Task<RegisterResponse> Register([FromBody] RegisterRequest request)
|
||||
{
|
||||
var user = await AuthService.Register(
|
||||
request.Username,
|
||||
request.Email,
|
||||
request.Password
|
||||
);
|
||||
|
||||
return new RegisterResponse()
|
||||
{
|
||||
Token = await AuthService.GenerateToken(user)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MoonCore.Services;
|
||||
using Moonlight.ApiServer.Configuration;
|
||||
using Moonlight.ApiServer.Models;
|
||||
|
||||
namespace Moonlight.ApiServer.Http.Controllers.Swagger;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/swagger")]
|
||||
public class SwaggerController : Controller
|
||||
{
|
||||
private readonly ConfigService<AppConfiguration> ConfigService;
|
||||
|
||||
public SwaggerController(ConfigService<AppConfiguration> configService)
|
||||
{
|
||||
ConfigService = configService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult> Get()
|
||||
{
|
||||
if (!ConfigService.Get().Development.EnableApiDocs)
|
||||
return BadRequest("Api docs are disabled");
|
||||
|
||||
var options = new ApiDocsOptions();
|
||||
var optionsJson = JsonSerializer.Serialize(options);
|
||||
|
||||
//TODO: Replace the css link with a better one
|
||||
|
||||
var html = "<!doctype html>\n" +
|
||||
"<html>\n" +
|
||||
"<head>\n" +
|
||||
"<title>Moonlight Api Reference</title>\n" +
|
||||
"<meta charset=\"utf-8\" />\n" +
|
||||
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n" +
|
||||
"</head>\n" +
|
||||
"<body>\n" +
|
||||
"<script id=\"api-reference\" data-url=\"/api/swagger/main\"></script>\n" +
|
||||
"<script>\n" +
|
||||
"var configuration =\n" +
|
||||
$"{optionsJson}\n" +
|
||||
"\n" +
|
||||
"document.getElementById('api-reference').dataset.configuration =\n" +
|
||||
"JSON.stringify(configuration)\n" +
|
||||
"</script>\n" +
|
||||
"<script src=\"https://cdn.jsdelivr.net/npm/@scalar/api-reference\"></script>\n" +
|
||||
"<style>.light-mode {\n --scalar-background-1: #fff;\n --scalar-background-2: #f8fafc;\n --scalar-background-3: #e7e7e7;\n --scalar-background-accent: #8ab4f81f;\n --scalar-color-1: #000;\n --scalar-color-2: #6b7280;\n --scalar-color-3: #9ca3af;\n --scalar-color-accent: #00c16a;\n --scalar-border-color: #e5e7eb;\n --scalar-color-green: #069061;\n --scalar-color-red: #ef4444;\n --scalar-color-yellow: #f59e0b;\n --scalar-color-blue: #1d4ed8;\n --scalar-color-orange: #fb892c;\n --scalar-color-purple: #6d28d9;\n --scalar-button-1: #000;\n --scalar-button-1-hover: rgba(0, 0, 0, 0.9);\n --scalar-button-1-color: #fff;\n}\n.dark-mode {\n --scalar-background-1: #020420;\n --scalar-background-2: #121a31;\n --scalar-background-3: #1e293b;\n --scalar-background-accent: #8ab4f81f;\n --scalar-color-1: #fff;\n --scalar-color-2: #cbd5e1;\n --scalar-color-3: #94a3b8;\n --scalar-color-accent: #00dc82;\n --scalar-border-color: #1e293b;\n --scalar-color-green: #069061;\n --scalar-color-red: #f87171;\n --scalar-color-yellow: #fde68a;\n --scalar-color-blue: #60a5fa;\n --scalar-color-orange: #fb892c;\n --scalar-color-purple: #ddd6fe;\n --scalar-button-1: hsla(0, 0%, 100%, 0.9);\n --scalar-button-1-hover: hsla(0, 0%, 100%, 0.8);\n --scalar-button-1-color: #000;\n}\n.dark-mode .t-doc__sidebar,\n.light-mode .t-doc__sidebar {\n --scalar-sidebar-background-1: var(--scalar-background-1);\n --scalar-sidebar-color-1: var(--scalar-color-1);\n --scalar-sidebar-color-2: var(--scalar-color-3);\n --scalar-sidebar-border-color: var(--scalar-border-color);\n --scalar-sidebar-item-hover-background: transparent;\n --scalar-sidebar-item-hover-color: var(--scalar-color-1);\n --scalar-sidebar-item-active-background: transparent;\n --scalar-sidebar-color-active: var(--scalar-color-accent);\n --scalar-sidebar-search-background: transparent;\n --scalar-sidebar-search-color: var(--scalar-color-3);\n --scalar-sidebar-search-border-color: var(--scalar-border-color);\n --scalar-sidebar-indent-border: var(--scalar-border-color);\n --scalar-sidebar-indent-border-hover: var(--scalar-color-1);\n --scalar-sidebar-indent-border-active: var(--scalar-color-accent);\n}\n.scalar-card .request-card-footer {\n --scalar-background-3: var(--scalar-background-2);\n --scalar-button-1: #0f172a;\n --scalar-button-1-hover: rgba(30, 41, 59, 0.5);\n --scalar-button-1-color: #fff;\n}\n.scalar-card .show-api-client-button {\n border: 1px solid #334155 !important;\n}</style>\n" +
|
||||
"</body>\n" +
|
||||
"</html>";
|
||||
|
||||
return Content(html, "text/html");
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,107 @@
|
||||
namespace Moonlight.ApiServer.Http.Middleware;
|
||||
using System.Text.Json;
|
||||
using MoonCore.Extended.Abstractions;
|
||||
using MoonCore.Extended.Helpers;
|
||||
using MoonCore.Services;
|
||||
using Moonlight.ApiServer.Configuration;
|
||||
using Moonlight.ApiServer.Database.Entities;
|
||||
using Moonlight.ApiServer.Helpers.Authentication;
|
||||
|
||||
namespace Moonlight.ApiServer.Http.Middleware;
|
||||
|
||||
public class AuthenticationMiddleware
|
||||
{
|
||||
private readonly RequestDelegate Next;
|
||||
private readonly ILogger<AuthenticationMiddleware> Logger;
|
||||
|
||||
public AuthenticationMiddleware(RequestDelegate next)
|
||||
public AuthenticationMiddleware(RequestDelegate next, ILogger<AuthenticationMiddleware> logger)
|
||||
{
|
||||
Next = next;
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
await Authenticate(context);
|
||||
await Next(context);
|
||||
}
|
||||
|
||||
private async Task Authenticate(HttpContext context)
|
||||
{
|
||||
var request = context.Request;
|
||||
string? token = null;
|
||||
|
||||
// Cookie for Moonlight.Client
|
||||
if (request.Cookies.ContainsKey("token") && !string.IsNullOrEmpty(request.Cookies["token"]))
|
||||
token = request.Cookies["token"];
|
||||
|
||||
// Header for api clients
|
||||
if (request.Headers.ContainsKey("Authorization") && !string.IsNullOrEmpty(request.Cookies["Authorization"]))
|
||||
{
|
||||
var headerValue = request.Cookies["Authorization"] ?? "";
|
||||
|
||||
if (headerValue.StartsWith("Bearer"))
|
||||
{
|
||||
var headerParts = headerValue.Split(" ");
|
||||
|
||||
if (headerParts.Length > 1 && !string.IsNullOrEmpty(headerParts[1]))
|
||||
token = headerParts[1];
|
||||
}
|
||||
}
|
||||
|
||||
if(token == null)
|
||||
return;
|
||||
|
||||
// Validate token
|
||||
if (token.Length > 300)
|
||||
{
|
||||
Logger.LogWarning("Received token bigger than 300 characters, Length: {length}", token.Length);
|
||||
return;
|
||||
}
|
||||
|
||||
// Decide which authentication method we need for the token
|
||||
if (token.Count(x => x == '.') == 2) // JWT only has two dots
|
||||
await AuthenticateUser(context, token);
|
||||
else
|
||||
await AuthenticateApiKey(context, token);
|
||||
}
|
||||
|
||||
private async Task AuthenticateUser(HttpContext context, string jwt)
|
||||
{
|
||||
var jwtHelper = context.RequestServices.GetRequiredService<JwtHelper>();
|
||||
var configService = context.RequestServices.GetRequiredService<ConfigService<AppConfiguration>>();
|
||||
var secret = configService.Get().Authentication.Secret;
|
||||
|
||||
if(!await jwtHelper.Validate(secret, jwt, "login"))
|
||||
return;
|
||||
|
||||
var data = await jwtHelper.Decode(secret, jwt);
|
||||
|
||||
if(!data.TryGetValue("iat", out var issuedAtString) || !data.TryGetValue("userId", out var userIdString))
|
||||
return;
|
||||
|
||||
var userId = int.Parse(userIdString);
|
||||
var issuedAt = DateTimeOffset.FromUnixTimeSeconds(long.Parse(issuedAtString)).DateTime;
|
||||
|
||||
var userRepo = context.RequestServices.GetRequiredService<DatabaseRepository<User>>();
|
||||
var user = userRepo.Get().FirstOrDefault(x => x.Id == userId);
|
||||
|
||||
if(user == null)
|
||||
return;
|
||||
|
||||
// Check if token is in the past
|
||||
if(user.TokenValidTimestamp > issuedAt)
|
||||
return;
|
||||
|
||||
// Load permissions, handle empty values
|
||||
var permissions = JsonSerializer.Deserialize<string[]>(
|
||||
string.IsNullOrEmpty(user.PermissionsJson) ? "[]" : user.PermissionsJson
|
||||
) ?? [];
|
||||
|
||||
// Save permission state
|
||||
context.User = new PermClaimsPrinciple(permissions, user);
|
||||
}
|
||||
|
||||
private async Task AuthenticateApiKey(HttpContext context, string apiKey)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Moonlight.ApiServer.Attributes;
|
||||
|
||||
namespace Moonlight.ApiServer.Http.Middleware;
|
||||
|
||||
public class AuthorisationMiddleware
|
||||
{
|
||||
private readonly RequestDelegate Next;
|
||||
private readonly ILogger<AuthorisationMiddleware> Logger;
|
||||
|
||||
public AuthorisationMiddleware(RequestDelegate next, ILogger<AuthorisationMiddleware> logger)
|
||||
{
|
||||
Next = next;
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
await Next(context);
|
||||
}
|
||||
|
||||
private async Task Authorize(HttpContext context)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private string[] ResolveRequiredPermissions(HttpContext context)
|
||||
{
|
||||
// Basic handling
|
||||
var endpoint = context.GetEndpoint();
|
||||
|
||||
if (endpoint == null)
|
||||
return [];
|
||||
|
||||
var metadata = endpoint
|
||||
.Metadata
|
||||
.GetMetadata<ControllerActionDescriptor>();
|
||||
|
||||
if (metadata == null)
|
||||
return [];
|
||||
|
||||
// Retrieve attribute infos
|
||||
var controllerAttrInfo = metadata
|
||||
.ControllerTypeInfo
|
||||
.CustomAttributes
|
||||
.FirstOrDefault(x => x.AttributeType == typeof(RequirePermissionAttribute));
|
||||
|
||||
var methodAttrInfo = metadata
|
||||
.MethodInfo
|
||||
.CustomAttributes
|
||||
.FirstOrDefault(x => x.AttributeType == typeof(RequirePermissionAttribute));
|
||||
|
||||
// Retrieve permissions from attribute infos
|
||||
var controllerPermission = controllerAttrInfo != null
|
||||
? controllerAttrInfo.ConstructorArguments.First().Value as string
|
||||
: null;
|
||||
|
||||
var methodPermission = methodAttrInfo != null
|
||||
? methodAttrInfo.ConstructorArguments.First().Value as string
|
||||
: null;
|
||||
|
||||
// If both have a permission flag, return both
|
||||
if (controllerPermission != null && methodPermission != null)
|
||||
return [controllerPermission, methodPermission];
|
||||
|
||||
// If either of them have a permission set, return it
|
||||
if (controllerPermission != null)
|
||||
return [controllerPermission];
|
||||
|
||||
if (methodPermission != null)
|
||||
return [methodPermission];
|
||||
|
||||
// If both have no permission set, allow everyone to access it
|
||||
return [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user