Added login/register function. Implemented authentication. Started authorization

This commit is contained in:
Masu-Baumgartner
2024-10-01 11:29:19 +02:00
parent 73bf27d222
commit ef2e6c9a20
23 changed files with 741 additions and 27 deletions

View File

@@ -0,0 +1,11 @@
namespace Moonlight.ApiServer.Attributes;
public class RequirePermissionAttribute : Attribute
{
public string Permission { get; set; }
public RequirePermissionAttribute(string permission)
{
Permission = permission;
}
}

View File

@@ -3,6 +3,8 @@
public class AppConfiguration
{
public DatabaseConfig Database { get; set; } = new();
public AuthenticationConfig Authentication { get; set; } = new();
public DevelopmentConfig Development { get; set; } = new();
public class DatabaseConfig
{
@@ -14,4 +16,19 @@ public class AppConfiguration
public string Database { get; set; } = "db_name";
}
public class AuthenticationConfig
{
public string Secret { get; set; } = Guid
.NewGuid()
.ToString()
.Replace("-", "");
public int TokenDuration { get; set; } = 10;
}
public class DevelopmentConfig
{
public bool EnableApiDocs { get; set; } = false;
}
}

View File

@@ -1,8 +1,12 @@
using Moonlight.ApiServer.Helpers;
using Microsoft.EntityFrameworkCore;
using Moonlight.ApiServer.Database.Entities;
using Moonlight.ApiServer.Helpers;
namespace Moonlight.ApiServer.Database;
public class CoreDataContext : DatabaseContext
{
public override string Prefix { get; } = "Core";
public DbSet<User> Users { get; set; }
}

View File

@@ -0,0 +1,13 @@
namespace Moonlight.ApiServer.Database.Entities;
public class User
{
public int Id { get; set; }
public string Username { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public DateTime TokenValidTimestamp { get; set; } = DateTime.UtcNow;
public string PermissionsJson { get; set; } = "[]";
}

View File

@@ -0,0 +1,63 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Moonlight.ApiServer.Database;
#nullable disable
namespace Moonlight.ApiServer.Database.Migrations
{
[DbContext(typeof(CoreDataContext))]
[Migration("20241001090841_AddedBasicUserModel")]
partial class AddedBasicUserModel
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("Core")
.HasAnnotation("ProductVersion", "8.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Email")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("PermissionsJson")
.IsRequired()
.HasColumnType("longtext");
b.Property<DateTime>("TokenValidTimestamp")
.HasColumnType("datetime(6)");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("Users", "Core");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,53 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.ApiServer.Database.Migrations
{
/// <inheritdoc />
public partial class AddedBasicUserModel : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.EnsureSchema(
name: "Core");
migrationBuilder.AlterDatabase()
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "Users",
schema: "Core",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Username = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Email = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Password = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
TokenValidTimestamp = table.Column<DateTime>(type: "datetime(6)", nullable: false),
PermissionsJson = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4")
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Users",
schema: "Core");
}
}
}

View File

