What Changed Added scene-owned occupancy data:Scenes.FloorPlanID Scenes.InitialFloorPlanFloorID new SceneFloorPlanOccupancy table Added Occupancy section to the Scene Inspector with AJAX save/refresh. Added Floor Plan editor Structure / Occupancy mode. Occupancy mode shows linked scenes, previous/next scene controls, and character/asset tokens inside assigned locations. Character tokens use image thumbnail, then initials fallback. Asset tokens do the same, smaller. No Features, no new floor-plan architecture, no transition changes. Files Changed [CoreModels.cs](C:/Source/PlotLine/PlotLine/Models/CoreModels.cs) [CoreViewModels.cs](C:/Source/PlotLine/PlotLine/ViewModels/CoreViewModels.cs) [Repositories.cs](C:/Source/PlotLine/PlotLine/Data/Repositories.cs) [CoreServices.cs](C:/Source/PlotLine/PlotLine/Services/CoreServices.cs) [ScenesController.cs](C:/Source/PlotLine/PlotLine/Controllers/ScenesController.cs) [Program.cs](C:/Source/PlotLine/PlotLine/Program.cs) [Edit.cshtml](C:/Source/PlotLine/PlotLine/Views/FloorPlans/Edit.cshtml) [_SceneInspector.cshtml](C:/Source/PlotLine/PlotLine/Views/Scenes/_SceneInspector.cshtml) [_SceneOccupancy.cshtml](C:/Source/PlotLine/PlotLine/Views/Scenes/_SceneOccupancy.cshtml) [site.js](C:/Source/PlotLine/PlotLine/wwwroot/js/site.js) [site.css](C:/Source/PlotLine/PlotLine/wwwroot/css/site.css) minified CSS/JS updated. Database Added new migration only: [080_SceneFloorPlanOccupancy.sql](C:/Source/PlotLine/PlotLine/Sql/080_SceneFloorPlanOccupancy.sql) Applied locally. Added constraints/FKs and filtered unique indexes for one character/asset occupancy row per scene. Stored Procedures Added/Updated Scene_Save Scene_Get Scene_FloorPlanLink_Save Scene_ListByFloorPlan FloorPlanFloor_ListByPlan FloorPlanBlock_ListByPlan SceneFloorPlanOccupancy_ListByScene SceneFloorPlanOccupancy_ListByFloorPlan SceneFloorPlanOccupancy_Save Test Data Permanent Calendar House seed data added: Book: The Calendar House Chapters 1-3 14 scenes Characters: Ellie, Grant, Stephen, Housekeeper, Gardener Assets: Brass Key, Journal, Candle, Map, Letter 45 occupancy assignments Verification dotnet build PlotLine.slnx passes with 0 warnings/errors. Migration applied locally. Local app boots and returns HTTP 200. DB checks confirm 14 linked scenes and 45 occupancy rows. Manual browser click-through of the authenticated UI still needs a human pass for the AJAX controls and visual token placement.
213 lines
12 KiB
C#
213 lines
12 KiB
C#
using System.Security.Claims;
|
|
using Microsoft.AspNetCore.Authentication;
|
|
using PlotLine.Data;
|
|
using PlotLine.Models;
|
|
using PlotLine.Services;
|
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
|
using Microsoft.AspNetCore.DataProtection;
|
|
using Microsoft.Extensions.FileProviders;
|
|
|
|
namespace PlotLine;
|
|
|
|
public class Program
|
|
{
|
|
public static void Main(string[] args)
|
|
{
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
builder.Logging.ClearProviders();
|
|
builder.Logging.AddConsole();
|
|
builder.Logging.AddDebug();
|
|
|
|
builder.Services.AddControllersWithViews(options =>
|
|
{
|
|
options.Filters.Add<ProjectAccessFilter>();
|
|
});
|
|
builder.Services.AddHttpContextAccessor();
|
|
builder.Services.AddDistributedMemoryCache();
|
|
builder.Services.AddSession(options =>
|
|
{
|
|
options.IdleTimeout = TimeSpan.FromMinutes(10);
|
|
options.Cookie.HttpOnly = true;
|
|
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
|
|
options.Cookie.SameSite = SameSiteMode.Lax;
|
|
});
|
|
builder.Services.Configure<EmailSettings>(builder.Configuration.GetSection("EmailSettings"));
|
|
builder.Services.Configure<StripeSettings>(builder.Configuration.GetSection("Stripe"));
|
|
builder.Services.Configure<StorageSettings>(builder.Configuration.GetSection("Storage"));
|
|
builder.Services.AddDataProtection()
|
|
.PersistKeysToFileSystem(new DirectoryInfo(Path.Combine(builder.Environment.ContentRootPath, "App_Data", "DataProtectionKeys")));
|
|
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
|
.AddCookie(options =>
|
|
{
|
|
options.Cookie.Name = "PlotLine.Auth";
|
|
options.LoginPath = "/Account/Login";
|
|
options.LogoutPath = "/Account/Logout";
|
|
options.AccessDeniedPath = "/Account/AccessDenied";
|
|
options.SlidingExpiration = true;
|
|
options.ExpireTimeSpan = TimeSpan.FromDays(14);
|
|
options.Cookie.HttpOnly = true;
|
|
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
|
|
options.Cookie.SameSite = SameSiteMode.Lax;
|
|
options.Events = new CookieAuthenticationEvents
|
|
{
|
|
OnValidatePrincipal = async context =>
|
|
{
|
|
var userIdValue = context.Principal?.FindFirstValue(ClaimTypes.NameIdentifier);
|
|
var securityStampValue = context.Principal?.FindFirstValue("PlotLine.SecurityStamp");
|
|
if (!int.TryParse(userIdValue, out var userId) || !Guid.TryParse(securityStampValue, out var securityStamp))
|
|
{
|
|
context.RejectPrincipal();
|
|
await context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
|
return;
|
|
}
|
|
|
|
using var scope = context.HttpContext.RequestServices.CreateScope();
|
|
var users = scope.ServiceProvider.GetRequiredService<IUserRepository>();
|
|
var user = await users.GetByIdAsync(userId);
|
|
var isLocked = user?.IsLocked == true && (!user.LockoutEndUtc.HasValue || user.LockoutEndUtc > DateTime.UtcNow);
|
|
if (user is null || user.SecurityStamp != securityStamp || isLocked)
|
|
{
|
|
context.RejectPrincipal();
|
|
await context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
|
}
|
|
}
|
|
};
|
|
});
|
|
builder.Services.AddAuthorization(options =>
|
|
{
|
|
options.AddPolicy("AdminOnly", policy =>
|
|
{
|
|
policy.RequireAuthenticatedUser();
|
|
policy.RequireAssertion(context =>
|
|
{
|
|
var email = context.User.FindFirstValue(ClaimTypes.Email);
|
|
var allowedEmails = builder.Configuration.GetSection("Admin:AllowedEmails").Get<string[]>() ?? [];
|
|
return !string.IsNullOrWhiteSpace(email)
|
|
&& allowedEmails.Any(allowed => string.Equals(allowed, email, StringComparison.OrdinalIgnoreCase));
|
|
});
|
|
});
|
|
});
|
|
builder.Services.AddSingleton<ISqlConnectionFactory, SqlConnectionFactory>();
|
|
builder.Services.AddScoped<IUserRepository, UserRepository>();
|
|
builder.Services.AddScoped<IAuthenticationRepository, AuthenticationRepository>();
|
|
builder.Services.AddScoped<IEmailQueueRepository, EmailQueueRepository>();
|
|
builder.Services.AddScoped<IUserFileRepository, UserFileRepository>();
|
|
builder.Services.AddScoped<IProjectRepository, ProjectRepository>();
|
|
builder.Services.AddScoped<IBookRepository, BookRepository>();
|
|
builder.Services.AddScoped<IChapterRepository, ChapterRepository>();
|
|
builder.Services.AddScoped<ISceneRepository, SceneRepository>();
|
|
builder.Services.AddScoped<ISceneFloorPlanOccupancyRepository, SceneFloorPlanOccupancyRepository>();
|
|
builder.Services.AddScoped<ILookupRepository, LookupRepository>();
|
|
builder.Services.AddScoped<ITimelineRepository, TimelineRepository>();
|
|
builder.Services.AddScoped<ITimelinePresetRepository, TimelinePresetRepository>();
|
|
builder.Services.AddScoped<ITimelineSettingsRepository, TimelineSettingsRepository>();
|
|
builder.Services.AddScoped<ISceneMetricTypeRepository, SceneMetricTypeRepository>();
|
|
builder.Services.AddScoped<IPlotRepository, PlotRepository>();
|
|
builder.Services.AddScoped<IAssetRepository, AssetRepository>();
|
|
builder.Services.AddScoped<ICharacterRepository, CharacterRepository>();
|
|
builder.Services.AddScoped<ILocationRepository, LocationRepository>();
|
|
builder.Services.AddScoped<IFloorPlanRepository, FloorPlanRepository>();
|
|
builder.Services.AddScoped<IWarningRepository, WarningRepository>();
|
|
builder.Services.AddScoped<IContinuityWarningAcknowledgementRepository, ContinuityWarningAcknowledgementRepository>();
|
|
builder.Services.AddScoped<ISceneDependencyRepository, SceneDependencyRepository>();
|
|
builder.Services.AddScoped<IAnalyticsRepository, AnalyticsRepository>();
|
|
builder.Services.AddScoped<IStoryBibleRepository, StoryBibleRepository>();
|
|
builder.Services.AddScoped<IWriterWorkspaceRepository, WriterWorkspaceRepository>();
|
|
builder.Services.AddScoped<IWritingScheduleRepository, WritingScheduleRepository>();
|
|
builder.Services.AddScoped<IExportRepository, ExportRepository>();
|
|
builder.Services.AddScoped<IProjectBackupRepository, ProjectBackupRepository>();
|
|
builder.Services.AddScoped<IProjectRestorePointRepository, ProjectRestorePointRepository>();
|
|
builder.Services.AddScoped<IDynamicsRepository, DynamicsRepository>();
|
|
builder.Services.AddScoped<IScenarioRepository, ScenarioRepository>();
|
|
builder.Services.AddScoped<IArchiveRepository, ArchiveRepository>();
|
|
builder.Services.AddScoped<IAcceptanceRepository, AcceptanceRepository>();
|
|
builder.Services.AddScoped<IImportRepository, ImportRepository>();
|
|
builder.Services.AddScoped<IWordCompanionRepository, WordCompanionRepository>();
|
|
builder.Services.AddScoped<IProjectAccessRepository, ProjectAccessRepository>();
|
|
builder.Services.AddScoped<IProjectCollaborationRepository, ProjectCollaborationRepository>();
|
|
builder.Services.AddScoped<IProjectActivityRepository, ProjectActivityRepository>();
|
|
builder.Services.AddScoped<ISubscriptionRepository, SubscriptionRepository>();
|
|
builder.Services.AddScoped<IProjectService, ProjectService>();
|
|
builder.Services.AddScoped<IHardDeleteFileCleanupService, HardDeleteFileCleanupService>();
|
|
builder.Services.AddScoped<IProjectCollaborationService, ProjectCollaborationService>();
|
|
builder.Services.AddScoped<IProjectActivityService, ProjectActivityService>();
|
|
builder.Services.AddScoped<IBookCoverService, BookCoverService>();
|
|
builder.Services.AddScoped<IVisualIdentityImageService, VisualIdentityImageService>();
|
|
builder.Services.AddScoped<IFloorPlanBackgroundImageService, FloorPlanBackgroundImageService>();
|
|
builder.Services.AddScoped<IBookService, BookService>();
|
|
builder.Services.AddScoped<IChapterService, ChapterService>();
|
|
builder.Services.AddScoped<ISceneService, SceneService>();
|
|
builder.Services.AddScoped<IStoryStateService, StoryStateService>();
|
|
builder.Services.AddScoped<ITimelineService, TimelineService>();
|
|
builder.Services.AddScoped<ISceneMetricTypeService, SceneMetricTypeService>();
|
|
builder.Services.AddScoped<ISceneMetricValueService, SceneMetricValueService>();
|
|
builder.Services.AddScoped<IPlotService, PlotService>();
|
|
builder.Services.AddScoped<IAssetService, AssetService>();
|
|
builder.Services.AddScoped<ICharacterService, CharacterService>();
|
|
builder.Services.AddScoped<ILocationService, LocationService>();
|
|
builder.Services.AddScoped<IFloorPlanService, FloorPlanService>();
|
|
builder.Services.AddScoped<IContinuityValidationService, ContinuityValidationService>();
|
|
builder.Services.AddScoped<IAnalyticsService, AnalyticsService>();
|
|
builder.Services.AddScoped<IStoryBibleService, StoryBibleService>();
|
|
builder.Services.AddScoped<IRelationshipMapService, RelationshipMapService>();
|
|
builder.Services.AddScoped<IWriterWorkspaceService, WriterWorkspaceService>();
|
|
builder.Services.AddScoped<IWritingScheduleService, WritingScheduleService>();
|
|
builder.Services.AddScoped<IStorageService, StorageService>();
|
|
builder.Services.AddSingleton<IUploadStorageService, UploadStorageService>();
|
|
builder.Services.AddScoped<IExportService, ExportService>();
|
|
builder.Services.AddScoped<IProjectExportService, ProjectExportService>();
|
|
builder.Services.AddScoped<IProjectRestorePointService, ProjectRestorePointService>();
|
|
builder.Services.AddScoped<IProjectRestoreService, ProjectRestoreService>();
|
|
builder.Services.AddScoped<IDynamicsService, DynamicsService>();
|
|
builder.Services.AddScoped<IScenarioService, ScenarioService>();
|
|
builder.Services.AddScoped<IArchiveService, ArchiveService>();
|
|
builder.Services.AddScoped<IAcceptanceService, AcceptanceService>();
|
|
builder.Services.AddScoped<IImportService, ImportService>();
|
|
builder.Services.AddScoped<IWordCompanionService, WordCompanionService>();
|
|
builder.Services.AddScoped<IProjectAccessService, ProjectAccessService>();
|
|
builder.Services.AddScoped<ISubscriptionService, SubscriptionService>();
|
|
builder.Services.AddScoped<IStripeBillingService, StripeBillingService>();
|
|
builder.Services.AddSingleton<IHelpService, HelpService>();
|
|
builder.Services.AddSingleton<IHelpMarkdownRenderer, HelpMarkdownRenderer>();
|
|
builder.Services.AddScoped<IAuthService, AuthService>();
|
|
builder.Services.AddScoped<IAccountDeletionService, AccountDeletionService>();
|
|
builder.Services.AddScoped<IPasswordService, PasswordService>();
|
|
builder.Services.AddScoped<ITwoFactorService, TwoFactorService>();
|
|
builder.Services.AddScoped<IEmailService, EmailService>();
|
|
builder.Services.AddScoped<IQueuedEmailSender, QueuedEmailSender>();
|
|
builder.Services.AddScoped<ICurrentUserService, CurrentUserService>();
|
|
builder.Services.AddHostedService<EmailQueueWorker>();
|
|
|
|
var app = builder.Build();
|
|
|
|
if (!app.Environment.IsDevelopment())
|
|
{
|
|
app.UseExceptionHandler("/Home/Error");
|
|
app.UseHsts();
|
|
}
|
|
|
|
app.UseHttpsRedirection();
|
|
app.UseStaticFiles();
|
|
var uploadStorage = app.Services.GetRequiredService<IUploadStorageService>();
|
|
app.UseStaticFiles(new StaticFileOptions
|
|
{
|
|
FileProvider = new PhysicalFileProvider(uploadStorage.UploadsRootPath),
|
|
RequestPath = "/uploads"
|
|
});
|
|
app.UseRouting();
|
|
|
|
app.UseSession();
|
|
app.UseAuthentication();
|
|
app.UseAuthorization();
|
|
|
|
app.MapStaticAssets();
|
|
app.MapControllerRoute(
|
|
name: "default",
|
|
pattern: "{controller=Home}/{action=Index}/{id?}")
|
|
.WithStaticAssets();
|
|
|
|
app.Run();
|
|
}
|
|
}
|