diff --git a/backend/Dockerfile.dev b/backend/Dockerfile.dev index 29a7bea..430ad2f 100644 --- a/backend/Dockerfile.dev +++ b/backend/Dockerfile.dev @@ -28,4 +28,4 @@ COPY . . EXPOSE 8080 # Hot reload: dotnet watch monitors file changes in mounted volumes -ENTRYPOINT ["dotnet", "watch", "run", "--project", "WorkClub.Api/WorkClub.Api.csproj", "--no-restore"] +ENTRYPOINT ["dotnet", "watch", "run", "--project", "WorkClub.Api/WorkClub.Api.csproj"] diff --git a/backend/WorkClub.Infrastructure/Seed/SeedDataService.cs b/backend/WorkClub.Infrastructure/Seed/SeedDataService.cs index 558b117..26dad9a 100644 --- a/backend/WorkClub.Infrastructure/Seed/SeedDataService.cs +++ b/backend/WorkClub.Infrastructure/Seed/SeedDataService.cs @@ -21,6 +21,68 @@ public class SeedDataService { using var scope = _serviceScopeFactory.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); + + await context.Database.MigrateAsync(); + + using var transaction = await context.Database.BeginTransactionAsync(); + + // Enable RLS on all tenant tables + await context.Database.ExecuteSqlRawAsync(@" + ALTER TABLE clubs ENABLE ROW LEVEL SECURITY; + ALTER TABLE clubs FORCE ROW LEVEL SECURITY; + ALTER TABLE members ENABLE ROW LEVEL SECURITY; + ALTER TABLE members FORCE ROW LEVEL SECURITY; + ALTER TABLE work_items ENABLE ROW LEVEL SECURITY; + ALTER TABLE work_items FORCE ROW LEVEL SECURITY; + ALTER TABLE shifts ENABLE ROW LEVEL SECURITY; + ALTER TABLE shifts FORCE ROW LEVEL SECURITY; + ALTER TABLE shift_signups ENABLE ROW LEVEL SECURITY; + ALTER TABLE shift_signups FORCE ROW LEVEL SECURITY; + "); + + // Create tenant isolation policies (idempotent) + await context.Database.ExecuteSqlRawAsync(@" + DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename='clubs' AND policyname='tenant_isolation_policy') THEN + CREATE POLICY tenant_isolation_policy ON clubs FOR ALL USING ((""TenantId"")::text = current_setting('app.current_tenant_id', true)); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename='members' AND policyname='tenant_isolation_policy') THEN + CREATE POLICY tenant_isolation_policy ON members FOR ALL USING ((""TenantId"")::text = current_setting('app.current_tenant_id', true)); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename='work_items' AND policyname='tenant_isolation_policy') THEN + CREATE POLICY tenant_isolation_policy ON work_items FOR ALL USING ((""TenantId"")::text = current_setting('app.current_tenant_id', true)); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename='shifts' AND policyname='tenant_isolation_policy') THEN + CREATE POLICY tenant_isolation_policy ON shifts FOR ALL USING ((""TenantId"")::text = current_setting('app.current_tenant_id', true)); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename='shift_signups' AND policyname='tenant_isolation_policy') THEN + CREATE POLICY tenant_isolation_policy ON shift_signups FOR ALL USING (""ShiftId"" IN (SELECT ""Id"" FROM shifts WHERE (""TenantId"")::text = current_setting('app.current_tenant_id', true))); + END IF; + END $$; + "); + + // Create admin bypass policies (idempotent) + await context.Database.ExecuteSqlRawAsync(@" + DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename='clubs' AND policyname='bypass_rls_policy') THEN + CREATE POLICY bypass_rls_policy ON clubs FOR ALL TO app_admin USING (true); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename='members' AND policyname='bypass_rls_policy') THEN + CREATE POLICY bypass_rls_policy ON members FOR ALL TO app_admin USING (true); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename='work_items' AND policyname='bypass_rls_policy') THEN + CREATE POLICY bypass_rls_policy ON work_items FOR ALL TO app_admin USING (true); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename='shifts' AND policyname='bypass_rls_policy') THEN + CREATE POLICY bypass_rls_policy ON shifts FOR ALL TO app_admin USING (true); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE tablename='shift_signups' AND policyname='bypass_rls_policy') THEN + CREATE POLICY bypass_rls_policy ON shift_signups FOR ALL TO app_admin USING (true); + END IF; + END $$; + "); + + await context.Database.ExecuteSqlRawAsync("SET LOCAL ROLE app_admin"); // Seed clubs if (!context.Clubs.Any()) @@ -437,6 +499,9 @@ public class SeedDataService await context.SaveChangesAsync(); } } + + await context.Database.ExecuteSqlRawAsync("RESET ROLE"); + await transaction.CommitAsync(); } private static string GenerateDeterministicGuid(string input)