Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/server/src/middleware/auth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextFunction, Request, Response } from "express";

import { ApiError } from "#utils/errors";
import { config } from "#config";

import { convertNodeHeadersToWebHeaders, getSessionFromRequestHeaders } from "#auth/index";

Expand Down Expand Up @@ -63,8 +64,7 @@ export async function getSessionUserFromRequest(req: Request): Promise<Authentic
name: user.name ?? null,
};

// 5. Cache result in Redis for 15 minutes (short TTL for security)
await cacheSet(cacheKey, authUser, 900);
await cacheSet(cacheKey, authUser, config.auth.sessionCacheMaxAgeSeconds);

req.authUser = authUser;
return authUser;
Expand Down
6 changes: 5 additions & 1 deletion apps/server/src/utils/authCache.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createHash } from "node:crypto";
import { cacheDel } from "./redis";

import { cacheDel } from "./redis.js";

export function extractStableAuthCookieFingerprint(cookieHeader: string): string | null {
const authCookies = cookieHeader
Expand Down Expand Up @@ -27,13 +28,16 @@ export function extractStableAuthCookieFingerprint(cookieHeader: string): string

export function getSessionCacheKey(cookieHeader: string): string | null {
const fingerprint = extractStableAuthCookieFingerprint(cookieHeader);

if (!fingerprint) return null;

const cookieHash = createHash("md5").update(fingerprint).digest("hex");
return `auth:session:${cookieHash}`;
}

export async function invalidateSessionCache(cookieHeader: string): Promise<void> {
const cacheKey = getSessionCacheKey(cookieHeader);

if (cacheKey) {
await cacheDel(cacheKey);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@ const OverviewHome = () => {
() => DOCUMENT_LIBRARY_SERVER_SNAPSHOT,
);

const totalCount = snapshot.counts.RESUME + snapshot.counts.COVER_LETTER;
const totalCount = Object.values(snapshot.counts).reduce((sum, count) => sum + count, 0);

const resumeCount = snapshot.counts.RESUME;
const coverLetterCount = snapshot.counts.COVER_LETTER;

const recentDocs = snapshot.docs.slice(0, 6);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export function useDocumentsWorkspace() {
);

const { docs, counts } = snapshot;
const totalCount = counts.RESUME + counts.COVER_LETTER;
const totalCount = Object.values(counts).reduce((sum, count) => sum + count, 0);

const bump = useCallback(() => setRefreshKey((key) => key + 1), []);

Expand Down
10 changes: 8 additions & 2 deletions apps/studio/app/(main)/(dashboard)/documents/workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import SyncDetailsModal from "@/components/modals/SyncDetailsModal";
import ShareDocumentModal from "@/components/modals/ShareDocumentModal";
import RenameDocumentModal from "@/components/modals/RenameDocumentModal";

import { DOCUMENT_TYPES } from "@/features/documents/core/document-types";
import { getDocumentDefinition } from "@/features/documents/core/registry";

import { DocumentListRow } from "./components/DocumentListRow";
import { IconToggle } from "./components/DocumentWorkspaceControls";
import { DocumentPreviewCard } from "./components/DocumentPreviewCard";
Expand Down Expand Up @@ -87,8 +90,11 @@ export default function DocumentsWorkspace() {
className="h-10 w-auto min-w-36 rounded-xl px-3 shadow-none"
>
<option value="ALL">All documents ({totalCount})</option>
<option value="RESUME">Resume ({counts.RESUME})</option>
<option value="COVER_LETTER">Cover letter ({counts.COVER_LETTER})</option>
{DOCUMENT_TYPES.map((type) => (
<option key={type} value={type}>
{getDocumentDefinition(type).label} ({counts[type]})
</option>
))}
</Select>

<label className="sr-only" htmlFor="document-sort">
Expand Down
6 changes: 5 additions & 1 deletion apps/studio/app/(main)/(dashboard)/error.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"use client";

import { useRouter } from "next/navigation";

import { Button } from "@veriworkly/ui";

const DashboardSegmentError = ({
Expand All @@ -8,6 +10,8 @@ const DashboardSegmentError = ({
error: Error & { digest?: string };
reset: () => void;
}) => {
const router = useRouter();

return (
<section className="border-border bg-card relative flex h-full items-center justify-center overflow-hidden rounded-3xl border px-6 py-10 sm:px-10">
<div className="bg-accent/10 absolute -top-20 -right-12 h-48 w-48 rounded-full blur-2xl" />
Expand All @@ -31,7 +35,7 @@ const DashboardSegmentError = ({
Try again
</Button>

<Button variant="secondary" onClick={() => (window.location.href = "/")}>
<Button variant="secondary" onClick={() => router.push("/")}>
Go to Dashboard
</Button>
</div>
Expand Down
84 changes: 82 additions & 2 deletions packages/ui/src/components/ui/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,30 @@ interface ModalProps {
children: React.ReactNode;
}

type ModalContentProps = React.HTMLAttributes<HTMLDivElement> & {
titleId?: string;
descriptionId?: string;
};

const FOCUSABLE_SELECTOR = [
"a[href]",
"button:not([disabled])",
"textarea:not([disabled])",
"input:not([disabled])",
"select:not([disabled])",
"[tabindex]:not([tabindex='-1'])",
].join(",");

function ModalRoot({ open, onClose, children }: ModalProps) {
const restoreFocusRef = React.useRef<HTMLElement | null>(null);

React.useEffect(() => {
if (!open) return;

restoreFocusRef.current =
document.activeElement instanceof HTMLElement ? document.activeElement : null;
const previousOverflow = document.body.style.overflow;

const handleKey = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
Expand All @@ -23,7 +43,9 @@ function ModalRoot({ open, onClose, children }: ModalProps) {

return () => {
document.removeEventListener("keydown", handleKey);
document.body.style.overflow = "";
document.body.style.overflow = previousOverflow;
restoreFocusRef.current?.focus({ preventScroll: true });
restoreFocusRef.current = null;
};
}, [open, onClose]);

Expand All @@ -39,11 +61,69 @@ function ModalRoot({ open, onClose, children }: ModalProps) {
);
}

function ModalContent({ children, className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
function ModalContent({
children,
className,
titleId,
descriptionId,
onKeyDown,
...props
}: ModalContentProps) {
const contentRef = React.useRef<HTMLDivElement>(null);

React.useEffect(() => {
const content = contentRef.current;
if (!content) return;

window.requestAnimationFrame(() => {
if (!content.contains(document.activeElement)) {
content.focus({ preventScroll: true });
}
});
}, []);

const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
onKeyDown?.(event);
if (event.defaultPrevented || event.key !== "Tab") return;

const content = contentRef.current;
if (!content) return;

const focusable = Array.from(content.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)).filter(
(element) =>
!element.hasAttribute("disabled") &&
element.tabIndex !== -1 &&
element.offsetParent !== null,
);

if (focusable.length === 0) {
event.preventDefault();
content.focus({ preventScroll: true });
return;
}

const first = focusable[0];
const last = focusable[focusable.length - 1];
const active = document.activeElement;

if (event.shiftKey && active === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && active === last) {
event.preventDefault();
first.focus();
}
};

return (
<div
role="dialog"
tabIndex={-1}
ref={contentRef}
aria-modal="true"
aria-labelledby={titleId}
onKeyDown={handleKeyDown}
aria-describedby={descriptionId}
onClick={(e) => e.stopPropagation()}
className={cn(
"border-border bg-card w-full rounded-t-3xl border p-6 shadow-2xl",
Expand Down
Loading