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:
James Wampler
2026-08-15 22:29:33 -07:00
parent 7d8dd0c4fd
commit e598c18d67
111 changed files with 6562 additions and 797 deletions
+23 -7
View File
@@ -5,12 +5,15 @@ using Novelly.Api.Genres;
using Novelly.Api.Projects;
using Novelly.Api.Questions;
using Novelly.Api.Tags;
using Novelly.Api.Users;
namespace Novelly.Api.Tests;
public abstract class ServiceTestFixture
{
protected TestDatabase Db { get; private set; } = null!;
protected TestUserContext UserContext { get; private set; } = null!;
protected ProjectAccessService Access { get; private set; } = null!;
protected TagService Tags { get; private set; } = null!;
protected ProjectService Projects { get; private set; } = null!;
protected CharacterService Characters { get; private set; } = null!;
@@ -33,6 +36,18 @@ public abstract class ServiceTestFixture
public void SetUpFixture()
{
Db = new TestDatabase();
UserContext = new TestUserContext();
Access = new ProjectAccessService(Db.Context, UserContext, new CapturingLogger<ProjectAccessService>());
Db.Context.Users.Add(new NovellyUser
{
Id = UserContext.UserId!.Value,
UserName = "admin@novelly.test",
Email = "admin@novelly.test",
DisplayName = "Test Admin",
GlobalRole = GlobalRole.Admin
});
Db.Context.SaveChanges();
TagLogs = new CapturingLogger<TagService>();
ProjectLogs = new CapturingLogger<ProjectService>();
@@ -43,21 +58,22 @@ public abstract class ServiceTestFixture
QuestionLogs = new CapturingLogger<OpenQuestionService>();
GenreLogs = new CapturingLogger<GenreService>();
Tags = new TagService(Db.Context, TagLogs, new CreateTagRequestValidator(), new UpdateTagRequestValidator());
Projects = new ProjectService(Db.Context, ProjectLogs, new CreateProjectRequestValidator(), new UpdateProjectRequestValidator());
Tags = new TagService(Db.Context, Access, TagLogs, new CreateTagRequestValidator(), new UpdateTagRequestValidator());
Projects = new ProjectService(
Db.Context, Access, UserContext, ProjectLogs, new CreateProjectRequestValidator(), new UpdateProjectRequestValidator());
Characters = new CharacterService(
Db.Context, Tags, CharacterLogs,
Db.Context, Access, Tags, CharacterLogs,
new CreateCharacterRequestValidator(), new UpdateCharacterRequestValidator(), new CreateRelationshipRequestValidator());
Chapters = new ChapterService(Db.Context, Tags, ChapterLogs, new CreateChapterRequestValidator(), new UpdateChapterRequestValidator());
Chapters = new ChapterService(Db.Context, Access, Tags, ChapterLogs, new CreateChapterRequestValidator(), new UpdateChapterRequestValidator());
Beats = new BeatService(
Db.Context, Tags, BeatLogs,
Db.Context, Access, Tags, BeatLogs,
new CreateBeatRequestValidator(), new UpdateBeatRequestValidator(), new ReorderBeatsRequestValidator(),
new AssignCharacterToBeatsRequestValidator());
Arcs = new CharacterArcService(
Db.Context, ArcLogs,
Db.Context, Access, ArcLogs,
new CreateArcStageRequestValidator(), new UpdateArcStageRequestValidator(), new ReorderArcStagesRequestValidator());
Questions = new OpenQuestionService(
Db.Context, QuestionLogs,
Db.Context, Access, QuestionLogs,
new CreateOpenQuestionRequestValidator(), new UpdateOpenQuestionRequestValidator(), new ResolveOpenQuestionRequestValidator());
Genres = new GenreService(Db.Context, GenreLogs);