From 48cd83b442fd6e6d454f733daf32308de2fb68f2 Mon Sep 17 00:00:00 2001 From: cesnimda Date: Sat, 11 Apr 2026 18:07:20 +0200 Subject: [PATCH] Clean error alerts and harden startup migration --- Data/JobTrackerContext.cs | 1 + JobTrackerApi/Program.cs | 10 +++++-- .../StartupInitializationExtensions.cs | 12 ++++++++- job-tracker-ui/src/api.ts | 27 +++++++++++++------ job-tracker-ui/src/setupTests.ts | 11 +++++--- 5 files changed, 47 insertions(+), 14 deletions(-) diff --git a/Data/JobTrackerContext.cs b/Data/JobTrackerContext.cs index 5b955a6..6067b28 100644 --- a/Data/JobTrackerContext.cs +++ b/Data/JobTrackerContext.cs @@ -59,6 +59,7 @@ namespace JobTrackerApi.Data .HasIndex(c => c.OwnerUserId); modelBuilder.Entity() + .HasQueryFilter(c => CurrentUserId != null && c.JobApplication.OwnerUserId == CurrentUserId) .HasOne(c => c.JobApplication) .WithMany(j => j.Messages) .HasForeignKey(c => c.JobApplicationId) diff --git a/JobTrackerApi/Program.cs b/JobTrackerApi/Program.cs index 852e9bb..30a658c 100644 --- a/JobTrackerApi/Program.cs +++ b/JobTrackerApi/Program.cs @@ -60,11 +60,17 @@ builder.Services.AddDbContext((sp, options) => // Avoid ServerVersion.AutoDetect here because it forces an immediate DB connection // during service registration, which can crash the API if MariaDB is temporarily // unavailable or on a different network during deploy startup. - options.UseMySql(cs, new MariaDbServerVersion(new Version(11, 0, 0))); + options.UseMySql(cs, new MariaDbServerVersion(new Version(11, 0, 0)), mysql => + { + mysql.MigrationsAssembly("JobTrackerApi"); + }); } else { - options.UseSqlite(cs); + options.UseSqlite(cs, sqlite => + { + sqlite.MigrationsAssembly("JobTrackerApi"); + }); } // We create Identity tables on startup in environments where `dotnet ef` isn't available. diff --git a/JobTrackerApi/Services/StartupInitializationExtensions.cs b/JobTrackerApi/Services/StartupInitializationExtensions.cs index b0dd212..ed1be33 100644 --- a/JobTrackerApi/Services/StartupInitializationExtensions.cs +++ b/JobTrackerApi/Services/StartupInitializationExtensions.cs @@ -870,7 +870,17 @@ public static class StartupInitializationExtensions } } - db.Database.Migrate(); + try + { + using var migrationScope = app.Services.CreateScope(); + var migrationDb = migrationScope.ServiceProvider.GetRequiredService(); + migrationDb.Database.Migrate(); + } + catch (Exception ex) + { + app.Logger.LogError(ex, "Database migration failed during startup initialization."); + throw; + } // Optional: seed an initial admin user for local username/password login. // Set Auth:AdminEmail and Auth:AdminPassword to enable. diff --git a/job-tracker-ui/src/api.ts b/job-tracker-ui/src/api.ts index 4bdc6d8..682c842 100644 --- a/job-tracker-ui/src/api.ts +++ b/job-tracker-ui/src/api.ts @@ -1,26 +1,37 @@ import axios from "axios"; import { clearAuthClientState, getCsrfToken } from "./auth"; +function looksLikeHtml(value: string) { + return /<\s*html\b|<\s*body\b|<\s*head\b|<\s*title\b|<\s*!doctype\b/i.test(value); +} + +function sanitizeServerMessage(value: string, fallback: string) { + const text = value.trim(); + if (!text) return fallback; + if (looksLikeHtml(text)) return fallback; + return text.length > 300 ? `${text.slice(0, 297).trimEnd()}...` : text; +} + export function getApiErrorMessage(error: any, fallback = "Request failed.") { const data = error?.response?.data; - if (typeof data === "string" && data.trim()) return data.trim(); - if (typeof data?.message === "string" && data.message.trim()) return data.message.trim(); - if (typeof data?.detail === "string" && data.detail.trim()) return data.detail.trim(); - if (typeof data?.title === "string" && data.title.trim()) return data.title.trim(); + if (typeof data === "string" && data.trim()) return sanitizeServerMessage(data, fallback); + if (typeof data?.message === "string" && data.message.trim()) return sanitizeServerMessage(data.message, fallback); + if (typeof data?.detail === "string" && data.detail.trim()) return sanitizeServerMessage(data.detail, fallback); + if (typeof data?.title === "string" && data.title.trim()) return sanitizeServerMessage(data.title, fallback); if (Array.isArray(data?.errors)) { const first = data.errors.find((value: unknown) => typeof value === "string" && value.trim()); - if (first) return first; + if (first) return sanitizeServerMessage(first, fallback); } if (data?.errors && typeof data.errors === "object") { for (const value of Object.values(data.errors)) { if (Array.isArray(value)) { const first = value.find((item: unknown) => typeof item === "string" && item.trim()); - if (first) return first; + if (first) return sanitizeServerMessage(first, fallback); } - if (typeof value === "string" && value.trim()) return value.trim(); + if (typeof value === "string" && value.trim()) return sanitizeServerMessage(value, fallback); } } - if (typeof error?.message === "string" && error.message.trim()) return error.message.trim(); + if (typeof error?.message === "string" && error.message.trim()) return sanitizeServerMessage(error.message, fallback); return fallback; } diff --git a/job-tracker-ui/src/setupTests.ts b/job-tracker-ui/src/setupTests.ts index 8609961..00e6cb3 100644 --- a/job-tracker-ui/src/setupTests.ts +++ b/job-tracker-ui/src/setupTests.ts @@ -10,9 +10,14 @@ jest.mock('./api', () => ({ interceptors: { request: { use: jest.fn() }, response: { use: jest.fn() } }, }, getApiErrorMessage: jest.fn((error: any, fallback?: string) => { - if (typeof error?.response?.data === 'string' && error.response.data.trim()) return error.response.data; - if (typeof error?.message === 'string' && error.message.trim()) return error.message; - return fallback || 'Request failed.'; + const text = typeof error?.response?.data === 'string' && error.response.data.trim() + ? error.response.data.trim() + : typeof error?.message === 'string' && error.message.trim() + ? error.message.trim() + : ''; + if (!text) return fallback || 'Request failed.'; + if (/<\s*html\b|<\s*body\b|<\s*head\b|<\s*title\b|<\s*!doctype\b/i.test(text)) return fallback || 'Request failed.'; + return text.length > 300 ? `${text.slice(0, 297).trimEnd()}...` : text; }), }));