Surface project ownership/role on the wire and gate the web UI by it

ProjectResponse now carries OwnerId and a server-resolved MyRole so
Editors/Reviewers see read-only fields and no delete/grant-management
affordances instead of only finding out via a 403 after the fact.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PuBH9QSv66DPXSSBERmPs6
This commit is contained in:
Claude
2026-08-16 15:37:50 +00:00
parent e598c18d67
commit e2a2e69631
19 changed files with 447 additions and 145 deletions
@@ -132,4 +132,51 @@ public class ProjectAccessTests : ServiceTestFixture
var ownerNavigation = Db.Context.Model.FindEntityType(typeof(Project))!.FindNavigation(nameof(Project.Owner))!;
Assert.That(ownerNavigation.ForeignKey.DeleteBehavior, Is.EqualTo(DeleteBehavior.Restrict));
}
[Test]
public async Task The_creator_of_a_novel_sees_their_role_as_owner()
{
var writerId = AsNewUser(GlobalRole.Writer);
var project = await Projects.CreateAsync(new CreateProjectRequest("Owned by writer"));
UserContext.UserId = writerId;
var role = await Access.GetMyRoleAsync(project);
Assert.That(role, Is.EqualTo("Owner"));
}
[Test]
public async Task An_admin_sees_their_role_as_admin_even_on_a_novel_they_do_not_own()
{
AsNewUser(GlobalRole.Writer);
var project = await Projects.CreateAsync(new CreateProjectRequest("Owned by someone else"));
AsAdmin();
var role = await Access.GetMyRoleAsync(project);
Assert.That(role, Is.EqualTo("Admin"));
}
[Test]
public async Task A_user_granted_editor_sees_their_role_as_editor()
{
var project = await Projects.CreateAsync(new CreateProjectRequest("Granted Novel"));
var editorId = AsNewUser(GlobalRole.Reviewer);
GrantProjectRole(project.Id, editorId, ProjectRole.Editor);
var role = await Access.GetMyRoleAsync(project);
Assert.That(role, Is.EqualTo("Editor"));
}
[Test]
public async Task A_user_with_no_access_sees_a_null_role()
{
var project = await Projects.CreateAsync(new CreateProjectRequest("Someone Else's Novel"));
AsNewUser(GlobalRole.Writer);
var role = await Access.GetMyRoleAsync(project);
Assert.That(role, Is.Null);
}
}