Add users, roles, and per-novel permissions
Introduces accounts (ASP.NET Identity + cookie auth), four global roles (Admin/Writer/Editor/Reviewer), per-novel ownership and grants via ProjectMember, and a service-API-key principal for the MCP server and background import jobs. Enforcement lives in the application services (not endpoint filters) so the embedded agent and MCP tools, which call the same services directly, can't bypass it. Web client gets a login page, session-aware routing, and a People section for managing per-novel access. Also includes prior in-flight changes from this branch (CLAUDE.md compliance pass, dev-deploy docker-compose setup) that were uncommitted when this feature work started.
This commit is contained in:
@@ -4,31 +4,23 @@ using Novelly.Api.Common;
|
||||
using Novelly.Api.Common.Validation;
|
||||
using Novelly.Api.Data;
|
||||
using Novelly.Api.Projects;
|
||||
using Novelly.Api.Users;
|
||||
|
||||
namespace Novelly.Api.Imports;
|
||||
|
||||
/// <summary>
|
||||
/// Read-only inspection and job creation for outline imports. The actual import — reading
|
||||
/// source files, calling the model, writing project data — runs in <see cref="ImportAgentService"/>,
|
||||
/// driven off the request thread by <see cref="ImportJobRunner"/>; this service only ever
|
||||
/// touches the filesystem to peek at a ledger, never to import anything itself.
|
||||
/// </summary>
|
||||
public class ImportService(
|
||||
INovelDbContext db,
|
||||
ProjectService projects,
|
||||
Channel<Guid> queue,
|
||||
INovelUserContext userContext,
|
||||
ILogger<ImportService> logger,
|
||||
IModelValidator<InspectImportRequest> inspectValidator,
|
||||
IModelValidator<StartImportRequest> startValidator)
|
||||
{
|
||||
/// <summary>
|
||||
/// Reports whether a folder is a fresh import, one to resume, or already complete —
|
||||
/// so the UI can offer the right action before committing to anything.
|
||||
/// </summary>
|
||||
public Task<ImportInspectionResponse> InspectAsync(InspectImportRequest request, CancellationToken ct = default)
|
||||
{
|
||||
Guard.Null(request, nameof(request));
|
||||
inspectValidator.Validate(request).ThrowIfInvalid();
|
||||
inspectValidator.Validate(request).ThrowIfInvalid(logger);
|
||||
|
||||
logger.LogInformation("Inspecting import source {SourceRoot}", request.SourceRoot);
|
||||
|
||||
@@ -48,16 +40,10 @@ public class ImportService(
|
||||
readiness, ledger.ProjectId, chaptersDone, total, ledger.CompletedPasses ?? []));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates (or reuses) an <see cref="ImportJob"/> for this source root and enqueues it
|
||||
/// for the background runner. <see cref="StartImportRequest.ForceRestart"/> deletes the
|
||||
/// ledger and the project it points at first — the "complete, delete and reimport" path —
|
||||
/// so make sure the caller has confirmed with the writer before setting it.
|
||||
/// </summary>
|
||||
public async Task<ImportJob> StartOrResumeAsync(StartImportRequest request, CancellationToken ct = default)
|
||||
{
|
||||
Guard.Null(request, nameof(request));
|
||||
startValidator.Validate(request).ThrowIfInvalid();
|
||||
startValidator.Validate(request).ThrowIfInvalid(logger);
|
||||
|
||||
logger.LogInformation(
|
||||
"Starting import for {SourceRoot}, forceRestart {ForceRestart}", request.SourceRoot, request.ForceRestart);
|
||||
@@ -88,7 +74,12 @@ public class ImportService(
|
||||
return existing;
|
||||
}
|
||||
|
||||
var job = new ImportJob { SourceRoot = root, ChaptersTotal = ImportPaths.CountChapterFiles(root) };
|
||||
var job = new ImportJob
|
||||
{
|
||||
SourceRoot = root,
|
||||
ChaptersTotal = ImportPaths.CountChapterFiles(root),
|
||||
RequestedByUserId = userContext.UserId
|
||||
};
|
||||
db.ImportJobs.Add(job);
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
@@ -97,7 +88,6 @@ public class ImportService(
|
||||
return job;
|
||||
}
|
||||
|
||||
/// <summary>Null when no job has this id — a lookup miss is expected, not exceptional.</summary>
|
||||
public async Task<ImportJob?> GetStatusAsync(Guid id, CancellationToken ct = default)
|
||||
{
|
||||
Guard.Default(id, nameof(id));
|
||||
|
||||
Reference in New Issue
Block a user