diff --git a/JobTrackerApi/Program.cs b/JobTrackerApi/Program.cs index 0985f2b..c4ecc85 100644 --- a/JobTrackerApi/Program.cs +++ b/JobTrackerApi/Program.cs @@ -5,12 +5,14 @@ using System.Data.Common; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.DataProtection; using Microsoft.IdentityModel.Tokens; using JobTrackerApi.Models; using JobTrackerApi.Services; using System.Diagnostics; using System.IdentityModel.Tokens.Jwt; using System.Net; +using System.IO; using System.Security.Cryptography; using JobTrackerApi.Services.JobImport; using JobTrackerApi.Services.JobImport.Plugins; @@ -74,7 +76,22 @@ builder.Services.AddCors(options => // Add controllers builder.Services.AddControllers(); -builder.Services.AddDataProtection(); +var dataRoot = (builder.Configuration["Data:Root"] ?? "").Trim(); +if (string.IsNullOrWhiteSpace(dataRoot)) +{ + dataRoot = builder.Environment.ContentRootPath; +} +if (!Path.IsPathRooted(dataRoot)) +{ + dataRoot = Path.Combine(builder.Environment.ContentRootPath, dataRoot); +} +Directory.CreateDirectory(dataRoot); +var dataProtectionKeysPath = Path.Combine(dataRoot, "keys"); +Directory.CreateDirectory(dataProtectionKeysPath); + +builder.Services.AddDataProtection() + .PersistKeysToFileSystem(new DirectoryInfo(dataProtectionKeysPath)) + .SetApplicationName("JobTracker"); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); diff --git a/JobTrackerApi/Services/GmailOAuthService.cs b/JobTrackerApi/Services/GmailOAuthService.cs index 172ec44..87bd27b 100644 --- a/JobTrackerApi/Services/GmailOAuthService.cs +++ b/JobTrackerApi/Services/GmailOAuthService.cs @@ -1,6 +1,7 @@ using System.Net.Http.Headers; using System.Text; using System.Text.Json; +using System.Security.Cryptography; using JobTrackerApi.Data; using JobTrackerApi.Models; using Microsoft.AspNetCore.DataProtection; @@ -230,7 +231,15 @@ public sealed class GmailOAuthService : IGmailOAuthService return _protector.Unprotect(connection.EncryptedAccessToken); } - var refreshToken = _protector.Unprotect(connection.EncryptedRefreshToken); + string refreshToken; + try + { + refreshToken = _protector.Unprotect(connection.EncryptedRefreshToken); + } + catch (CryptographicException) + { + throw new InvalidOperationException("Your stored Gmail connection can no longer be decrypted after a server key change. Disconnect Gmail and connect it again."); + } var refreshed = await RefreshAccessTokenAsync(refreshToken, cancellationToken); var accessToken = refreshed.access_token?.Trim(); if (string.IsNullOrWhiteSpace(accessToken)) diff --git a/job-tracker-ui/src/components/Correspondence.tsx b/job-tracker-ui/src/components/Correspondence.tsx index 469216a..152b60b 100644 --- a/job-tracker-ui/src/components/Correspondence.tsx +++ b/job-tracker-ui/src/components/Correspondence.tsx @@ -59,7 +59,7 @@ function parseRawEmail(raw: string): { const to = headers["to"]; const dateRaw = headers["date"]; - let iso: string | undefined = undefined; + let iso: string | undefined; if (dateRaw) { const d = new Date(dateRaw); if (!Number.isNaN(+d)) iso = d.toISOString(); @@ -114,8 +114,12 @@ export default function Correspondence({ jobId }: { jobId: number }) { }, }); setGmailMessages(res.data); - } catch { - toast("Failed to load Gmail messages.", "error"); + } catch (error: any) { + const message = + error?.response?.data && typeof error.response.data === "string" + ? error.response.data + : "Failed to load Gmail messages."; + toast(message, "error"); } finally { setGmailMessagesLoading(false); } @@ -284,10 +288,7 @@ export default function Correspondence({ jobId }: { jobId: number }) { maxWidth: "80%", borderRadius: 3, p: 1.25, - border: `1px solid ${alpha( - accent, - theme.palette.mode === "dark" ? 0.32 : 0.22, - )}`, + border: `1px solid ${alpha(accent, theme.palette.mode === "dark" ? 0.32 : 0.22)}`, background: alpha(accent, theme.palette.mode === "dark" ? 0.14 : 0.1), color: "text.primary", }} @@ -302,13 +303,10 @@ export default function Correspondence({ jobId }: { jobId: number }) { {m.content} - + {isMe ? "Me" : "Company"} - {m.channel ? ` · ${m.channel}` : ""} - {m.date ? ` · ${new Date(m.date).toLocaleString()}` : ""} + {m.channel ? ` - ${m.channel}` : ""} + {m.date ? ` - ${new Date(m.date).toLocaleString()}` : ""} @@ -403,7 +401,11 @@ export default function Correspondence({ jobId }: { jobId: number }) { Google Gmail - {gmailLoading ? "Checking connection..." : gmailStatus?.connected ? `Connected as ${gmailStatus.gmailAddress}` : "Connect your Gmail account to browse recent emails."} + {gmailLoading + ? "Checking connection..." + : gmailStatus?.connected + ? `Connected as ${gmailStatus.gmailAddress}` + : "Connect your Gmail account to browse recent emails."} diff --git a/job-tracker-ui/src/components/JobFlowBar.tsx b/job-tracker-ui/src/components/JobFlowBar.tsx index e945af5..eb34bbb 100644 --- a/job-tracker-ui/src/components/JobFlowBar.tsx +++ b/job-tracker-ui/src/components/JobFlowBar.tsx @@ -159,7 +159,7 @@ export default function JobFlowBar({ job, history = [] }: { job: JobApplication {index < items.length - 1 ? ( - ? + {"->"} ) : null} diff --git a/job-tracker-ui/src/components/KanbanBoard.tsx b/job-tracker-ui/src/components/KanbanBoard.tsx index 44c1e40..1223c7c 100644 --- a/job-tracker-ui/src/components/KanbanBoard.tsx +++ b/job-tracker-ui/src/components/KanbanBoard.tsx @@ -131,19 +131,19 @@ export default function KanbanBoard() { cursor: "grab", borderRadius: 3, border: `1px solid ${alpha(c, theme.palette.mode === "dark" ? 0.22 : 0.14)}`, - background: theme.palette.mode === "dark" ? "rgba(15,23,42,0.55)" : "rgba(255,255,255,0.8)", + background: theme.palette.mode === "dark" ? "#f8fafc" : "rgba(255,255,255,0.96)", backdropFilter: "blur(8px)", - color: theme.palette.text.primary, + color: "#0f172a", }} > - + {j.company?.name ?? ""} { e.stopPropagation(); setMenuJobId(j.id); @@ -153,12 +153,12 @@ export default function KanbanBoard() { - + {j.jobTitle} - - {j.location ? : null} + + {j.location ? : null} diff --git a/job-tracker-ui/src/components/RemindersView.tsx b/job-tracker-ui/src/components/RemindersView.tsx index 1fc6714..f2f6557 100644 --- a/job-tracker-ui/src/components/RemindersView.tsx +++ b/job-tracker-ui/src/components/RemindersView.tsx @@ -72,7 +72,7 @@ export default function RemindersView() { {j.company?.name ?? ""}{" "} - ·{" "} + Â�{" "} {j.jobTitle}