Add admin database download
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using RpgRoller.Contracts;
|
||||
using RpgRoller.Domain;
|
||||
using RpgRoller.Hosting;
|
||||
using RpgRoller.Services;
|
||||
|
||||
namespace RpgRoller.Api;
|
||||
@@ -25,6 +28,23 @@ internal static class AdminEndpoints
|
||||
return ApiResultMapper.ToApiResult(result);
|
||||
});
|
||||
|
||||
group.MapGet("/admin/database", Results<FileStreamHttpResult, BadRequest<ApiError>, UnauthorizedHttpResult> (HttpContext context, IGameService game, SqliteDatabaseFile databaseFile) =>
|
||||
{
|
||||
var sessionToken = context.GetRequiredSessionToken();
|
||||
var user = game.GetUserBySession(sessionToken);
|
||||
if (user is null)
|
||||
return TypedResults.Unauthorized();
|
||||
|
||||
if (!user.Roles.Contains(UserRoles.Admin, StringComparer.OrdinalIgnoreCase))
|
||||
return ApiResultMapper.ToBadRequest(new ServiceError("forbidden", "Admin role is required."));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(databaseFile.Path) || !File.Exists(databaseFile.Path))
|
||||
return ApiResultMapper.ToBadRequest(new ServiceError("database_unavailable", "SQLite database file is not available."));
|
||||
|
||||
var stream = new FileStream(databaseFile.Path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
return TypedResults.File(stream, "application/octet-stream", Path.GetFileName(databaseFile.Path));
|
||||
});
|
||||
|
||||
return group;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,18 @@
|
||||
else if (IsAdminScreen)
|
||||
{
|
||||
<main class="management-screen">
|
||||
@if (IsCurrentUserAdmin)
|
||||
{
|
||||
<section class="card">
|
||||
<div class="section-head">
|
||||
<h2>Database</h2>
|
||||
</div>
|
||||
<p class="muted">Download the current SQLite file for backup or offline inspection.</p>
|
||||
<div class="management-actions">
|
||||
<a class="action-link" href="@AdminDatabaseDownloadUrl" download>Download SQLite database</a>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
<section class="card">
|
||||
<div class="section-head">
|
||||
<h2>User Management</h2>
|
||||
|
||||
@@ -928,6 +928,9 @@ public partial class Workspace : IAsyncDisposable
|
||||
[Inject]
|
||||
private RpgRollerApiClient ApiClient { get; set; } = null!;
|
||||
|
||||
[Inject]
|
||||
private NavigationManager Navigation { get; set; } = null!;
|
||||
|
||||
private UserSummary? User { get; set; }
|
||||
private Guid? ActiveCharacterId { get; set; }
|
||||
private Guid? SelectedCampaignId { get; set; }
|
||||
@@ -1117,6 +1120,7 @@ public partial class Workspace : IAsyncDisposable
|
||||
};
|
||||
|
||||
private string AppCssClass => IsPlayScreen ? "rr-app app-play" : "rr-app";
|
||||
private string AdminDatabaseDownloadUrl => Navigation.ToAbsoluteUri("api/admin/database").ToString();
|
||||
|
||||
private const string ScreenPlay = "play";
|
||||
private const string ScreenManagement = "management";
|
||||
|
||||
@@ -12,9 +12,11 @@ public static class ServiceCollectionExtensions
|
||||
public static IServiceCollection AddRpgRollerCore(this IServiceCollection services, IConfiguration configuration, IWebHostEnvironment environment)
|
||||
{
|
||||
var sqliteConnectionString = configuration.GetConnectionString("RpgRoller") ?? "Data Source=App_Data/rpgroller.db";
|
||||
EnsureSqliteDataDirectory(sqliteConnectionString, environment.ContentRootPath);
|
||||
var sqliteDatabasePath = ResolveSqliteDatabasePath(sqliteConnectionString, environment.ContentRootPath);
|
||||
EnsureSqliteDataDirectory(sqliteDatabasePath);
|
||||
|
||||
services.AddSingleton<IPasswordHasher<UserAccount>, PasswordHasher<UserAccount>>();
|
||||
services.AddSingleton(new SqliteDatabaseFile(sqliteDatabasePath));
|
||||
services.AddDbContextFactory<RpgRollerDbContext>(options => options.UseSqlite(sqliteConnectionString));
|
||||
services.AddSingleton<IDiceRoller, RandomDiceRoller>();
|
||||
services.AddSingleton<IGameService, GameService>();
|
||||
@@ -22,16 +24,20 @@ public static class ServiceCollectionExtensions
|
||||
return services;
|
||||
}
|
||||
|
||||
private static void EnsureSqliteDataDirectory(string connectionString, string contentRootPath)
|
||||
private static string? ResolveSqliteDatabasePath(string connectionString, string contentRootPath)
|
||||
{
|
||||
var builder = new SqliteConnectionStringBuilder(connectionString);
|
||||
if (string.IsNullOrWhiteSpace(builder.DataSource) || builder.DataSource == ":memory:")
|
||||
return;
|
||||
return null;
|
||||
|
||||
var fullPath = Path.IsPathRooted(builder.DataSource) ? builder.DataSource : Path.Combine(contentRootPath, builder.DataSource);
|
||||
var databasePath = Path.IsPathRooted(builder.DataSource) ? builder.DataSource : Path.Combine(contentRootPath, builder.DataSource);
|
||||
return Path.GetFullPath(databasePath);
|
||||
}
|
||||
|
||||
var directory = Path.GetDirectoryName(fullPath);
|
||||
private static void EnsureSqliteDataDirectory(string? sqliteDatabasePath)
|
||||
{
|
||||
var directory = Path.GetDirectoryName(sqliteDatabasePath);
|
||||
if (!string.IsNullOrWhiteSpace(directory))
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3
RpgRoller/Hosting/SqliteDatabaseFile.cs
Normal file
3
RpgRoller/Hosting/SqliteDatabaseFile.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace RpgRoller.Hosting;
|
||||
|
||||
public sealed record SqliteDatabaseFile(string? Path);
|
||||
@@ -711,6 +711,29 @@ select:focus-visible {
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.action-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
align-self: flex-start;
|
||||
padding: 0.55rem 0.65rem;
|
||||
border: 1px solid #b39f79;
|
||||
border-radius: 0.45rem;
|
||||
background: #f9f2e2;
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.action-link:hover {
|
||||
background: var(--button-hover);
|
||||
}
|
||||
|
||||
.action-link:focus-visible {
|
||||
outline: 3px solid var(--focus);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.add-row-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user