Fix Styling for kanban board, removed encrpytion for gmail as it was affecting connection

This commit is contained in:
cesnimda
2026-03-21 20:03:50 +01:00
parent 793ed6eb65
commit 2b0e86f0ac
6 changed files with 54 additions and 26 deletions
+18 -1
View File
@@ -5,12 +5,14 @@ using System.Data.Common;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using JobTrackerApi.Models; using JobTrackerApi.Models;
using JobTrackerApi.Services; using JobTrackerApi.Services;
using System.Diagnostics; using System.Diagnostics;
using System.IdentityModel.Tokens.Jwt; using System.IdentityModel.Tokens.Jwt;
using System.Net; using System.Net;
using System.IO;
using System.Security.Cryptography; using System.Security.Cryptography;
using JobTrackerApi.Services.JobImport; using JobTrackerApi.Services.JobImport;
using JobTrackerApi.Services.JobImport.Plugins; using JobTrackerApi.Services.JobImport.Plugins;
@@ -74,7 +76,22 @@ builder.Services.AddCors(options =>
// Add controllers // Add controllers
builder.Services.AddControllers(); 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<RulesHostedService>(); builder.Services.AddHostedService<RulesHostedService>();
builder.Services.AddHostedService<DailyExportHostedService>(); builder.Services.AddHostedService<DailyExportHostedService>();
builder.Services.AddHostedService<JobEnrichmentHostedService>(); builder.Services.AddHostedService<JobEnrichmentHostedService>();
+10 -1
View File
@@ -1,6 +1,7 @@
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Security.Cryptography;
using JobTrackerApi.Data; using JobTrackerApi.Data;
using JobTrackerApi.Models; using JobTrackerApi.Models;
using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.DataProtection;
@@ -230,7 +231,15 @@ public sealed class GmailOAuthService : IGmailOAuthService
return _protector.Unprotect(connection.EncryptedAccessToken); 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 refreshed = await RefreshAccessTokenAsync(refreshToken, cancellationToken);
var accessToken = refreshed.access_token?.Trim(); var accessToken = refreshed.access_token?.Trim();
if (string.IsNullOrWhiteSpace(accessToken)) if (string.IsNullOrWhiteSpace(accessToken))
@@ -59,7 +59,7 @@ function parseRawEmail(raw: string): {
const to = headers["to"]; const to = headers["to"];
const dateRaw = headers["date"]; const dateRaw = headers["date"];
let iso: string | undefined = undefined; let iso: string | undefined;
if (dateRaw) { if (dateRaw) {
const d = new Date(dateRaw); const d = new Date(dateRaw);
if (!Number.isNaN(+d)) iso = d.toISOString(); if (!Number.isNaN(+d)) iso = d.toISOString();
@@ -114,8 +114,12 @@ export default function Correspondence({ jobId }: { jobId: number }) {
}, },
}); });
setGmailMessages(res.data); setGmailMessages(res.data);
} catch { } catch (error: any) {
toast("Failed to load Gmail messages.", "error"); const message =
error?.response?.data && typeof error.response.data === "string"
? error.response.data
: "Failed to load Gmail messages.";
toast(message, "error");
} finally { } finally {
setGmailMessagesLoading(false); setGmailMessagesLoading(false);
} }
@@ -284,10 +288,7 @@ export default function Correspondence({ jobId }: { jobId: number }) {
maxWidth: "80%", maxWidth: "80%",
borderRadius: 3, borderRadius: 3,
p: 1.25, p: 1.25,
border: `1px solid ${alpha( border: `1px solid ${alpha(accent, theme.palette.mode === "dark" ? 0.32 : 0.22)}`,
accent,
theme.palette.mode === "dark" ? 0.32 : 0.22,
)}`,
background: alpha(accent, theme.palette.mode === "dark" ? 0.14 : 0.1), background: alpha(accent, theme.palette.mode === "dark" ? 0.14 : 0.1),
color: "text.primary", color: "text.primary",
}} }}
@@ -302,13 +303,10 @@ export default function Correspondence({ jobId }: { jobId: number }) {
{m.content} {m.content}
</Typography> </Typography>
<Typography <Typography variant="caption" sx={{ display: "block", mt: 0.75, color: "text.secondary" }}>
variant="caption"
sx={{ display: "block", mt: 0.75, color: "text.secondary" }}
>
{isMe ? "Me" : "Company"} {isMe ? "Me" : "Company"}
{m.channel ? ` · ${m.channel}` : ""} {m.channel ? ` - ${m.channel}` : ""}
{m.date ? ` · ${new Date(m.date).toLocaleString()}` : ""} {m.date ? ` - ${new Date(m.date).toLocaleString()}` : ""}
</Typography> </Typography>
</Box> </Box>
</Box> </Box>
@@ -403,7 +401,11 @@ export default function Correspondence({ jobId }: { jobId: number }) {
<Box> <Box>
<Typography sx={{ fontWeight: 800 }}>Google Gmail</Typography> <Typography sx={{ fontWeight: 800 }}>Google Gmail</Typography>
<Typography variant="body2" sx={{ color: "text.secondary" }}> <Typography variant="body2" sx={{ color: "text.secondary" }}>
{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."}
</Typography> </Typography>
</Box> </Box>
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}> <Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
+2 -2
View File
@@ -159,7 +159,7 @@ export default function JobFlowBar({ job, history = [] }: { job: JobApplication
<React.Fragment key={`${item.key}-${item.at.toISOString()}`}> <React.Fragment key={`${item.key}-${item.at.toISOString()}`}>
<Chip <Chip
icon={item.icon as React.ReactElement} icon={item.icon as React.ReactElement}
label={`${item.label} · ${item.at.toLocaleDateString()}`} label={`${item.label} - ${item.at.toLocaleDateString()}`}
sx={{ sx={{
fontWeight: 800, fontWeight: 800,
color: theme.palette.getContrastText(item.color), color: theme.palette.getContrastText(item.color),
@@ -170,7 +170,7 @@ export default function JobFlowBar({ job, history = [] }: { job: JobApplication
/> />
{index < items.length - 1 ? ( {index < items.length - 1 ? (
<Typography sx={{ color: "text.secondary", fontWeight: 900 }}> <Typography sx={{ color: "text.secondary", fontWeight: 900 }}>
? {"->"}
</Typography> </Typography>
) : null} ) : null}
</React.Fragment> </React.Fragment>
@@ -131,19 +131,19 @@ export default function KanbanBoard() {
cursor: "grab", cursor: "grab",
borderRadius: 3, borderRadius: 3,
border: `1px solid ${alpha(c, theme.palette.mode === "dark" ? 0.22 : 0.14)}`, 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)", backdropFilter: "blur(8px)",
color: theme.palette.text.primary, color: "#0f172a",
}} }}
> >
<CardContent sx={{ p: 1.25, "&:last-child": { pb: 1.25 } }}> <CardContent sx={{ p: 1.25, "&:last-child": { pb: 1.25 } }}>
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 1 }}> <Box sx={{ display: "flex", justifyContent: "space-between", gap: 1 }}>
<Typography sx={{ fontWeight: 800, lineHeight: 1.25 }}> <Typography sx={{ fontWeight: 800, lineHeight: 1.25, color: "#0f172a" }}>
{j.company?.name ?? ""} {j.company?.name ?? ""}
</Typography> </Typography>
<IconButton <IconButton
size="small" size="small"
sx={{ color: theme.palette.text.primary }} sx={{ color: "#0f172a" }}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
setMenuJobId(j.id); setMenuJobId(j.id);
@@ -153,12 +153,12 @@ export default function KanbanBoard() {
<MoreHorizIcon fontSize="small" /> <MoreHorizIcon fontSize="small" />
</IconButton> </IconButton>
</Box> </Box>
<Typography variant="body2" sx={{ color: "text.secondary" }}> <Typography variant="body2" sx={{ color: "#475569" }}>
{j.jobTitle} {j.jobTitle}
</Typography> </Typography>
<Box sx={{ display: "flex", gap: 1, mt: 1, flexWrap: "wrap" }}> <Box sx={{ display: "flex", gap: 1, mt: 1, flexWrap: "wrap" }}>
<Chip size="small" label={`${j.daysSince}d`} /> <Chip size="small" label={`${j.daysSince}d`} sx={{ color: "#0f172a", backgroundColor: "rgba(148,163,184,0.18)" }} />
{j.location ? <Chip size="small" label={j.location} /> : null} {j.location ? <Chip size="small" label={j.location} sx={{ color: "#0f172a", backgroundColor: "rgba(148,163,184,0.18)" }} /> : null}
</Box> </Box>
</CardContent> </CardContent>
</Card> </Card>
@@ -72,7 +72,7 @@ export default function RemindersView() {
<Box> <Box>
<Typography sx={{ fontWeight: 900, lineHeight: 1.25 }}> <Typography sx={{ fontWeight: 900, lineHeight: 1.25 }}>
{j.company?.name ?? ""}{" "} {j.company?.name ?? ""}{" "}
<span style={{ fontWeight: 700, opacity: 0.7 }}>÷</span>{" "} <span style={{ fontWeight: 700, opacity: 0.7 }}>Â</span>{" "}
{j.jobTitle} {j.jobTitle}
</Typography> </Typography>
<Box sx={{ display: "flex", gap: 1, mt: 0.5, flexWrap: "wrap" }}> <Box sx={{ display: "flex", gap: 1, mt: 0.5, flexWrap: "wrap" }}>