Fix Styling for kanban board, removed encrpytion for gmail as it was affecting connection
This commit is contained in:
@@ -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>();
|
||||||
|
|||||||
@@ -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" }}>
|
||||||
|
|||||||
@@ -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" }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user