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:
@@ -1,8 +1,9 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Novelly.Api.Projects;
|
||||
|
||||
namespace Novelly.Api.Agent;
|
||||
|
||||
/// <summary>A chat thread between the writer and the embedded agent, scoped to one project.</summary>
|
||||
public class AgentConversation
|
||||
{
|
||||
public Guid Id { get; init; } = Guid.NewGuid();
|
||||
@@ -17,11 +18,6 @@ public class AgentConversation
|
||||
public List<AgentMessage> Messages { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One turn in an agent conversation. Assistant turns may carry a record of the tools
|
||||
/// the agent called, so the UI can show what it changed and the next request can replay
|
||||
/// the turn back to the model.
|
||||
/// </summary>
|
||||
public class AgentMessage
|
||||
{
|
||||
public Guid Id { get; init; } = Guid.NewGuid();
|
||||
@@ -30,20 +26,30 @@ public class AgentMessage
|
||||
|
||||
public AgentRole Role { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Position in the conversation, 0-based. Timestamps are not enough to order a
|
||||
/// transcript: a fast turn can produce two messages inside the same tick.
|
||||
/// </summary>
|
||||
public int Sequence { get; init; }
|
||||
|
||||
/// <summary>The visible text of the turn.</summary>
|
||||
public string Content { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// JSON array of <c>{ name, input, result }</c> objects describing tool calls made
|
||||
/// during this turn. Null on user turns and on assistant turns that used no tools.
|
||||
/// </summary>
|
||||
public string? ToolCallsJson { get; init; }
|
||||
|
||||
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public class AgentConversationEntityTypeConfiguration : IEntityTypeConfiguration<AgentConversation>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<AgentConversation> entity)
|
||||
{
|
||||
entity.Property(c => c.Title).IsRequired().HasMaxLength(200);
|
||||
entity.HasMany(c => c.Messages).WithOne(m => m.Conversation!)
|
||||
.HasForeignKey(m => m.ConversationId).OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
|
||||
public class AgentMessageEntityTypeConfiguration : IEntityTypeConfiguration<AgentMessage>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<AgentMessage> entity)
|
||||
{
|
||||
entity.Property(m => m.Role).HasConversion<string>().HasMaxLength(16);
|
||||
entity.HasIndex(m => new { m.ConversationId, m.Sequence }).IsUnique();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user