@@ -0,0 +1,60 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Moonlight.ApiServer.Database;
#nullable disable
namespace Moonlight.ApiServer.Database.Migrations
{
[DbContext(typeof(CoreDataContext))]
partial class CoreDataContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("Core")
.HasAnnotation("ProductVersion", "8.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
modelBuilder.Entity("Moonlight.ApiServer.Database.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Email")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("PermissionsJson")
.IsRequired()
.HasColumnType("longtext");
b.Property<DateTime>("TokenValidTimestamp")
.HasColumnType("datetime(6)");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("Users", "Core");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,52 @@
using System.Security.Claims;
using Moonlight.ApiServer.Database.Entities;
namespace Moonlight.ApiServer.Helpers.Authentication;
public class PermClaimsPrinciple : ClaimsPrincipal
{
public string[] Permissions { get; private set; }
public User? CurrentModel { get; private set; }
public PermClaimsPrinciple(string[] permissions, User? currentModel)
{
Permissions = permissions;
CurrentModel = currentModel;
}
public bool HasPermission(string requiredPermission)
{
// Check for wildcard permission
if (Permissions.Contains("*"))
return true;
var requiredSegments = requiredPermission.Split('.');
// Check if the user has the exact permission or a wildcard match
foreach (var permission in Permissions)
{
var permissionSegments = permission.Split('.');
// Iterate over the segments of the required permission
for (var i = 0; i < requiredSegments.Length; i++)
{
// If the current segment matches or is a wildcard, continue to the next segment
if (i < permissionSegments.Length && requiredSegments[i] == permissionSegments[i] ||
permissionSegments[i] == "*")
{
// If we've reached the end of the permissionSegments array, it means we've found a match
if (i == permissionSegments.Length - 1)
return true; // Found an exact match or a wildcard match
}
else
{
// If we reach here, it means the segments don't match and we break out of the loop
break;
}
}
}
// No matching permission found
return false;
}
}

View File

@@ -1,8 +0,0 @@
using System.Security.Claims;
namespace Moonlight.ApiServer.Helpers.Authentication;
public class SyncedClaimsPrinciple : ClaimsPrincipal
{
}

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,43 @@
namespace Moonlight.ApiServer.Models;
// From https://github.com/scalar/scalar/blob/main/packages/scalar.aspnetcore/ScalarOptions.cs
public class ApiDocsOptions
{
public string Theme { get; set; } = "purple";
public bool? DarkMode { get; set; }
public bool? HideDownloadButton { get; set; }
public bool? ShowSideBar { get; set; }
public bool? WithDefaultFonts { get; set; }
public string? Layout { get; set; }
public string? CustomCss { get; set; }
public string? SearchHotkey { get; set; }
public Dictionary<string, string>? Metadata { get; set; }
public ScalarAuthenticationOptions? Authentication { get; set; }
}
public class ScalarAuthenticationOptions
{
public string? PreferredSecurityScheme { get; set; }
public ScalarAuthenticationApiKey? ApiKey { get; set; }
}
public class ScalarAuthenticationoAuth2
{
public string? ClientId { get; set; }
public List<string>? Scopes { get; set; }
}
public class ScalarAuthenticationApiKey
{
public string? Token { get; set; }
}

View File

@@ -12,7 +12,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MoonCore" Version="1.5.7" />
<PackageReference Include="MoonCore" Version="1.5.8" />
<PackageReference Include="MoonCore.Extended" Version="1.0.2" />
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
@@ -25,11 +25,9 @@
</ItemGroup>
<ItemGroup>
<Folder Include="Database\Entities\" />
<Folder Include="Database\Migrations\" />
<Folder Include="Http\Controllers\" />
<Folder Include="Models\" />
<Folder Include="Services\" />
<Folder Include="Implementations\" />
<Folder Include="Interfaces\" />
<Folder Include="storage\" />
</ItemGroup>

View File

@@ -1,3 +1,5 @@
using Microsoft.OpenApi.Models;
using MoonCore.Extended.Abstractions;
using MoonCore.Extended.Helpers;
using MoonCore.Extensions;
using MoonCore.Helpers;
@@ -5,6 +7,7 @@ using MoonCore.Services;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Database;
using Moonlight.ApiServer.Helpers;
using Moonlight.ApiServer.Http.Middleware;
// Prepare file system
Directory.CreateDirectory(PathBuilder.Dir("storage"));
@@ -73,18 +76,32 @@ builder.Services.AddSwaggerGen();
builder.Services.AddSingleton(configService);
builder.Services.AddSingleton<JwtHelper>();
builder.Services.AutoAddServices<Program>();
// Database
var databaseHelper = new DatabaseHelper(
loggerFactory.CreateLogger<DatabaseHelper>()
);
builder.Services.AddSingleton(databaseHelper);
builder.Services.AddScoped(typeof(DatabaseRepository<>));
builder.Services.AddDbContext<CoreDataContext>();
databaseHelper.AddDbContext<CoreDataContext>();
databaseHelper.GenerateMappings();
// API Docs
if (configService.Get().Development.EnableApiDocs)
{
// Configure swagger api specification generator and set the document title for the api docs to use
builder.Services.AddSwaggerGen(options => options.SwaggerDoc("main", new OpenApiInfo()
{
Title = "Moonlight API"
}));
}
var app = builder.Build();
using (var scope = app.Services.CreateScope())
@@ -93,18 +110,21 @@ using (var scope = app.Services.CreateScope())
}
if(app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
app.UseWebAssemblyDebugging();
}
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseMiddleware<AuthenticationMiddleware>();
app.MapControllers();
app.MapFallbackToFile("index.html");
// API Docs
if (configService.Get().Development.EnableApiDocs)
app.MapSwagger("/api/swagger/{documentName}");
app.Run();

View File

@@ -0,0 +1,83 @@
using MoonCore.Attributes;
using MoonCore.Exceptions;
using MoonCore.Extended.Abstractions;
using MoonCore.Extended.Helpers;
using MoonCore.Services;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Database.Entities;
namespace Moonlight.ApiServer.Services;
[Scoped]
public class AuthService
{
private readonly DatabaseRepository<User> UserRepository;
private readonly ConfigService<AppConfiguration> ConfigService;
private readonly JwtHelper JwtHelper;
public AuthService(
DatabaseRepository<User> userRepository,
ConfigService<AppConfiguration> configService,
JwtHelper jwtHelper)
{
UserRepository = userRepository;
ConfigService = configService;
JwtHelper = jwtHelper;
}
public Task<User> Register(string username, string email, string password)
{
// Reformat values
username = username.ToLower().Trim();
email = email.ToLower().Trim();
// Check for users with the same values
if (UserRepository.Get().Any(x => x.Username == username))
throw new HttpApiException("A user with that username already exists", 400);
if (UserRepository.Get().Any(x => x.Email == email))
throw new HttpApiException("A user with that email address already exists", 400);
// Build model and add it to the database
var user = new User()
{
Username = username,
Email = email,
Password = HashHelper.Hash(password),
PermissionsJson = "[]",
TokenValidTimestamp = DateTime.UtcNow
};
UserRepository.Add(user);
return Task.FromResult(user);
}
public Task<User> Login(string email, string password)
{
// Reformat values
email = email.ToLower().Trim();
var user = UserRepository
.Get()
.FirstOrDefault(x => x.Email == email);
if (user == null)
throw new HttpApiException("Invalid email or password", 400);
if(!HashHelper.Verify(password, user.Password))
throw new HttpApiException("Invalid email or password", 400);
return Task.FromResult(user);
}
public async Task<string> GenerateToken(User user)
{
var authConfig = ConfigService.Get().Authentication;
return await JwtHelper.Create(authConfig.Secret, data =>
{
data.Add("userId", user.Id.ToString());
}, "login", TimeSpan.FromDays(authConfig.TokenDuration));
}
}

View File

@@ -10,7 +10,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.6"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.6" PrivateAssets="all"/>
<PackageReference Include="MoonCore" Version="1.5.7" />
<PackageReference Include="MoonCore" Version="1.5.8" />
<PackageReference Include="MoonCore.Blazor.Tailwind" Version="1.0.1" />
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.0" />
</ItemGroup>

View File

@@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.Shared.Http.Requests.Auth;
public class LoginRequest
{
[Required(ErrorMessage = "You need to provide an email address")]
[EmailAddress(ErrorMessage = "You need to provide a valid email address")]
public string Email { get; set; }
[Required(ErrorMessage = "You need to provide a password")]
public string Password { get; set; }
}

View File

@@ -0,0 +1,19 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.Shared.Http.Requests.Auth;
public class RegisterRequest
{
[Required(ErrorMessage = "You need to provide an email address")]
[EmailAddress(ErrorMessage = "You need to provide a valid email address")]
public string Email { get; set; }
[Required(ErrorMessage = "You need to provide a username")]
[RegularExpression("^[a-z][a-z0-9]*$", ErrorMessage = "Usernames can only contain lowercase characters and numbers and should not start with a number")]
public string Username { get; set; }
[Required(ErrorMessage = "You need to provide a password")]
[MinLength(8, ErrorMessage = "Your password needs to be at least 8 characters long")]
[MaxLength(256, ErrorMessage = "Your password should not exceed the length of 256 characters")]
public string Password { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace Moonlight.Shared.Http.Responses.Auth;
public class LoginResponse
{
public string Token { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace Moonlight.Shared.Http.Responses.Auth;
public class RegisterResponse
{
public string Token { get; set; }
}

View File

@@ -6,9 +6,4 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Folder Include="Http\Requests\" />
<Folder Include="Http\Responses\" />
</ItemGroup>
</Project>