Recreated solution with web app template. Improved theme. Switched to ShadcnBlazor library
This commit is contained in:
49
Moonlight.Api/Services/DbMigrationService.cs
Normal file
49
Moonlight.Api/Services/DbMigrationService.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moonlight.Api.Database;
|
||||
|
||||
namespace Moonlight.Api.Services;
|
||||
|
||||
public class DbMigrationService : IHostedLifecycleService
|
||||
{
|
||||
private readonly ILogger<DbMigrationService> Logger;
|
||||
private readonly IServiceProvider ServiceProvider;
|
||||
|
||||
public DbMigrationService(ILogger<DbMigrationService> logger, IServiceProvider serviceProvider)
|
||||
{
|
||||
Logger = logger;
|
||||
ServiceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public async Task StartingAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Logger.LogTrace("Checking for pending migrations");
|
||||
|
||||
await using var scope = ServiceProvider.CreateAsyncScope();
|
||||
var context = scope.ServiceProvider.GetRequiredService<DataContext>();
|
||||
|
||||
var pendingMigrations = await context.Database.GetPendingMigrationsAsync(cancellationToken);
|
||||
var migrationNames = pendingMigrations.ToArray();
|
||||
|
||||
if (migrationNames.Length == 0)
|
||||
{
|
||||
Logger.LogDebug("No pending migrations found");
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogInformation("Pending migrations: {names}", string.Join(", ", migrationNames));
|
||||
Logger.LogInformation("Migration started");
|
||||
|
||||
await context.Database.MigrateAsync(cancellationToken);
|
||||
|
||||
Logger.LogInformation("Migration complete");
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
99
Moonlight.Api/Services/UserAuthService.cs
Normal file
99
Moonlight.Api/Services/UserAuthService.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moonlight.Api.Database;
|
||||
using Moonlight.Api.Database.Entities;
|
||||
|
||||
namespace Moonlight.Api.Services;
|
||||
|
||||
public class UserAuthService
|
||||
{
|
||||
private readonly DatabaseRepository<User> UserRepository;
|
||||
private readonly ILogger<UserAuthService> Logger;
|
||||
|
||||
private const string UserIdClaim = "UserId";
|
||||
private const string IssuedAtClaim = "IssuedAt";
|
||||
|
||||
public UserAuthService(DatabaseRepository<User> userRepository, ILogger<UserAuthService> logger)
|
||||
{
|
||||
UserRepository = userRepository;
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
public async Task<bool> SyncAsync(ClaimsPrincipal? principal)
|
||||
{
|
||||
if (principal is null)
|
||||
return false;
|
||||
|
||||
var username = principal.FindFirstValue(ClaimTypes.Name);
|
||||
var email = principal.FindFirstValue(ClaimTypes.Email);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(email))
|
||||
{
|
||||
Logger.LogWarning("Unable to sync user to database as name and/or email claims are missing");
|
||||
return false;
|
||||
}
|
||||
|
||||
// We use email as the primary identifier here
|
||||
var user = await UserRepository
|
||||
.Query()
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(user => user.Email == email);
|
||||
|
||||
if (user == null) // Sync user if not already existing in the database
|
||||
{
|
||||
user = await UserRepository.AddAsync(new User()
|
||||
{
|
||||
Username = username,
|
||||
Email = email,
|
||||
InvalidateTimestamp = DateTimeOffset.UtcNow.AddMinutes(-1)
|
||||
});
|
||||
}
|
||||
else // Update properties of existing user
|
||||
{
|
||||
user.Username = username;
|
||||
|
||||
await UserRepository.UpdateAsync(user);
|
||||
}
|
||||
|
||||
principal.Identities.First().AddClaims([
|
||||
new Claim(UserIdClaim, user.Id.ToString()),
|
||||
new Claim(IssuedAtClaim, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString())
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> ValidateAsync(ClaimsPrincipal? principal)
|
||||
{
|
||||
// Ignore malformed claims principal
|
||||
if (principal is not { Identity.IsAuthenticated: true })
|
||||
return false;
|
||||
|
||||
var userIdString = principal.FindFirstValue(UserIdClaim);
|
||||
|
||||
if (!int.TryParse(userIdString, out var userId))
|
||||
return false;
|
||||
|
||||
var user = await UserRepository
|
||||
.Query()
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(user => user.Id == userId);
|
||||
|
||||
if (user == null)
|
||||
return false;
|
||||
|
||||
var issuedAtString = principal.FindFirstValue(IssuedAtClaim);
|
||||
|
||||
if (!long.TryParse(issuedAtString, out var issuedAtUnix))
|
||||
return false;
|
||||
|
||||
var issuedAt = DateTimeOffset.FromUnixTimeSeconds(issuedAtUnix).ToUniversalTime();
|
||||
|
||||
// If the issued at timestamp is greater than the token validation timestamp
|
||||
// everything is fine. If not it means that the token should be invalidated
|
||||
// as it is too old
|
||||
|
||||
return issuedAt > user.InvalidateTimestamp;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user