Phase 5A Project User access
This commit is contained in:
parent
63597a8f65
commit
90b2212890
@ -39,6 +39,11 @@ public sealed class ProjectsController(IProjectService projects) : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
var projectId = await projects.SaveAsync(model);
|
var projectId = await projects.SaveAsync(model);
|
||||||
|
if (projectId == 0)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
return RedirectToAction(nameof(Details), new { id = projectId });
|
return RedirectToAction(nameof(Details), new { id = projectId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,12 +7,12 @@ namespace PlotLine.Data;
|
|||||||
|
|
||||||
public interface IImportRepository
|
public interface IImportRepository
|
||||||
{
|
{
|
||||||
Task<bool> ProjectTitleExistsAsync(string projectTitle);
|
Task<bool> ProjectTitleExistsAsync(string projectTitle, int userId);
|
||||||
Task<ImportResult> ImportAsync(PlotLineImportPackage package, string? importedProjectTitle);
|
Task<ImportResult> ImportAsync(PlotLineImportPackage package, string? importedProjectTitle, int userId);
|
||||||
Task<IReadOnlyDictionary<string, int>> GetIdentityMaxValuesAsync(IEnumerable<string> tableNames);
|
Task<IReadOnlyDictionary<string, int>> GetIdentityMaxValuesAsync(IEnumerable<string> tableNames);
|
||||||
Task<ProjectBackupRelationshipImportLookups> GetRelationshipImportLookupsAsync();
|
Task<ProjectBackupRelationshipImportLookups> GetRelationshipImportLookupsAsync();
|
||||||
Task<ProjectBackupRelationshipImportDiagnostics> GetRelationshipImportDiagnosticsAsync(int projectId);
|
Task<ProjectBackupRelationshipImportDiagnostics> GetRelationshipImportDiagnosticsAsync(int projectId);
|
||||||
Task<int> CreateProjectShellAsync(string projectName, string? description);
|
Task<int> CreateProjectShellAsync(string projectName, string? description, int userId);
|
||||||
Task ApplyProjectBackupJsonAsync(int projectId, string jsonData);
|
Task ApplyProjectBackupJsonAsync(int projectId, string jsonData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,16 +20,20 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
|
|||||||
{
|
{
|
||||||
private const string ImportMarker = "Imported via JSON Import Phase 1F";
|
private const string ImportMarker = "Imported via JSON Import Phase 1F";
|
||||||
|
|
||||||
public async Task<bool> ProjectTitleExistsAsync(string projectTitle)
|
public async Task<bool> ProjectTitleExistsAsync(string projectTitle, int userId)
|
||||||
{
|
{
|
||||||
using var connection = connectionFactory.CreateConnection();
|
using var connection = connectionFactory.CreateConnection();
|
||||||
var count = await connection.ExecuteScalarAsync<int>(
|
var count = await connection.ExecuteScalarAsync<int>(
|
||||||
"""
|
"""
|
||||||
SELECT COUNT(1)
|
SELECT COUNT(1)
|
||||||
FROM dbo.Projects
|
FROM dbo.Projects p
|
||||||
WHERE IsArchived = 0 AND ProjectName = @ProjectName;
|
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID
|
||||||
|
WHERE p.IsArchived = 0
|
||||||
|
AND p.ProjectName = @ProjectName
|
||||||
|
AND pua.UserID = @UserID
|
||||||
|
AND pua.IsActive = 1;
|
||||||
""",
|
""",
|
||||||
new { ProjectName = projectTitle });
|
new { ProjectName = projectTitle, UserID = userId });
|
||||||
|
|
||||||
return count > 0;
|
return count > 0;
|
||||||
}
|
}
|
||||||
@ -93,12 +97,12 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
|
|||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<int> CreateProjectShellAsync(string projectName, string? description)
|
public async Task<int> CreateProjectShellAsync(string projectName, string? description, int userId)
|
||||||
{
|
{
|
||||||
using var connection = connectionFactory.CreateConnection();
|
using var connection = connectionFactory.CreateConnection();
|
||||||
return await connection.QuerySingleAsync<int>(
|
return await connection.QuerySingleAsync<int>(
|
||||||
"dbo.Project_Save",
|
"dbo.Project_Save",
|
||||||
new { ProjectID = 0, ProjectName = projectName, Description = description },
|
new { ProjectID = 0, ProjectName = projectName, Description = description, UserID = userId },
|
||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,7 +122,7 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
|
|||||||
commandTimeout: 180);
|
commandTimeout: 180);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ImportResult> ImportAsync(PlotLineImportPackage package, string? importedProjectTitle)
|
public async Task<ImportResult> ImportAsync(PlotLineImportPackage package, string? importedProjectTitle, int userId)
|
||||||
{
|
{
|
||||||
using var connection = connectionFactory.CreateConnection();
|
using var connection = connectionFactory.CreateConnection();
|
||||||
connection.Open();
|
connection.Open();
|
||||||
@ -156,7 +160,8 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
|
|||||||
Description = CombineText(
|
Description = CombineText(
|
||||||
package.Project.Description,
|
package.Project.Description,
|
||||||
package.Project.Notes,
|
package.Project.Notes,
|
||||||
ImportMetadata(package))
|
ImportMetadata(package)),
|
||||||
|
UserID = userId
|
||||||
},
|
},
|
||||||
transaction,
|
transaction,
|
||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
|
|||||||
@ -7,11 +7,15 @@ namespace PlotLine.Data;
|
|||||||
public interface IProjectRepository
|
public interface IProjectRepository
|
||||||
{
|
{
|
||||||
Task<IReadOnlyList<Project>> ListAsync();
|
Task<IReadOnlyList<Project>> ListAsync();
|
||||||
|
Task<IReadOnlyList<Project>> ListForUserAsync(int userId);
|
||||||
Task<Project?> GetAsync(int projectId);
|
Task<Project?> GetAsync(int projectId);
|
||||||
|
Task<Project?> GetForUserAsync(int projectId, int userId);
|
||||||
Task<IReadOnlyList<GenreMetricPreset>> ListGenreMetricPresetsAsync();
|
Task<IReadOnlyList<GenreMetricPreset>> ListGenreMetricPresetsAsync();
|
||||||
Task<int> SaveAsync(Project project);
|
Task<int> SaveAsync(Project project);
|
||||||
|
Task<int> SaveAsync(Project project, int userId, string? genreMetricPresetKey);
|
||||||
Task<int> SaveAsync(Project project, string? genreMetricPresetKey);
|
Task<int> SaveAsync(Project project, string? genreMetricPresetKey);
|
||||||
Task ArchiveAsync(int projectId);
|
Task ArchiveAsync(int projectId);
|
||||||
|
Task ArchiveForUserAsync(int projectId, int userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IBookRepository
|
public interface IBookRepository
|
||||||
@ -275,7 +279,9 @@ public interface IScenarioRepository
|
|||||||
public interface IArchiveRepository
|
public interface IArchiveRepository
|
||||||
{
|
{
|
||||||
Task<IReadOnlyList<ArchivedItem>> ListAsync(int? projectId, string? entityType);
|
Task<IReadOnlyList<ArchivedItem>> ListAsync(int? projectId, string? entityType);
|
||||||
|
Task<IReadOnlyList<ArchivedItem>> ListForUserAsync(int? projectId, string? entityType, int userId);
|
||||||
Task<IReadOnlyList<Project>> ListProjectFiltersAsync();
|
Task<IReadOnlyList<Project>> ListProjectFiltersAsync();
|
||||||
|
Task<IReadOnlyList<Project>> ListProjectFiltersForUserAsync(int userId);
|
||||||
Task ArchiveAsync(string entityType, int entityId, string? reason);
|
Task ArchiveAsync(string entityType, int entityId, string? reason);
|
||||||
Task RestoreAsync(string entityType, int entityId);
|
Task RestoreAsync(string entityType, int entityId);
|
||||||
Task<ArchiveDeleteResult> DeleteForeverAsync(string entityType, int entityId, string confirmationText);
|
Task<ArchiveDeleteResult> DeleteForeverAsync(string entityType, int entityId, string confirmationText);
|
||||||
@ -1031,6 +1037,16 @@ public sealed class ArchiveRepository(ISqlConnectionFactory connectionFactory) :
|
|||||||
return rows.ToList();
|
return rows.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<ArchivedItem>> ListForUserAsync(int? projectId, string? entityType, int userId)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
var rows = await connection.QueryAsync<ArchivedItem>(
|
||||||
|
"dbo.Archive_ListForUser",
|
||||||
|
new { ProjectID = projectId, EntityType = entityType, UserID = userId },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
return rows.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IReadOnlyList<Project>> ListProjectFiltersAsync()
|
public async Task<IReadOnlyList<Project>> ListProjectFiltersAsync()
|
||||||
{
|
{
|
||||||
using var connection = connectionFactory.CreateConnection();
|
using var connection = connectionFactory.CreateConnection();
|
||||||
@ -1040,6 +1056,16 @@ public sealed class ArchiveRepository(ISqlConnectionFactory connectionFactory) :
|
|||||||
return rows.ToList();
|
return rows.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<Project>> ListProjectFiltersForUserAsync(int userId)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
var rows = await connection.QueryAsync<Project>(
|
||||||
|
"dbo.Archive_ProjectFilterListForUser",
|
||||||
|
new { UserID = userId },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
return rows.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task ArchiveAsync(string entityType, int entityId, string? reason)
|
public async Task ArchiveAsync(string entityType, int entityId, string? reason)
|
||||||
{
|
{
|
||||||
using var connection = connectionFactory.CreateConnection();
|
using var connection = connectionFactory.CreateConnection();
|
||||||
@ -1145,12 +1171,25 @@ public sealed class ProjectRepository(ISqlConnectionFactory connectionFactory) :
|
|||||||
return rows.ToList();
|
return rows.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<Project>> ListForUserAsync(int userId)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
var rows = await connection.QueryAsync<Project>("dbo.Project_ListForUser", new { UserID = userId }, commandType: CommandType.StoredProcedure);
|
||||||
|
return rows.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<Project?> GetAsync(int projectId)
|
public async Task<Project?> GetAsync(int projectId)
|
||||||
{
|
{
|
||||||
using var connection = connectionFactory.CreateConnection();
|
using var connection = connectionFactory.CreateConnection();
|
||||||
return await connection.QuerySingleOrDefaultAsync<Project>("dbo.Project_Get", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
|
return await connection.QuerySingleOrDefaultAsync<Project>("dbo.Project_Get", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<Project?> GetForUserAsync(int projectId, int userId)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
return await connection.QuerySingleOrDefaultAsync<Project>("dbo.Project_GetForUser", new { ProjectID = projectId, UserID = userId }, commandType: CommandType.StoredProcedure);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IReadOnlyList<GenreMetricPreset>> ListGenreMetricPresetsAsync()
|
public async Task<IReadOnlyList<GenreMetricPreset>> ListGenreMetricPresetsAsync()
|
||||||
{
|
{
|
||||||
using var connection = connectionFactory.CreateConnection();
|
using var connection = connectionFactory.CreateConnection();
|
||||||
@ -1160,12 +1199,18 @@ public sealed class ProjectRepository(ISqlConnectionFactory connectionFactory) :
|
|||||||
|
|
||||||
public Task<int> SaveAsync(Project project) => SaveAsync(project, null);
|
public Task<int> SaveAsync(Project project) => SaveAsync(project, null);
|
||||||
|
|
||||||
|
public Task<int> SaveAsync(Project project, int userId, string? genreMetricPresetKey)
|
||||||
|
=> SaveInternalAsync(project, userId, genreMetricPresetKey);
|
||||||
|
|
||||||
public async Task<int> SaveAsync(Project project, string? genreMetricPresetKey)
|
public async Task<int> SaveAsync(Project project, string? genreMetricPresetKey)
|
||||||
|
=> await SaveInternalAsync(project, null, genreMetricPresetKey);
|
||||||
|
|
||||||
|
private async Task<int> SaveInternalAsync(Project project, int? userId, string? genreMetricPresetKey)
|
||||||
{
|
{
|
||||||
using var connection = connectionFactory.CreateConnection();
|
using var connection = connectionFactory.CreateConnection();
|
||||||
return await connection.QuerySingleAsync<int>(
|
return await connection.QuerySingleAsync<int>(
|
||||||
"dbo.Project_Save",
|
"dbo.Project_Save",
|
||||||
new { project.ProjectID, project.ProjectName, project.Description, GenreMetricPresetKey = genreMetricPresetKey },
|
new { project.ProjectID, project.ProjectName, project.Description, GenreMetricPresetKey = genreMetricPresetKey, UserID = userId },
|
||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1174,6 +1219,12 @@ public sealed class ProjectRepository(ISqlConnectionFactory connectionFactory) :
|
|||||||
using var connection = connectionFactory.CreateConnection();
|
using var connection = connectionFactory.CreateConnection();
|
||||||
await connection.ExecuteAsync("dbo.Project_Archive", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
|
await connection.ExecuteAsync("dbo.Project_Archive", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task ArchiveForUserAsync(int projectId, int userId)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
await connection.ExecuteAsync("dbo.Project_ArchiveForUser", new { ProjectID = projectId, UserID = userId }, commandType: CommandType.StoredProcedure);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class BookRepository(ISqlConnectionFactory connectionFactory) : IBookRepository
|
public sealed class BookRepository(ISqlConnectionFactory connectionFactory) : IBookRepository
|
||||||
|
|||||||
@ -18,7 +18,10 @@ public class Program
|
|||||||
builder.Logging.AddConsole();
|
builder.Logging.AddConsole();
|
||||||
builder.Logging.AddDebug();
|
builder.Logging.AddDebug();
|
||||||
|
|
||||||
builder.Services.AddControllersWithViews();
|
builder.Services.AddControllersWithViews(options =>
|
||||||
|
{
|
||||||
|
options.Filters.Add<ProjectAccessFilter>();
|
||||||
|
});
|
||||||
builder.Services.AddHttpContextAccessor();
|
builder.Services.AddHttpContextAccessor();
|
||||||
builder.Services.AddDistributedMemoryCache();
|
builder.Services.AddDistributedMemoryCache();
|
||||||
builder.Services.AddSession(options =>
|
builder.Services.AddSession(options =>
|
||||||
@ -99,6 +102,7 @@ public class Program
|
|||||||
builder.Services.AddScoped<IArchiveRepository, ArchiveRepository>();
|
builder.Services.AddScoped<IArchiveRepository, ArchiveRepository>();
|
||||||
builder.Services.AddScoped<IAcceptanceRepository, AcceptanceRepository>();
|
builder.Services.AddScoped<IAcceptanceRepository, AcceptanceRepository>();
|
||||||
builder.Services.AddScoped<IImportRepository, ImportRepository>();
|
builder.Services.AddScoped<IImportRepository, ImportRepository>();
|
||||||
|
builder.Services.AddScoped<IProjectAccessRepository, ProjectAccessRepository>();
|
||||||
builder.Services.AddScoped<IProjectService, ProjectService>();
|
builder.Services.AddScoped<IProjectService, ProjectService>();
|
||||||
builder.Services.AddScoped<IBookService, BookService>();
|
builder.Services.AddScoped<IBookService, BookService>();
|
||||||
builder.Services.AddScoped<IChapterService, ChapterService>();
|
builder.Services.AddScoped<IChapterService, ChapterService>();
|
||||||
@ -123,6 +127,7 @@ public class Program
|
|||||||
builder.Services.AddScoped<IArchiveService, ArchiveService>();
|
builder.Services.AddScoped<IArchiveService, ArchiveService>();
|
||||||
builder.Services.AddScoped<IAcceptanceService, AcceptanceService>();
|
builder.Services.AddScoped<IAcceptanceService, AcceptanceService>();
|
||||||
builder.Services.AddScoped<IImportService, ImportService>();
|
builder.Services.AddScoped<IImportService, ImportService>();
|
||||||
|
builder.Services.AddScoped<IProjectAccessService, ProjectAccessService>();
|
||||||
builder.Services.AddSingleton<IHelpService, HelpService>();
|
builder.Services.AddSingleton<IHelpService, HelpService>();
|
||||||
builder.Services.AddSingleton<IHelpMarkdownRenderer, HelpMarkdownRenderer>();
|
builder.Services.AddSingleton<IHelpMarkdownRenderer, HelpMarkdownRenderer>();
|
||||||
builder.Services.AddScoped<IAuthService, AuthService>();
|
builder.Services.AddScoped<IAuthService, AuthService>();
|
||||||
|
|||||||
@ -238,13 +238,14 @@ public interface IAcceptanceService
|
|||||||
Task<Phase1AcceptanceViewModel> GetPhase1ChecklistAsync(int? projectId);
|
Task<Phase1AcceptanceViewModel> GetPhase1ChecklistAsync(int? projectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class ProjectService(IProjectRepository projects, IBookRepository books) : IProjectService
|
public sealed class ProjectService(IProjectRepository projects, IBookRepository books, ICurrentUserService currentUser) : IProjectService
|
||||||
{
|
{
|
||||||
public Task<IReadOnlyList<Project>> ListAsync() => projects.ListAsync();
|
public Task<IReadOnlyList<Project>> ListAsync()
|
||||||
|
=> currentUser.UserId.HasValue ? projects.ListForUserAsync(currentUser.UserId.Value) : Task.FromResult<IReadOnlyList<Project>>([]);
|
||||||
|
|
||||||
public async Task<ProjectDetailViewModel?> GetDetailAsync(int projectId)
|
public async Task<ProjectDetailViewModel?> GetDetailAsync(int projectId)
|
||||||
{
|
{
|
||||||
var project = await projects.GetAsync(projectId);
|
var project = currentUser.UserId.HasValue ? await projects.GetForUserAsync(projectId, currentUser.UserId.Value) : null;
|
||||||
if (project is null)
|
if (project is null)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
@ -266,7 +267,7 @@ public sealed class ProjectService(IProjectRepository projects, IBookRepository
|
|||||||
|
|
||||||
public async Task<ProjectEditViewModel?> GetEditAsync(int projectId)
|
public async Task<ProjectEditViewModel?> GetEditAsync(int projectId)
|
||||||
{
|
{
|
||||||
var project = await projects.GetAsync(projectId);
|
var project = currentUser.UserId.HasValue ? await projects.GetForUserAsync(projectId, currentUser.UserId.Value) : null;
|
||||||
return project is null ? null : new ProjectEditViewModel
|
return project is null ? null : new ProjectEditViewModel
|
||||||
{
|
{
|
||||||
ProjectID = project.ProjectID,
|
ProjectID = project.ProjectID,
|
||||||
@ -283,14 +284,23 @@ public sealed class ProjectService(IProjectRepository projects, IBookRepository
|
|||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<int> SaveAsync(ProjectEditViewModel model) => projects.SaveAsync(new Project
|
public Task<int> SaveAsync(ProjectEditViewModel model)
|
||||||
{
|
{
|
||||||
ProjectID = model.ProjectID,
|
if (!currentUser.UserId.HasValue)
|
||||||
ProjectName = model.ProjectName,
|
{
|
||||||
Description = model.Description
|
return Task.FromResult(0);
|
||||||
}, model.ProjectID == 0 ? model.GenreMetricPresetKey : null);
|
}
|
||||||
|
|
||||||
public Task ArchiveAsync(int projectId) => projects.ArchiveAsync(projectId);
|
return projects.SaveAsync(new Project
|
||||||
|
{
|
||||||
|
ProjectID = model.ProjectID,
|
||||||
|
ProjectName = model.ProjectName,
|
||||||
|
Description = model.Description
|
||||||
|
}, currentUser.UserId.Value, model.ProjectID == 0 ? model.GenreMetricPresetKey : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ArchiveAsync(int projectId)
|
||||||
|
=> currentUser.UserId.HasValue ? projects.ArchiveForUserAsync(projectId, currentUser.UserId.Value) : Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class BookService(IProjectRepository projects, IBookRepository books, IChapterRepository chapters) : IBookService
|
public sealed class BookService(IProjectRepository projects, IBookRepository books, IChapterRepository chapters) : IBookService
|
||||||
@ -2390,23 +2400,52 @@ public sealed class WriterWorkspaceService(
|
|||||||
IProjectRepository projects,
|
IProjectRepository projects,
|
||||||
IBookRepository books,
|
IBookRepository books,
|
||||||
IChapterRepository chapters,
|
IChapterRepository chapters,
|
||||||
IWriterWorkspaceRepository writerWorkspace) : IWriterWorkspaceService
|
IWriterWorkspaceRepository writerWorkspace,
|
||||||
|
ICurrentUserService currentUser) : IWriterWorkspaceService
|
||||||
{
|
{
|
||||||
public async Task<WriterDashboardViewModel> GetDashboardAsync(int? projectId)
|
public async Task<WriterDashboardViewModel> GetDashboardAsync(int? projectId)
|
||||||
{
|
{
|
||||||
var projectRows = await projects.ListAsync();
|
var projectRows = currentUser.UserId.HasValue
|
||||||
|
? await projects.ListForUserAsync(currentUser.UserId.Value)
|
||||||
|
: Array.Empty<Project>();
|
||||||
|
var projectIds = projectId.HasValue
|
||||||
|
? projectRows.Where(project => project.ProjectID == projectId.Value).Select(project => project.ProjectID).ToList()
|
||||||
|
: projectRows.Select(project => project.ProjectID).ToList();
|
||||||
|
|
||||||
return new WriterDashboardViewModel
|
return new WriterDashboardViewModel
|
||||||
{
|
{
|
||||||
ProjectID = projectId,
|
ProjectID = projectId,
|
||||||
ProjectOptions = projectRows.Select(x => new SelectListItem(x.ProjectName, x.ProjectID.ToString(), x.ProjectID == projectId)).ToList(),
|
ProjectOptions = projectRows.Select(x => new SelectListItem(x.ProjectName, x.ProjectID.ToString(), x.ProjectID == projectId)).ToList(),
|
||||||
ContinueWriting = await writerWorkspace.GetWriterDashboardScenesAsync(projectId, "ContinueWriting"),
|
ContinueWriting = await GetDashboardScenesAsync(projectIds, "ContinueWriting"),
|
||||||
NeedsAttention = await writerWorkspace.GetWriterDashboardScenesAsync(projectId, "NeedsAttention"),
|
NeedsAttention = await GetDashboardScenesAsync(projectIds, "NeedsAttention"),
|
||||||
RevisionQueue = await writerWorkspace.GetWriterDashboardScenesAsync(projectId, "RevisionQueue"),
|
RevisionQueue = await GetDashboardScenesAsync(projectIds, "RevisionQueue"),
|
||||||
PolishingQueue = await writerWorkspace.GetWriterDashboardScenesAsync(projectId, "PolishingQueue"),
|
PolishingQueue = await GetDashboardScenesAsync(projectIds, "PolishingQueue"),
|
||||||
StoryHealthReminders = await writerWorkspace.GetWriterDashboardRemindersAsync(projectId)
|
StoryHealthReminders = await GetDashboardRemindersAsync(projectIds)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<IReadOnlyList<WriterDashboardScene>> GetDashboardScenesAsync(IReadOnlyList<int> projectIds, string queue)
|
||||||
|
{
|
||||||
|
var rows = new List<WriterDashboardScene>();
|
||||||
|
foreach (var accessibleProjectId in projectIds)
|
||||||
|
{
|
||||||
|
rows.AddRange(await writerWorkspace.GetWriterDashboardScenesAsync(accessibleProjectId, queue));
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IReadOnlyList<StoryBibleAttentionItem>> GetDashboardRemindersAsync(IReadOnlyList<int> projectIds)
|
||||||
|
{
|
||||||
|
var rows = new List<StoryBibleAttentionItem>();
|
||||||
|
foreach (var accessibleProjectId in projectIds)
|
||||||
|
{
|
||||||
|
rows.AddRange(await writerWorkspace.GetWriterDashboardRemindersAsync(accessibleProjectId));
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<ChapterWorkflowViewModel?> GetChapterWorkflowAsync(int chapterId)
|
public async Task<ChapterWorkflowViewModel?> GetChapterWorkflowAsync(int chapterId)
|
||||||
{
|
{
|
||||||
var chapter = await chapters.GetAsync(chapterId);
|
var chapter = await chapters.GetAsync(chapterId);
|
||||||
@ -2949,7 +2988,7 @@ public sealed class ScenarioService(IProjectRepository projects, IBookRepository
|
|||||||
public Task ArchiveAsync(int scenarioId) => scenarios.ArchiveAsync(scenarioId);
|
public Task ArchiveAsync(int scenarioId) => scenarios.ArchiveAsync(scenarioId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class ArchiveService(IArchiveRepository archives) : IArchiveService
|
public sealed class ArchiveService(IArchiveRepository archives, ICurrentUserService currentUser) : IArchiveService
|
||||||
{
|
{
|
||||||
private static readonly (string Value, string Label)[] EntityTypes =
|
private static readonly (string Value, string Label)[] EntityTypes =
|
||||||
[
|
[
|
||||||
@ -2969,7 +3008,9 @@ public sealed class ArchiveService(IArchiveRepository archives) : IArchiveServic
|
|||||||
public async Task<ArchivedItemsViewModel> GetArchivedItemsAsync(int? projectId, string? entityType)
|
public async Task<ArchivedItemsViewModel> GetArchivedItemsAsync(int? projectId, string? entityType)
|
||||||
{
|
{
|
||||||
var normalisedType = EntityTypes.Any(x => x.Value == entityType) ? entityType : null;
|
var normalisedType = EntityTypes.Any(x => x.Value == entityType) ? entityType : null;
|
||||||
var projects = await archives.ListProjectFiltersAsync();
|
var projects = currentUser.UserId.HasValue
|
||||||
|
? await archives.ListProjectFiltersForUserAsync(currentUser.UserId.Value)
|
||||||
|
: Array.Empty<Project>();
|
||||||
return new ArchivedItemsViewModel
|
return new ArchivedItemsViewModel
|
||||||
{
|
{
|
||||||
ProjectID = projectId,
|
ProjectID = projectId,
|
||||||
@ -2979,7 +3020,9 @@ public sealed class ArchiveService(IArchiveRepository archives) : IArchiveServic
|
|||||||
project.ProjectID.ToString(),
|
project.ProjectID.ToString(),
|
||||||
project.ProjectID == projectId)).ToList(),
|
project.ProjectID == projectId)).ToList(),
|
||||||
EntityTypeOptions = EntityTypes.Select(type => new SelectListItem(type.Label, type.Value, type.Value == normalisedType)).ToList(),
|
EntityTypeOptions = EntityTypes.Select(type => new SelectListItem(type.Label, type.Value, type.Value == normalisedType)).ToList(),
|
||||||
Items = await archives.ListAsync(projectId, normalisedType)
|
Items = currentUser.UserId.HasValue
|
||||||
|
? await archives.ListForUserAsync(projectId, normalisedType, currentUser.UserId.Value)
|
||||||
|
: Array.Empty<ArchivedItem>()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2993,11 +3036,13 @@ public sealed class ArchiveService(IArchiveRepository archives) : IArchiveServic
|
|||||||
archives.DeleteForeverAsync(entityType, entityId, confirmationText);
|
archives.DeleteForeverAsync(entityType, entityId, confirmationText);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class AcceptanceService(IProjectRepository projects, IAcceptanceRepository acceptance) : IAcceptanceService
|
public sealed class AcceptanceService(IProjectRepository projects, IAcceptanceRepository acceptance, ICurrentUserService currentUser) : IAcceptanceService
|
||||||
{
|
{
|
||||||
public async Task<Phase1AcceptanceViewModel> GetPhase1ChecklistAsync(int? projectId)
|
public async Task<Phase1AcceptanceViewModel> GetPhase1ChecklistAsync(int? projectId)
|
||||||
{
|
{
|
||||||
var projectRows = await projects.ListAsync();
|
var projectRows = currentUser.UserId.HasValue
|
||||||
|
? await projects.ListForUserAsync(currentUser.UserId.Value)
|
||||||
|
: Array.Empty<Project>();
|
||||||
var selectedProject = projectId.HasValue
|
var selectedProject = projectId.HasValue
|
||||||
? projectRows.FirstOrDefault(x => x.ProjectID == projectId.Value)
|
? projectRows.FirstOrDefault(x => x.ProjectID == projectId.Value)
|
||||||
: projectRows.FirstOrDefault();
|
: projectRows.FirstOrDefault();
|
||||||
|
|||||||
@ -14,7 +14,7 @@ public interface IImportService
|
|||||||
Task<string> BuildValidationReportAsync(ImportIndexViewModel model);
|
Task<string> BuildValidationReportAsync(ImportIndexViewModel model);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class ImportService(IImportRepository imports, ILogger<ImportService> logger) : IImportService
|
public sealed class ImportService(IImportRepository imports, ICurrentUserService currentUser, ILogger<ImportService> logger) : IImportService
|
||||||
{
|
{
|
||||||
private const int TitleMaxLength = 200;
|
private const int TitleMaxLength = 200;
|
||||||
private const int RelativeTimeMaxLength = 200;
|
private const int RelativeTimeMaxLength = 200;
|
||||||
@ -58,6 +58,16 @@ public sealed class ImportService(IImportRepository imports, ILogger<ImportServi
|
|||||||
|
|
||||||
public async Task<ImportResult> ImportAsync(ImportIndexViewModel model)
|
public async Task<ImportResult> ImportAsync(ImportIndexViewModel model)
|
||||||
{
|
{
|
||||||
|
var userId = currentUser.UserId;
|
||||||
|
if (!userId.HasValue)
|
||||||
|
{
|
||||||
|
return new ImportResult
|
||||||
|
{
|
||||||
|
Succeeded = false,
|
||||||
|
Message = "Sign in before importing a project."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
model.JsonText = await ReadJsonTextAsync(model);
|
model.JsonText = await ReadJsonTextAsync(model);
|
||||||
var formatValidation = new ImportValidationResult();
|
var formatValidation = new ImportValidationResult();
|
||||||
var format = DetectImportFormat(model.JsonText, formatValidation);
|
var format = DetectImportFormat(model.JsonText, formatValidation);
|
||||||
@ -65,7 +75,7 @@ public sealed class ImportService(IImportRepository imports, ILogger<ImportServi
|
|||||||
&& TryReadProjectBackup(model.JsonText, out var backup, out var backupValidation)
|
&& TryReadProjectBackup(model.JsonText, out var backup, out var backupValidation)
|
||||||
&& backup is not null)
|
&& backup is not null)
|
||||||
{
|
{
|
||||||
return await ImportProjectBackupAsync(model, backup, backupValidation);
|
return await ImportProjectBackupAsync(model, backup, backupValidation, userId.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (format == ProjectImportFormat.Unsupported)
|
if (format == ProjectImportFormat.Unsupported)
|
||||||
@ -129,7 +139,7 @@ public sealed class ImportService(IImportRepository imports, ILogger<ImportServi
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var timer = Stopwatch.StartNew();
|
var timer = Stopwatch.StartNew();
|
||||||
var result = await imports.ImportAsync(preview.Package, preview.HasDuplicateProjectTitle ? preview.DuplicateImportProjectTitle : null);
|
var result = await imports.ImportAsync(preview.Package, preview.HasDuplicateProjectTitle ? preview.DuplicateImportProjectTitle : null, userId.Value);
|
||||||
timer.Stop();
|
timer.Stop();
|
||||||
logger.LogInformation("JSON import committed in {ElapsedMilliseconds} ms for project {ProjectName}. Created {SceneCount} scenes and skipped/unresolved count {SkippedCount}/{UnresolvedCount}.",
|
logger.LogInformation("JSON import committed in {ElapsedMilliseconds} ms for project {ProjectName}. Created {SceneCount} scenes and skipped/unresolved count {SkippedCount}/{UnresolvedCount}.",
|
||||||
timer.ElapsedMilliseconds,
|
timer.ElapsedMilliseconds,
|
||||||
@ -265,7 +275,8 @@ public sealed class ImportService(IImportRepository imports, ILogger<ImportServi
|
|||||||
}
|
}
|
||||||
|
|
||||||
var duplicate = !string.IsNullOrWhiteSpace(package.Project.Title)
|
var duplicate = !string.IsNullOrWhiteSpace(package.Project.Title)
|
||||||
&& await imports.ProjectTitleExistsAsync(package.Project.Title.Trim());
|
&& currentUser.UserId.HasValue
|
||||||
|
&& await imports.ProjectTitleExistsAsync(package.Project.Title.Trim(), currentUser.UserId.Value);
|
||||||
|
|
||||||
var duplicateTitle = duplicate
|
var duplicateTitle = duplicate
|
||||||
? $"{package.Project.Title.Trim()} (Imported {DateTime.Now:yyyy-MM-dd HH:mm})"
|
? $"{package.Project.Title.Trim()} (Imported {DateTime.Now:yyyy-MM-dd HH:mm})"
|
||||||
@ -302,7 +313,8 @@ public sealed class ImportService(IImportRepository imports, ILogger<ImportServi
|
|||||||
var package = BuildPreviewPackageFromBackup(backup);
|
var package = BuildPreviewPackageFromBackup(backup);
|
||||||
var projectTitle = package.Project.Title;
|
var projectTitle = package.Project.Title;
|
||||||
var duplicate = !string.IsNullOrWhiteSpace(projectTitle)
|
var duplicate = !string.IsNullOrWhiteSpace(projectTitle)
|
||||||
&& await imports.ProjectTitleExistsAsync(projectTitle.Trim());
|
&& currentUser.UserId.HasValue
|
||||||
|
&& await imports.ProjectTitleExistsAsync(projectTitle.Trim(), currentUser.UserId.Value);
|
||||||
var duplicateTitle = duplicate
|
var duplicateTitle = duplicate
|
||||||
? $"{projectTitle.Trim()} (Imported {DateTime.Now:yyyy-MM-dd HH:mm})"
|
? $"{projectTitle.Trim()} (Imported {DateTime.Now:yyyy-MM-dd HH:mm})"
|
||||||
: null;
|
: null;
|
||||||
@ -323,7 +335,7 @@ public sealed class ImportService(IImportRepository imports, ILogger<ImportServi
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ImportResult> ImportProjectBackupAsync(ImportIndexViewModel model, ProjectBackupExport backup, ImportValidationResult validation)
|
private async Task<ImportResult> ImportProjectBackupAsync(ImportIndexViewModel model, ProjectBackupExport backup, ImportValidationResult validation, int userId)
|
||||||
{
|
{
|
||||||
var preview = await BuildProjectBackupPreviewAsync(backup, validation);
|
var preview = await BuildProjectBackupPreviewAsync(backup, validation);
|
||||||
model.Preview = preview;
|
model.Preview = preview;
|
||||||
@ -374,7 +386,7 @@ public sealed class ImportService(IImportRepository imports, ILogger<ImportServi
|
|||||||
var projectName = preview.HasDuplicateProjectTitle
|
var projectName = preview.HasDuplicateProjectTitle
|
||||||
? preview.DuplicateImportProjectTitle!
|
? preview.DuplicateImportProjectTitle!
|
||||||
: preview.Package!.Project.Title;
|
: preview.Package!.Project.Title;
|
||||||
var projectId = await imports.CreateProjectShellAsync(projectName, backup.Project?.Description);
|
var projectId = await imports.CreateProjectShellAsync(projectName, backup.Project?.Description, userId);
|
||||||
var maxIds = await imports.GetIdentityMaxValuesAsync(ProjectBackupIdentityTables);
|
var maxIds = await imports.GetIdentityMaxValuesAsync(ProjectBackupIdentityTables);
|
||||||
var relationshipLookups = await imports.GetRelationshipImportLookupsAsync();
|
var relationshipLookups = await imports.GetRelationshipImportLookupsAsync();
|
||||||
var remapWarnings = new List<string>();
|
var remapWarnings = new List<string>();
|
||||||
|
|||||||
245
PlotLine/Services/ProjectAccessFilter.cs
Normal file
245
PlotLine/Services/ProjectAccessFilter.cs
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using System.Reflection;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
|
||||||
|
namespace PlotLine.Services;
|
||||||
|
|
||||||
|
public sealed class ProjectAccessFilter(IProjectAccessService access, ICurrentUserService currentUser) : IAsyncActionFilter
|
||||||
|
{
|
||||||
|
private static readonly HashSet<string> SkippedControllers = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
"Account",
|
||||||
|
"Home",
|
||||||
|
"Help",
|
||||||
|
"HelpDiagnostics",
|
||||||
|
"HelpDrawer",
|
||||||
|
"ManageAccount"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly Dictionary<string, string> ModelPropertyEntities = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["ProjectID"] = "Project",
|
||||||
|
["ReturnProjectID"] = "Project",
|
||||||
|
["BookID"] = "Book",
|
||||||
|
["ReturnBookID"] = "Book",
|
||||||
|
["InitialBookID"] = "Book",
|
||||||
|
["ChapterID"] = "Chapter",
|
||||||
|
["TargetChapterID"] = "Chapter",
|
||||||
|
["SceneID"] = "Scene",
|
||||||
|
["CurrentSceneID"] = "Scene",
|
||||||
|
["ReturnSelectedSceneID"] = "Scene",
|
||||||
|
["SelectedSceneID"] = "Scene",
|
||||||
|
["AnchorSceneID"] = "Scene",
|
||||||
|
["SourceSceneID"] = "Scene",
|
||||||
|
["TargetSceneID"] = "Scene",
|
||||||
|
["CharacterID"] = "Character",
|
||||||
|
["RelatedCharacterID"] = "Character",
|
||||||
|
["CharacterAID"] = "Character",
|
||||||
|
["CharacterBID"] = "Character",
|
||||||
|
["POVCharacterID"] = "Character",
|
||||||
|
["FocusCharacterID"] = "Character",
|
||||||
|
["StoryAssetID"] = "StoryAsset",
|
||||||
|
["SourceAssetID"] = "StoryAsset",
|
||||||
|
["TargetAssetID"] = "StoryAsset",
|
||||||
|
["LocationID"] = "Location",
|
||||||
|
["ParentLocationID"] = "Location",
|
||||||
|
["FromLocationID"] = "Location",
|
||||||
|
["ToLocationID"] = "Location",
|
||||||
|
["PlotLineID"] = "PlotLine",
|
||||||
|
["ParentPlotLineID"] = "PlotLine",
|
||||||
|
["PlotThreadID"] = "PlotThread",
|
||||||
|
["CharacterRelationshipID"] = "CharacterRelationship",
|
||||||
|
["TimelineViewPresetID"] = "TimelineViewPreset",
|
||||||
|
["ScenarioID"] = "Scenario"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly Dictionary<string, Dictionary<string, string>> ControllerParameterEntities = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["Projects"] = new(StringComparer.OrdinalIgnoreCase) { ["id"] = "Project", ["projectId"] = "Project" },
|
||||||
|
["Books"] = new(StringComparer.OrdinalIgnoreCase) { ["id"] = "Book", ["bookId"] = "Book", ["projectId"] = "Project" },
|
||||||
|
["Chapters"] = new(StringComparer.OrdinalIgnoreCase) { ["id"] = "Chapter", ["chapterId"] = "Chapter", ["bookId"] = "Book" },
|
||||||
|
["Characters"] = new(StringComparer.OrdinalIgnoreCase) { ["id"] = "Character", ["characterId"] = "Character", ["projectId"] = "Project" },
|
||||||
|
["Locations"] = new(StringComparer.OrdinalIgnoreCase) { ["id"] = "Location", ["locationId"] = "Location", ["returnLocationId"] = "Location", ["projectId"] = "Project" },
|
||||||
|
["PlotLines"] = new(StringComparer.OrdinalIgnoreCase) { ["id"] = "PlotLine", ["plotLineId"] = "PlotLine", ["projectId"] = "Project" },
|
||||||
|
["PlotThreads"] = new(StringComparer.OrdinalIgnoreCase) { ["id"] = "PlotThread", ["plotThreadId"] = "PlotThread", ["plotLineId"] = "PlotLine", ["projectId"] = "Project" },
|
||||||
|
["ProjectMetrics"] = new(StringComparer.OrdinalIgnoreCase) { ["projectId"] = "Project" },
|
||||||
|
["ProjectRestorePoints"] = new(StringComparer.OrdinalIgnoreCase) { ["projectId"] = "Project", ["id"] = "ProjectRestorePoint" },
|
||||||
|
["RelationshipMap"] = new(StringComparer.OrdinalIgnoreCase) { ["projectId"] = "Project", ["bookId"] = "Book", ["chapterId"] = "Chapter", ["sceneId"] = "Scene", ["focusCharacterId"] = "Character" },
|
||||||
|
["Scenarios"] = new(StringComparer.OrdinalIgnoreCase) { ["id"] = "Scenario", ["scenarioId"] = "Scenario", ["projectId"] = "Project", ["bookId"] = "Book", ["sceneId"] = "Scene", ["anchorSceneId"] = "Scene", ["targetChapterId"] = "Chapter" },
|
||||||
|
["StoryAssets"] = new(StringComparer.OrdinalIgnoreCase) { ["id"] = "StoryAsset", ["returnAssetId"] = "StoryAsset", ["projectId"] = "Project" },
|
||||||
|
["Timeline"] = new(StringComparer.OrdinalIgnoreCase) { ["id"] = "TimelineViewPreset", ["projectId"] = "Project", ["bookId"] = "Book" },
|
||||||
|
["Writer"] = new(StringComparer.OrdinalIgnoreCase) { ["id"] = "Chapter", ["projectId"] = "Project" },
|
||||||
|
["Warnings"] = new(StringComparer.OrdinalIgnoreCase) { ["id"] = "ContinuityWarning", ["projectId"] = "Project", ["bookId"] = "Book", ["sceneId"] = "Scene" },
|
||||||
|
["Analytics"] = new(StringComparer.OrdinalIgnoreCase) { ["projectId"] = "Project", ["bookId"] = "Book" },
|
||||||
|
["Archives"] = new(StringComparer.OrdinalIgnoreCase) { ["projectId"] = "Project" },
|
||||||
|
["Dynamics"] = new(StringComparer.OrdinalIgnoreCase) { ["projectId"] = "Project", ["bookId"] = "Book", ["sceneId"] = "Scene" },
|
||||||
|
["Exports"] = new(StringComparer.OrdinalIgnoreCase) { ["projectId"] = "Project", ["sceneId"] = "Scene" },
|
||||||
|
["Scenes"] = new(StringComparer.OrdinalIgnoreCase) { ["chapterId"] = "Chapter", ["sceneId"] = "Scene", ["projectId"] = "Project", ["bookId"] = "Book" },
|
||||||
|
["Acceptance"] = new(StringComparer.OrdinalIgnoreCase) { ["projectId"] = "Project" },
|
||||||
|
["StoryBible"] = new(StringComparer.OrdinalIgnoreCase) { ["projectId"] = "Project" }
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly Dictionary<string, Dictionary<string, string>> ActionIdEntities = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["Characters"] = new(StringComparer.OrdinalIgnoreCase) { ["ArchiveRelationship"] = "CharacterRelationship" },
|
||||||
|
["Dynamics"] = new(StringComparer.OrdinalIgnoreCase) { ["Character"] = "Character", ["Knowledge"] = "Character", ["Relationship"] = "CharacterRelationship" },
|
||||||
|
["Exports"] = new(StringComparer.OrdinalIgnoreCase) { ["Character"] = "Character", ["SceneBrief"] = "Scene", ["ChapterBrief"] = "Chapter" },
|
||||||
|
["Locations"] = new(StringComparer.OrdinalIgnoreCase) { ["ArchiveRelationship"] = "LocationRelationship" },
|
||||||
|
["Scenes"] = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["Create"] = "Chapter",
|
||||||
|
["Edit"] = "Scene",
|
||||||
|
["Archive"] = "Scene",
|
||||||
|
["MoveUp"] = "Scene",
|
||||||
|
["MoveDown"] = "Scene",
|
||||||
|
["DeleteThreadEvent"] = "ThreadEvent",
|
||||||
|
["DeleteAssetEvent"] = "AssetEvent",
|
||||||
|
["DeleteSceneAssetLocation"] = "SceneAssetLocation",
|
||||||
|
["DeleteAssetCustodyEvent"] = "AssetCustodyEvent",
|
||||||
|
["DeleteSceneCharacter"] = "SceneCharacter",
|
||||||
|
["DeleteSceneDependency"] = "SceneDependency",
|
||||||
|
["DeleteCharacterKnowledge"] = "CharacterKnowledge",
|
||||||
|
["DeleteRelationshipEvent"] = "RelationshipEvent",
|
||||||
|
["DeleteNote"] = "SceneNote",
|
||||||
|
["DeleteChecklistItem"] = "SceneChecklistItem",
|
||||||
|
["DeleteAttachment"] = "SceneAttachment"
|
||||||
|
},
|
||||||
|
["StoryAssets"] = new(StringComparer.OrdinalIgnoreCase) { ["DeleteDependency"] = "AssetDependency" },
|
||||||
|
["Timeline"] = new(StringComparer.OrdinalIgnoreCase) { ["LoadPreset"] = "TimelineViewPreset", ["DeletePreset"] = "TimelineViewPreset" },
|
||||||
|
["Warnings"] = new(StringComparer.OrdinalIgnoreCase) { ["Dismiss"] = "ContinuityWarning", ["MarkIntentional"] = "ContinuityWarning", ["Restore"] = "ContinuityWarning" }
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly Dictionary<string, string> ArchiveEntityTypes = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["Project"] = "Project",
|
||||||
|
["Book"] = "Book",
|
||||||
|
["Chapter"] = "Chapter",
|
||||||
|
["Scene"] = "Scene",
|
||||||
|
["PlotLine"] = "PlotLine",
|
||||||
|
["PlotThread"] = "PlotThread",
|
||||||
|
["StoryAsset"] = "StoryAsset",
|
||||||
|
["Character"] = "Character",
|
||||||
|
["Location"] = "Location",
|
||||||
|
["Scenario"] = "Scenario",
|
||||||
|
["Relationship"] = "CharacterRelationship",
|
||||||
|
["CharacterRelationship"] = "CharacterRelationship"
|
||||||
|
};
|
||||||
|
|
||||||
|
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
|
||||||
|
{
|
||||||
|
if (!currentUser.IsAuthenticated || context.ActionDescriptor is not ControllerActionDescriptor descriptor)
|
||||||
|
{
|
||||||
|
await next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var controllerName = descriptor.ControllerName;
|
||||||
|
if (SkippedControllers.Contains(controllerName))
|
||||||
|
{
|
||||||
|
await next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var requirements = CollectRequirements(controllerName, descriptor.ActionName, context.ActionArguments);
|
||||||
|
foreach (var requirement in requirements)
|
||||||
|
{
|
||||||
|
var allowed = requirement.EntityType == "Project"
|
||||||
|
? await access.CanAccessProjectAsync(requirement.EntityId)
|
||||||
|
: await access.CanAccessEntityAsync(requirement.EntityType, requirement.EntityId);
|
||||||
|
|
||||||
|
if (!allowed)
|
||||||
|
{
|
||||||
|
context.Result = new NotFoundResult();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await next();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HashSet<AccessRequirement> CollectRequirements(string controllerName, string actionName, IDictionary<string, object?> arguments)
|
||||||
|
{
|
||||||
|
var requirements = new HashSet<AccessRequirement>();
|
||||||
|
var parameterMap = ControllerParameterEntities.GetValueOrDefault(controllerName);
|
||||||
|
var actionIdEntity = ActionIdEntities.GetValueOrDefault(controllerName)?.GetValueOrDefault(actionName);
|
||||||
|
|
||||||
|
if (string.Equals(controllerName, "Archives", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& arguments.TryGetValue("entityType", out var archiveEntityType)
|
||||||
|
&& arguments.TryGetValue("entityId", out var archiveEntityId)
|
||||||
|
&& archiveEntityType is string archiveEntityTypeValue
|
||||||
|
&& ArchiveEntityTypes.TryGetValue(archiveEntityTypeValue, out var archiveAccessEntityType)
|
||||||
|
&& TryGetInt(archiveEntityId, out var archiveId)
|
||||||
|
&& archiveId > 0)
|
||||||
|
{
|
||||||
|
requirements.Add(new AccessRequirement(archiveAccessEntityType, archiveId));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var argument in arguments)
|
||||||
|
{
|
||||||
|
if (TryGetInt(argument.Value, out var id) && id > 0)
|
||||||
|
{
|
||||||
|
if (string.Equals(argument.Key, "id", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(actionIdEntity))
|
||||||
|
{
|
||||||
|
requirements.Add(new AccessRequirement(actionIdEntity, id));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameterMap is not null && parameterMap.TryGetValue(argument.Key, out var entityType))
|
||||||
|
{
|
||||||
|
requirements.Add(new AccessRequirement(entityType, id));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectModelRequirements(argument.Value, requirements);
|
||||||
|
}
|
||||||
|
|
||||||
|
return requirements;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CollectModelRequirements(object? value, HashSet<AccessRequirement> requirements)
|
||||||
|
{
|
||||||
|
if (value is null || value is string || value.GetType().IsPrimitive || value is IFormFile)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value is IEnumerable enumerable && value is not IDictionary)
|
||||||
|
{
|
||||||
|
foreach (var item in enumerable)
|
||||||
|
{
|
||||||
|
CollectModelRequirements(item, requirements);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var property in value.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public))
|
||||||
|
{
|
||||||
|
if (property.GetIndexParameters().Length > 0 || !ModelPropertyEntities.TryGetValue(property.Name, out var entityType))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryGetInt(property.GetValue(value), out var id) && id > 0)
|
||||||
|
{
|
||||||
|
requirements.Add(new AccessRequirement(entityType, id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetInt(object? value, out int id)
|
||||||
|
{
|
||||||
|
id = 0;
|
||||||
|
return value switch
|
||||||
|
{
|
||||||
|
int intValue => (id = intValue) > 0,
|
||||||
|
string stringValue => int.TryParse(stringValue, out id) && id > 0,
|
||||||
|
_ => false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly record struct AccessRequirement(string EntityType, int EntityId);
|
||||||
|
}
|
||||||
76
PlotLine/Services/ProjectAccessServices.cs
Normal file
76
PlotLine/Services/ProjectAccessServices.cs
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
using System.Data;
|
||||||
|
using Dapper;
|
||||||
|
using PlotLine.Data;
|
||||||
|
|
||||||
|
namespace PlotLine.Services;
|
||||||
|
|
||||||
|
public interface IProjectAccessRepository
|
||||||
|
{
|
||||||
|
Task<bool> UserCanAccessProjectAsync(int projectId, int userId);
|
||||||
|
Task<bool> UserCanOwnProjectAsync(int projectId, int userId);
|
||||||
|
Task<bool> UserCanAccessEntityAsync(string entityType, int entityId, int userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IProjectAccessService
|
||||||
|
{
|
||||||
|
Task<bool> CanAccessProjectAsync(int projectId);
|
||||||
|
Task<bool> CanOwnProjectAsync(int projectId);
|
||||||
|
Task<bool> CanAccessEntityAsync(string entityType, int entityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ProjectAccessRepository(ISqlConnectionFactory connectionFactory) : IProjectAccessRepository
|
||||||
|
{
|
||||||
|
public async Task<bool> UserCanAccessProjectAsync(int projectId, int userId)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
return await connection.QuerySingleAsync<bool>(
|
||||||
|
"dbo.ProjectAccess_UserCanAccess",
|
||||||
|
new { ProjectID = projectId, UserID = userId },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> UserCanOwnProjectAsync(int projectId, int userId)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
return await connection.QuerySingleAsync<bool>(
|
||||||
|
"dbo.ProjectAccess_UserCanOwn",
|
||||||
|
new { ProjectID = projectId, UserID = userId },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> UserCanAccessEntityAsync(string entityType, int entityId, int userId)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
return await connection.QuerySingleAsync<bool>(
|
||||||
|
"dbo.ProjectAccess_UserCanAccessEntity",
|
||||||
|
new { EntityType = entityType, EntityID = entityId, UserID = userId },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ProjectAccessService(IProjectAccessRepository access, ICurrentUserService currentUser) : IProjectAccessService
|
||||||
|
{
|
||||||
|
public Task<bool> CanAccessProjectAsync(int projectId)
|
||||||
|
{
|
||||||
|
var userId = currentUser.UserId;
|
||||||
|
return userId.HasValue && projectId > 0
|
||||||
|
? access.UserCanAccessProjectAsync(projectId, userId.Value)
|
||||||
|
: Task.FromResult(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<bool> CanOwnProjectAsync(int projectId)
|
||||||
|
{
|
||||||
|
var userId = currentUser.UserId;
|
||||||
|
return userId.HasValue && projectId > 0
|
||||||
|
? access.UserCanOwnProjectAsync(projectId, userId.Value)
|
||||||
|
: Task.FromResult(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<bool> CanAccessEntityAsync(string entityType, int entityId)
|
||||||
|
{
|
||||||
|
var userId = currentUser.UserId;
|
||||||
|
return userId.HasValue && entityId > 0 && !string.IsNullOrWhiteSpace(entityType)
|
||||||
|
? access.UserCanAccessEntityAsync(entityType, entityId, userId.Value)
|
||||||
|
: Task.FromResult(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
378
PlotLine/Sql/042_Phase5A_ProjectUserAccess.sql
Normal file
378
PlotLine/Sql/042_Phase5A_ProjectUserAccess.sql
Normal file
@ -0,0 +1,378 @@
|
|||||||
|
SET ANSI_NULLS ON;
|
||||||
|
GO
|
||||||
|
SET QUOTED_IDENTIFIER ON;
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF OBJECT_ID(N'dbo.ProjectUserAccess', N'U') IS NULL
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE dbo.ProjectUserAccess
|
||||||
|
(
|
||||||
|
ProjectUserAccessID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_ProjectUserAccess PRIMARY KEY,
|
||||||
|
ProjectID int NOT NULL,
|
||||||
|
UserID int NOT NULL,
|
||||||
|
AccessRole nvarchar(50) NOT NULL,
|
||||||
|
InvitedByUserID int NULL,
|
||||||
|
InvitedDateUTC datetime2 NULL,
|
||||||
|
AcceptedDateUTC datetime2 NULL,
|
||||||
|
IsActive bit NOT NULL CONSTRAINT DF_ProjectUserAccess_IsActive DEFAULT 1,
|
||||||
|
CONSTRAINT FK_ProjectUserAccess_Project FOREIGN KEY (ProjectID) REFERENCES dbo.Projects(ProjectID),
|
||||||
|
CONSTRAINT FK_ProjectUserAccess_User FOREIGN KEY (UserID) REFERENCES dbo.AppUser(UserID),
|
||||||
|
CONSTRAINT FK_ProjectUserAccess_InvitedByUser FOREIGN KEY (InvitedByUserID) REFERENCES dbo.AppUser(UserID),
|
||||||
|
CONSTRAINT CK_ProjectUserAccess_AccessRole CHECK (AccessRole IN (N'Owner', N'Collaborator'))
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'UX_ProjectUserAccess_Project_User_Active' AND object_id = OBJECT_ID(N'dbo.ProjectUserAccess'))
|
||||||
|
CREATE UNIQUE INDEX UX_ProjectUserAccess_Project_User_Active ON dbo.ProjectUserAccess(ProjectID, UserID) WHERE IsActive = 1;
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_ProjectUserAccess_User_Project' AND object_id = OBJECT_ID(N'dbo.ProjectUserAccess'))
|
||||||
|
CREATE INDEX IX_ProjectUserAccess_User_Project ON dbo.ProjectUserAccess(UserID, ProjectID, IsActive, AccessRole);
|
||||||
|
GO
|
||||||
|
|
||||||
|
DECLARE @LegacyOwnerUserID int;
|
||||||
|
SELECT TOP (1) @LegacyOwnerUserID = UserID
|
||||||
|
FROM dbo.AppUser
|
||||||
|
ORDER BY UserID;
|
||||||
|
|
||||||
|
IF @LegacyOwnerUserID IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO dbo.ProjectUserAccess (ProjectID, UserID, AccessRole, AcceptedDateUTC, IsActive)
|
||||||
|
SELECT p.ProjectID, @LegacyOwnerUserID, N'Owner', SYSUTCDATETIME(), 1
|
||||||
|
FROM dbo.Projects p
|
||||||
|
WHERE NOT EXISTS
|
||||||
|
(
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.ProjectUserAccess pua
|
||||||
|
WHERE pua.ProjectID = p.ProjectID
|
||||||
|
AND pua.UserID = @LegacyOwnerUserID
|
||||||
|
AND pua.IsActive = 1
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.ProjectAccess_EnsureOwner
|
||||||
|
@ProjectID int,
|
||||||
|
@UserID int
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
IF NOT EXISTS
|
||||||
|
(
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.ProjectUserAccess
|
||||||
|
WHERE ProjectID = @ProjectID
|
||||||
|
AND UserID = @UserID
|
||||||
|
AND IsActive = 1
|
||||||
|
)
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO dbo.ProjectUserAccess (ProjectID, UserID, AccessRole, AcceptedDateUTC, IsActive)
|
||||||
|
VALUES (@ProjectID, @UserID, N'Owner', SYSUTCDATETIME(), 1);
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.ProjectAccess_UserCanAccess
|
||||||
|
@ProjectID int,
|
||||||
|
@UserID int
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
SELECT CAST(CASE WHEN EXISTS
|
||||||
|
(
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.ProjectUserAccess pua
|
||||||
|
INNER JOIN dbo.Projects p ON p.ProjectID = pua.ProjectID
|
||||||
|
WHERE pua.ProjectID = @ProjectID
|
||||||
|
AND pua.UserID = @UserID
|
||||||
|
AND pua.IsActive = 1
|
||||||
|
AND p.IsArchived = 0
|
||||||
|
)
|
||||||
|
THEN 1 ELSE 0 END AS bit);
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.ProjectAccess_UserCanOwn
|
||||||
|
@ProjectID int,
|
||||||
|
@UserID int
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
SELECT CAST(CASE WHEN EXISTS
|
||||||
|
(
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.ProjectUserAccess pua
|
||||||
|
INNER JOIN dbo.Projects p ON p.ProjectID = pua.ProjectID
|
||||||
|
WHERE pua.ProjectID = @ProjectID
|
||||||
|
AND pua.UserID = @UserID
|
||||||
|
AND pua.AccessRole = N'Owner'
|
||||||
|
AND pua.IsActive = 1
|
||||||
|
AND p.IsArchived = 0
|
||||||
|
)
|
||||||
|
THEN 1 ELSE 0 END AS bit);
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.ProjectAccess_GetProjectIDForEntity
|
||||||
|
@EntityType nvarchar(100),
|
||||||
|
@EntityID int
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
DECLARE @ProjectID int = NULL;
|
||||||
|
|
||||||
|
IF @EntityType = N'Project'
|
||||||
|
SELECT @ProjectID = ProjectID FROM dbo.Projects WHERE ProjectID = @EntityID;
|
||||||
|
ELSE IF @EntityType = N'Book'
|
||||||
|
SELECT @ProjectID = ProjectID FROM dbo.Books WHERE BookID = @EntityID;
|
||||||
|
ELSE IF @EntityType = N'Chapter'
|
||||||
|
SELECT @ProjectID = b.ProjectID FROM dbo.Chapters c INNER JOIN dbo.Books b ON b.BookID = c.BookID WHERE c.ChapterID = @EntityID;
|
||||||
|
ELSE IF @EntityType = N'Scene'
|
||||||
|
SELECT @ProjectID = b.ProjectID FROM dbo.Scenes s INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID INNER JOIN dbo.Books b ON b.BookID = c.BookID WHERE s.SceneID = @EntityID;
|
||||||
|
ELSE IF @EntityType = N'Character'
|
||||||
|
SELECT @ProjectID = ProjectID FROM dbo.Characters WHERE CharacterID = @EntityID;
|
||||||
|
ELSE IF @EntityType = N'SceneCharacter'
|
||||||
|
SELECT @ProjectID = ch.ProjectID FROM dbo.SceneCharacters sc INNER JOIN dbo.Characters ch ON ch.CharacterID = sc.CharacterID WHERE sc.SceneCharacterID = @EntityID;
|
||||||
|
ELSE IF @EntityType = N'CharacterAttributeEvent'
|
||||||
|
SELECT @ProjectID = ch.ProjectID FROM dbo.CharacterAttributeEvents cae INNER JOIN dbo.Characters ch ON ch.CharacterID = cae.CharacterID WHERE cae.CharacterAttributeEventID = @EntityID;
|
||||||
|
ELSE IF @EntityType = N'CharacterKnowledge'
|
||||||
|
SELECT @ProjectID = ch.ProjectID FROM dbo.CharacterKnowledge ck INNER JOIN dbo.Characters ch ON ch.CharacterID = ck.CharacterID WHERE ck.CharacterKnowledgeID = @EntityID;
|
||||||
|
ELSE IF @EntityType = N'CharacterRelationship'
|
||||||
|
SELECT @ProjectID = ProjectID FROM dbo.CharacterRelationships WHERE CharacterRelationshipID = @EntityID;
|
||||||
|
ELSE IF @EntityType = N'RelationshipEvent'
|
||||||
|
SELECT @ProjectID = cr.ProjectID FROM dbo.RelationshipEvents re INNER JOIN dbo.CharacterRelationships cr ON cr.CharacterRelationshipID = re.CharacterRelationshipID WHERE re.RelationshipEventID = @EntityID;
|
||||||
|
ELSE IF @EntityType = N'StoryAsset'
|
||||||
|
SELECT @ProjectID = ProjectID FROM dbo.StoryAssets WHERE StoryAssetID = @EntityID;
|
||||||
|
ELSE IF @EntityType = N'AssetEvent'
|
||||||
|
SELECT @ProjectID = sa.ProjectID FROM dbo.AssetEvents ae INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ae.StoryAssetID WHERE ae.AssetEventID = @EntityID;
|
||||||
|
ELSE IF @EntityType = N'AssetDependency'
|
||||||
|
SELECT @ProjectID = sa.ProjectID FROM dbo.AssetDependencies ad INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ad.SourceAssetID WHERE ad.AssetDependencyID = @EntityID;
|
||||||
|
ELSE IF @EntityType = N'AssetCustodyEvent'
|
||||||
|
SELECT @ProjectID = sa.ProjectID FROM dbo.AssetCustodyEvents ace INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ace.StoryAssetID WHERE ace.AssetCustodyEventID = @EntityID;
|
||||||
|
ELSE IF @EntityType = N'Location'
|
||||||
|
SELECT @ProjectID = ProjectID FROM dbo.Locations WHERE LocationID = @EntityID;
|
||||||
|
ELSE IF @EntityType = N'LocationRelationship'
|
||||||
|
SELECT @ProjectID = l.ProjectID FROM dbo.LocationRelationships lr INNER JOIN dbo.Locations l ON l.LocationID = lr.FromLocationID WHERE lr.LocationRelationshipID = @EntityID;
|
||||||
|
ELSE IF @EntityType = N'SceneAssetLocation'
|
||||||
|
SELECT @ProjectID = l.ProjectID FROM dbo.SceneAssetLocations sal INNER JOIN dbo.Locations l ON l.LocationID = sal.LocationID WHERE sal.SceneAssetLocationID = @EntityID;
|
||||||
|
ELSE IF @EntityType = N'PlotLine'
|
||||||
|
SELECT @ProjectID = ProjectID FROM dbo.PlotLines WHERE PlotLineID = @EntityID;
|
||||||
|
ELSE IF @EntityType = N'PlotThread'
|
||||||
|
SELECT @ProjectID = pl.ProjectID FROM dbo.PlotThreads pt INNER JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID WHERE pt.PlotThreadID = @EntityID;
|
||||||
|
ELSE IF @EntityType = N'ThreadEvent'
|
||||||
|
SELECT @ProjectID = pl.ProjectID FROM dbo.ThreadEvents te INNER JOIN dbo.PlotThreads pt ON pt.PlotThreadID = te.PlotThreadID INNER JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID WHERE te.ThreadEventID = @EntityID;
|
||||||
|
ELSE IF @EntityType = N'SceneDependency'
|
||||||
|
SELECT @ProjectID = ProjectID FROM dbo.SceneDependencies WHERE SceneDependencyID = @EntityID;
|
||||||
|
ELSE IF @EntityType = N'SceneMetricType'
|
||||||
|
SELECT @ProjectID = ProjectID FROM dbo.SceneMetricTypes WHERE MetricTypeID = @EntityID;
|
||||||
|
ELSE IF @EntityType = N'TimelineViewPreset'
|
||||||
|
SELECT @ProjectID = ProjectID FROM dbo.TimelineViewPresets WHERE TimelineViewPresetID = @EntityID;
|
||||||
|
ELSE IF @EntityType = N'ContinuityWarning'
|
||||||
|
SELECT @ProjectID = ProjectID FROM dbo.ContinuityWarnings WHERE ContinuityWarningID = @EntityID;
|
||||||
|
ELSE IF @EntityType = N'Scenario'
|
||||||
|
SELECT @ProjectID = ProjectID FROM dbo.Scenarios WHERE ScenarioID = @EntityID;
|
||||||
|
ELSE IF @EntityType = N'ScenarioWarning'
|
||||||
|
SELECT @ProjectID = sc.ProjectID FROM dbo.ScenarioWarnings sw INNER JOIN dbo.Scenarios sc ON sc.ScenarioID = sw.ScenarioID WHERE sw.ScenarioWarningID = @EntityID;
|
||||||
|
ELSE IF @EntityType = N'ProjectRestorePoint'
|
||||||
|
SELECT @ProjectID = ProjectID FROM dbo.ProjectRestorePoints WHERE ProjectRestorePointID = @EntityID;
|
||||||
|
ELSE IF @EntityType = N'ChapterGoal'
|
||||||
|
SELECT @ProjectID = b.ProjectID FROM dbo.ChapterGoals cg INNER JOIN dbo.Chapters c ON c.ChapterID = cg.ChapterID INNER JOIN dbo.Books b ON b.BookID = c.BookID WHERE cg.ChapterGoalID = @EntityID;
|
||||||
|
ELSE IF @EntityType = N'SceneWorkflow'
|
||||||
|
SELECT @ProjectID = b.ProjectID FROM dbo.SceneWorkflow sw INNER JOIN dbo.Scenes s ON s.SceneID = sw.SceneID INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID INNER JOIN dbo.Books b ON b.BookID = c.BookID WHERE sw.SceneWorkflowID = @EntityID;
|
||||||
|
ELSE IF @EntityType = N'SceneNote'
|
||||||
|
SELECT @ProjectID = b.ProjectID FROM dbo.SceneNotes sn INNER JOIN dbo.Scenes s ON s.SceneID = sn.SceneID INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID INNER JOIN dbo.Books b ON b.BookID = c.BookID WHERE sn.SceneNoteID = @EntityID;
|
||||||
|
ELSE IF @EntityType = N'SceneChecklistItem'
|
||||||
|
SELECT @ProjectID = b.ProjectID FROM dbo.SceneChecklistItems sci INNER JOIN dbo.Scenes s ON s.SceneID = sci.SceneID INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID INNER JOIN dbo.Books b ON b.BookID = c.BookID WHERE sci.SceneChecklistItemID = @EntityID;
|
||||||
|
ELSE IF @EntityType = N'SceneAttachment'
|
||||||
|
SELECT @ProjectID = b.ProjectID FROM dbo.SceneAttachments sa INNER JOIN dbo.Scenes s ON s.SceneID = sa.SceneID INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID INNER JOIN dbo.Books b ON b.BookID = c.BookID WHERE sa.SceneAttachmentID = @EntityID;
|
||||||
|
|
||||||
|
SELECT @ProjectID AS ProjectID;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.ProjectAccess_UserCanAccessEntity
|
||||||
|
@EntityType nvarchar(100),
|
||||||
|
@EntityID int,
|
||||||
|
@UserID int
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
DECLARE @ProjectID int;
|
||||||
|
DECLARE @Project TABLE (ProjectID int NULL);
|
||||||
|
|
||||||
|
INSERT INTO @Project
|
||||||
|
EXEC dbo.ProjectAccess_GetProjectIDForEntity @EntityType = @EntityType, @EntityID = @EntityID;
|
||||||
|
|
||||||
|
SELECT @ProjectID = ProjectID FROM @Project;
|
||||||
|
|
||||||
|
IF @ProjectID IS NULL
|
||||||
|
BEGIN
|
||||||
|
SELECT CAST(0 AS bit);
|
||||||
|
RETURN;
|
||||||
|
END;
|
||||||
|
|
||||||
|
EXEC dbo.ProjectAccess_UserCanAccess @ProjectID = @ProjectID, @UserID = @UserID;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.Project_ListForUser
|
||||||
|
@UserID int
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
SELECT p.ProjectID, p.ProjectName, p.Description, p.CreatedDate, p.UpdatedDate, p.IsArchived
|
||||||
|
FROM dbo.Projects p
|
||||||
|
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID
|
||||||
|
WHERE p.IsArchived = 0
|
||||||
|
AND pua.UserID = @UserID
|
||||||
|
AND pua.IsActive = 1
|
||||||
|
ORDER BY p.ProjectName;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.Project_GetForUser
|
||||||
|
@ProjectID int,
|
||||||
|
@UserID int
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
SELECT p.ProjectID, p.ProjectName, p.Description, p.CreatedDate, p.UpdatedDate, p.IsArchived
|
||||||
|
FROM dbo.Projects p
|
||||||
|
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID
|
||||||
|
WHERE p.ProjectID = @ProjectID
|
||||||
|
AND p.IsArchived = 0
|
||||||
|
AND pua.UserID = @UserID
|
||||||
|
AND pua.IsActive = 1;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.Project_Save
|
||||||
|
@ProjectID int = NULL,
|
||||||
|
@ProjectName nvarchar(200),
|
||||||
|
@Description nvarchar(max) = NULL,
|
||||||
|
@GenreMetricPresetKey nvarchar(50) = NULL,
|
||||||
|
@UserID int = NULL
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
IF @ProjectID IS NULL OR @ProjectID = 0
|
||||||
|
BEGIN
|
||||||
|
INSERT dbo.Projects (ProjectName, Description)
|
||||||
|
VALUES (@ProjectName, @Description);
|
||||||
|
|
||||||
|
SET @ProjectID = CAST(SCOPE_IDENTITY() AS int);
|
||||||
|
|
||||||
|
IF @UserID IS NOT NULL
|
||||||
|
EXEC dbo.ProjectAccess_EnsureOwner @ProjectID = @ProjectID, @UserID = @UserID;
|
||||||
|
|
||||||
|
EXEC dbo.ProjectMetricPreset_Apply @ProjectID = @ProjectID, @GenreMetricPresetKey = @GenreMetricPresetKey;
|
||||||
|
|
||||||
|
SELECT @ProjectID AS ProjectID;
|
||||||
|
RETURN;
|
||||||
|
END;
|
||||||
|
|
||||||
|
UPDATE p
|
||||||
|
SET ProjectName = @ProjectName,
|
||||||
|
Description = @Description,
|
||||||
|
UpdatedDate = SYSUTCDATETIME()
|
||||||
|
FROM dbo.Projects p
|
||||||
|
WHERE p.ProjectID = @ProjectID
|
||||||
|
AND
|
||||||
|
(
|
||||||
|
@UserID IS NULL
|
||||||
|
OR EXISTS
|
||||||
|
(
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.ProjectUserAccess pua
|
||||||
|
WHERE pua.ProjectID = p.ProjectID
|
||||||
|
AND pua.UserID = @UserID
|
||||||
|
AND pua.IsActive = 1
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
SELECT CASE WHEN @@ROWCOUNT = 1 THEN @ProjectID ELSE 0 END AS ProjectID;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.Project_ArchiveForUser
|
||||||
|
@ProjectID int,
|
||||||
|
@UserID int
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
UPDATE p
|
||||||
|
SET IsArchived = 1,
|
||||||
|
UpdatedDate = SYSUTCDATETIME()
|
||||||
|
FROM dbo.Projects p
|
||||||
|
WHERE p.ProjectID = @ProjectID
|
||||||
|
AND EXISTS
|
||||||
|
(
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.ProjectUserAccess pua
|
||||||
|
WHERE pua.ProjectID = p.ProjectID
|
||||||
|
AND pua.UserID = @UserID
|
||||||
|
AND pua.AccessRole = N'Owner'
|
||||||
|
AND pua.IsActive = 1
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.Archive_ListForUser
|
||||||
|
@UserID int,
|
||||||
|
@ProjectID int = NULL,
|
||||||
|
@EntityType nvarchar(80) = NULL
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
DECLARE @Archived TABLE
|
||||||
|
(
|
||||||
|
EntityType nvarchar(80),
|
||||||
|
EntityID int,
|
||||||
|
DisplayName nvarchar(500),
|
||||||
|
ProjectID int,
|
||||||
|
ProjectName nvarchar(200),
|
||||||
|
BookID int NULL,
|
||||||
|
BookTitle nvarchar(200) NULL,
|
||||||
|
ArchivedDate datetime2 NULL,
|
||||||
|
ArchivedReason nvarchar(500) NULL,
|
||||||
|
UpdatedDate datetime2
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO @Archived
|
||||||
|
EXEC dbo.Archive_List @ProjectID = @ProjectID, @EntityType = @EntityType;
|
||||||
|
|
||||||
|
SELECT a.EntityType, a.EntityID, a.DisplayName, a.ProjectID, a.ProjectName, a.BookID, a.BookTitle,
|
||||||
|
a.ArchivedDate, a.ArchivedReason, a.UpdatedDate
|
||||||
|
FROM @Archived a
|
||||||
|
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = a.ProjectID
|
||||||
|
WHERE pua.UserID = @UserID
|
||||||
|
AND pua.IsActive = 1
|
||||||
|
ORDER BY ISNULL(a.ArchivedDate, a.UpdatedDate) DESC, a.EntityType, a.DisplayName;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.Archive_ProjectFilterListForUser
|
||||||
|
@UserID int
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
SELECT p.ProjectID, p.ProjectName, p.Description, p.CreatedDate, p.UpdatedDate, p.IsArchived
|
||||||
|
FROM dbo.Projects p
|
||||||
|
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID
|
||||||
|
WHERE pua.UserID = @UserID
|
||||||
|
AND pua.IsActive = 1
|
||||||
|
ORDER BY p.IsArchived, p.ProjectName;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
Loading…
x
Reference in New Issue
Block a user