remove excess comments
This commit is contained in:
@@ -114,7 +114,6 @@ function AppLayout(props: { children: any }) {
|
|||||||
if (Math.abs(deltaX) > Math.abs(deltaY)) {
|
if (Math.abs(deltaX) > Math.abs(deltaY)) {
|
||||||
// Mobile: Only left bar
|
// Mobile: Only left bar
|
||||||
if (currentIsMobile) {
|
if (currentIsMobile) {
|
||||||
// Swipe right anywhere - reveal left bar
|
|
||||||
if (deltaX > SWIPE_THRESHOLD) {
|
if (deltaX > SWIPE_THRESHOLD) {
|
||||||
setLeftBarVisible(true);
|
setLeftBarVisible(true);
|
||||||
}
|
}
|
||||||
@@ -124,7 +123,6 @@ function AppLayout(props: { children: any }) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Desktop: Both bars
|
// Desktop: Both bars
|
||||||
// Swipe right anywhere - reveal left bar
|
|
||||||
if (deltaX > SWIPE_THRESHOLD) {
|
if (deltaX > SWIPE_THRESHOLD) {
|
||||||
setLeftBarVisible(true);
|
setLeftBarVisible(true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import LoadingSpinner from "~/components/LoadingSpinner";
|
|||||||
import { getClientCookie } from "~/lib/cookies.client";
|
import { getClientCookie } from "~/lib/cookies.client";
|
||||||
|
|
||||||
export default function DeletionForm() {
|
export default function DeletionForm() {
|
||||||
// State management
|
|
||||||
const [countDown, setCountDown] = createSignal(0);
|
const [countDown, setCountDown] = createSignal(0);
|
||||||
const [emailSent, setEmailSent] = createSignal(false);
|
const [emailSent, setEmailSent] = createSignal(false);
|
||||||
const [error, setError] = createSignal("");
|
const [error, setError] = createSignal("");
|
||||||
@@ -30,7 +29,6 @@ export default function DeletionForm() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check for existing timer on mount
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const timer = getClientCookie("deletionRequestSent");
|
const timer = getClientCookie("deletionRequestSent");
|
||||||
if (timer) {
|
if (timer) {
|
||||||
@@ -46,7 +44,6 @@ export default function DeletionForm() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Form submission handler
|
|
||||||
const sendEmailTrigger = async (e: Event) => {
|
const sendEmailTrigger = async (e: Event) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import ReactionBar from "./ReactionBar";
|
|||||||
export default function CommentBlock(props: CommentBlockProps) {
|
export default function CommentBlock(props: CommentBlockProps) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
// State signals
|
|
||||||
const [commentCollapsed, setCommentCollapsed] = createSignal(false);
|
const [commentCollapsed, setCommentCollapsed] = createSignal(false);
|
||||||
const [showingReactionOptions, setShowingReactionOptions] =
|
const [showingReactionOptions, setShowingReactionOptions] =
|
||||||
createSignal(false);
|
createSignal(false);
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ const RETRY_INTERVAL = 5000;
|
|||||||
export default function CommentSectionWrapper(
|
export default function CommentSectionWrapper(
|
||||||
props: CommentSectionWrapperProps
|
props: CommentSectionWrapperProps
|
||||||
) {
|
) {
|
||||||
// State signals
|
|
||||||
const [allComments, setAllComments] = createSignal<Comment[]>(
|
const [allComments, setAllComments] = createSignal<Comment[]>(
|
||||||
props.allComments
|
props.allComments
|
||||||
);
|
);
|
||||||
@@ -57,14 +56,12 @@ export default function CommentSectionWrapper(
|
|||||||
const [commentBodyForModification, setCommentBodyForModification] =
|
const [commentBodyForModification, setCommentBodyForModification] =
|
||||||
createSignal<string>("");
|
createSignal<string>("");
|
||||||
|
|
||||||
// Non-reactive refs (store without triggering reactivity)
|
|
||||||
let userCommentMap: Map<UserPublicData, number[]> = props.userCommentMap;
|
let userCommentMap: Map<UserPublicData, number[]> = props.userCommentMap;
|
||||||
let deletePromptRef: HTMLDivElement | undefined;
|
let deletePromptRef: HTMLDivElement | undefined;
|
||||||
let modificationPromptRef: HTMLDivElement | undefined;
|
let modificationPromptRef: HTMLDivElement | undefined;
|
||||||
let retryCount = 0;
|
let retryCount = 0;
|
||||||
let socket: WebSocket | undefined;
|
let socket: WebSocket | undefined;
|
||||||
|
|
||||||
// WebSocket connection effect
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const connect = () => {
|
const connect = () => {
|
||||||
if (socket) return;
|
if (socket) return;
|
||||||
@@ -121,7 +118,6 @@ export default function CommentSectionWrapper(
|
|||||||
|
|
||||||
connect();
|
connect();
|
||||||
|
|
||||||
// Cleanup on unmount
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
if (socket?.readyState === WebSocket.OPEN) {
|
if (socket?.readyState === WebSocket.OPEN) {
|
||||||
socket.close();
|
socket.close();
|
||||||
@@ -130,7 +126,6 @@ export default function CommentSectionWrapper(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Helper functions
|
|
||||||
const updateChannel = () => {
|
const updateChannel = () => {
|
||||||
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||||
return;
|
return;
|
||||||
@@ -155,7 +150,6 @@ export default function CommentSectionWrapper(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Comment creation
|
|
||||||
const newComment = async (commentBody: string, parentCommentID?: number) => {
|
const newComment = async (commentBody: string, parentCommentID?: number) => {
|
||||||
setCommentSubmitLoading(true);
|
setCommentSubmitLoading(true);
|
||||||
|
|
||||||
@@ -179,11 +173,9 @@ export default function CommentSectionWrapper(
|
|||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error sending comment creation:", error);
|
console.error("Error sending comment creation:", error);
|
||||||
// Fallback to HTTP API on WebSocket error
|
|
||||||
await fallbackCommentCreation(commentBody, parentCommentID);
|
await fallbackCommentCreation(commentBody, parentCommentID);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback to HTTP API if WebSocket unavailable
|
|
||||||
await fallbackCommentCreation(commentBody, parentCommentID);
|
await fallbackCommentCreation(commentBody, parentCommentID);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -254,7 +246,6 @@ export default function CommentSectionWrapper(
|
|||||||
}
|
}
|
||||||
setAllComments((prevComments) => [...(prevComments || []), newComment]);
|
setAllComments((prevComments) => [...(prevComments || []), newComment]);
|
||||||
|
|
||||||
// Update user comment map
|
|
||||||
const existingIDs = Array.from(userCommentMap.entries()).find(
|
const existingIDs = Array.from(userCommentMap.entries()).find(
|
||||||
([key, _]) =>
|
([key, _]) =>
|
||||||
key.email === userData.email &&
|
key.email === userData.email &&
|
||||||
@@ -272,7 +263,6 @@ export default function CommentSectionWrapper(
|
|||||||
setCommentSubmitLoading(false);
|
setCommentSubmitLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Comment updating
|
|
||||||
const editComment = async (body: string, comment_id: number) => {
|
const editComment = async (body: string, comment_id: number) => {
|
||||||
setCommentEditLoading(true);
|
setCommentEditLoading(true);
|
||||||
|
|
||||||
@@ -375,14 +365,12 @@ export default function CommentSectionWrapper(
|
|||||||
"[deleteComment] WebSocket error, falling back to HTTP:",
|
"[deleteComment] WebSocket error, falling back to HTTP:",
|
||||||
error
|
error
|
||||||
);
|
);
|
||||||
// Fallback to HTTP API on WebSocket error
|
|
||||||
await fallbackCommentDeletion(commentID, commenterID, deletionType);
|
await fallbackCommentDeletion(commentID, commenterID, deletionType);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(
|
||||||
"[deleteComment] WebSocket not available, using HTTP fallback"
|
"[deleteComment] WebSocket not available, using HTTP fallback"
|
||||||
);
|
);
|
||||||
// Fallback to HTTP API if WebSocket unavailable
|
|
||||||
await fallbackCommentDeletion(commentID, commenterID, deletionType);
|
await fallbackCommentDeletion(commentID, commenterID, deletionType);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -407,7 +395,6 @@ export default function CommentSectionWrapper(
|
|||||||
|
|
||||||
console.log("[fallbackCommentDeletion] Success:", result);
|
console.log("[fallbackCommentDeletion] Success:", result);
|
||||||
|
|
||||||
// Handle the deletion response
|
|
||||||
deleteCommentHandler({
|
deleteCommentHandler({
|
||||||
action: "commentDeletionBroadcast",
|
action: "commentDeletionBroadcast",
|
||||||
commentID: commentID,
|
commentID: commentID,
|
||||||
|
|||||||
@@ -84,7 +84,6 @@ async function loadHighlightJS(): Promise<HLJSApi> {
|
|||||||
hljs.registerLanguage("diff", diff.default);
|
hljs.registerLanguage("diff", diff.default);
|
||||||
hljs.registerLanguage("toml", toml.default);
|
hljs.registerLanguage("toml", toml.default);
|
||||||
|
|
||||||
// Also register common aliases
|
|
||||||
hljs.registerLanguage("js", javascript.default);
|
hljs.registerLanguage("js", javascript.default);
|
||||||
hljs.registerLanguage("ts", typescript.default);
|
hljs.registerLanguage("ts", typescript.default);
|
||||||
hljs.registerLanguage("jsx", javascript.default);
|
hljs.registerLanguage("jsx", javascript.default);
|
||||||
@@ -97,18 +96,15 @@ export default function PostBodyClient(props: PostBodyClientProps) {
|
|||||||
let contentRef: HTMLDivElement | undefined;
|
let contentRef: HTMLDivElement | undefined;
|
||||||
const [hljs, setHljs] = createSignal<HLJSApi | null>(null);
|
const [hljs, setHljs] = createSignal<HLJSApi | null>(null);
|
||||||
|
|
||||||
// Process superscript references and enhance the References section
|
|
||||||
const processReferences = () => {
|
const processReferences = () => {
|
||||||
if (!contentRef) return;
|
if (!contentRef) return;
|
||||||
|
|
||||||
const foundRefs = new Map<string, HTMLElement>();
|
const foundRefs = new Map<string, HTMLElement>();
|
||||||
|
|
||||||
// Find all <sup> elements with [n] pattern
|
|
||||||
const supElements = contentRef.querySelectorAll("sup");
|
const supElements = contentRef.querySelectorAll("sup");
|
||||||
|
|
||||||
supElements.forEach((sup) => {
|
supElements.forEach((sup) => {
|
||||||
const text = sup.textContent?.trim() || "";
|
const text = sup.textContent?.trim() || "";
|
||||||
// Match patterns like [1], [2], [a], [*], etc.
|
|
||||||
const match = text.match(/^\[(.+?)\]$/);
|
const match = text.match(/^\[(.+?)\]$/);
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
@@ -116,10 +112,8 @@ export default function PostBodyClient(props: PostBodyClientProps) {
|
|||||||
const refId = `ref-${refNumber}`;
|
const refId = `ref-${refNumber}`;
|
||||||
const refBackId = `ref-${refNumber}-back`;
|
const refBackId = `ref-${refNumber}-back`;
|
||||||
|
|
||||||
// Add ID to the sup element itself for back navigation
|
|
||||||
sup.id = refBackId;
|
sup.id = refBackId;
|
||||||
|
|
||||||
// Replace sup content with a clickable link
|
|
||||||
sup.innerHTML = "";
|
sup.innerHTML = "";
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
link.href = `#${refId}`;
|
link.href = `#${refId}`;
|
||||||
@@ -129,13 +123,11 @@ export default function PostBodyClient(props: PostBodyClientProps) {
|
|||||||
link.style.cssText =
|
link.style.cssText =
|
||||||
"text-decoration: none; font-size: 0.75em; vertical-align: super;";
|
"text-decoration: none; font-size: 0.75em; vertical-align: super;";
|
||||||
|
|
||||||
// Add smooth scroll behavior
|
|
||||||
link.onclick = (e) => {
|
link.onclick = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const target = document.getElementById(refId);
|
const target = document.getElementById(refId);
|
||||||
if (target) {
|
if (target) {
|
||||||
target.scrollIntoView({ behavior: "smooth", block: "center" });
|
target.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||||
// Highlight the reference briefly
|
|
||||||
target.style.backgroundColor = "rgba(137, 180, 250, 0.2)";
|
target.style.backgroundColor = "rgba(137, 180, 250, 0.2)";
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
target.style.backgroundColor = "";
|
target.style.backgroundColor = "";
|
||||||
@@ -147,7 +139,6 @@ export default function PostBodyClient(props: PostBodyClientProps) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Find and enhance the References section
|
|
||||||
const headings = contentRef.querySelectorAll("h2");
|
const headings = contentRef.querySelectorAll("h2");
|
||||||
let referencesSection: HTMLElement | null = null;
|
let referencesSection: HTMLElement | null = null;
|
||||||
|
|
||||||
@@ -158,7 +149,6 @@ export default function PostBodyClient(props: PostBodyClientProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (referencesSection) {
|
if (referencesSection) {
|
||||||
// Style the References heading
|
|
||||||
referencesSection.className = "text-2xl font-bold mb-4 text-text";
|
referencesSection.className = "text-2xl font-bold mb-4 text-text";
|
||||||
|
|
||||||
// Find the parent container and add styling
|
// Find the parent container and add styling
|
||||||
|
|||||||
@@ -44,10 +44,8 @@ import swift from "highlight.js/lib/languages/swift";
|
|||||||
import kotlin from "highlight.js/lib/languages/kotlin";
|
import kotlin from "highlight.js/lib/languages/kotlin";
|
||||||
import dockerfile from "highlight.js/lib/languages/dockerfile";
|
import dockerfile from "highlight.js/lib/languages/dockerfile";
|
||||||
|
|
||||||
// Create lowlight instance with common languages
|
|
||||||
const lowlight = createLowlight(common);
|
const lowlight = createLowlight(common);
|
||||||
|
|
||||||
// Register existing languages
|
|
||||||
lowlight.register("css", css);
|
lowlight.register("css", css);
|
||||||
lowlight.register("js", js);
|
lowlight.register("js", js);
|
||||||
lowlight.register("javascript", js);
|
lowlight.register("javascript", js);
|
||||||
@@ -56,7 +54,6 @@ lowlight.register("typescript", ts);
|
|||||||
lowlight.register("ocaml", ocaml);
|
lowlight.register("ocaml", ocaml);
|
||||||
lowlight.register("rust", rust);
|
lowlight.register("rust", rust);
|
||||||
|
|
||||||
// Register new languages
|
|
||||||
lowlight.register("python", python);
|
lowlight.register("python", python);
|
||||||
lowlight.register("py", python);
|
lowlight.register("py", python);
|
||||||
lowlight.register("java", java);
|
lowlight.register("java", java);
|
||||||
@@ -87,7 +84,6 @@ lowlight.register("kt", kotlin);
|
|||||||
lowlight.register("dockerfile", dockerfile);
|
lowlight.register("dockerfile", dockerfile);
|
||||||
lowlight.register("docker", dockerfile);
|
lowlight.register("docker", dockerfile);
|
||||||
|
|
||||||
// Available languages for selector
|
|
||||||
const AVAILABLE_LANGUAGES = [
|
const AVAILABLE_LANGUAGES = [
|
||||||
{ value: null, label: "Plain Text" },
|
{ value: null, label: "Plain Text" },
|
||||||
{ value: "bash", label: "Bash/Shell" },
|
{ value: "bash", label: "Bash/Shell" },
|
||||||
@@ -115,7 +111,6 @@ const AVAILABLE_LANGUAGES = [
|
|||||||
{ value: "yaml", label: "YAML" }
|
{ value: "yaml", label: "YAML" }
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
// Mermaid diagram templates
|
|
||||||
const MERMAID_TEMPLATES = [
|
const MERMAID_TEMPLATES = [
|
||||||
{
|
{
|
||||||
name: "Flowchart",
|
name: "Flowchart",
|
||||||
@@ -183,7 +178,6 @@ const MERMAID_TEMPLATES = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
// Keyboard shortcuts data
|
|
||||||
interface ShortcutCategory {
|
interface ShortcutCategory {
|
||||||
name: string;
|
name: string;
|
||||||
shortcuts: Array<{
|
shortcuts: Array<{
|
||||||
@@ -271,7 +265,6 @@ const isMac = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// IFrame extension
|
|
||||||
interface IframeOptions {
|
interface IframeOptions {
|
||||||
allowFullscreen: boolean;
|
allowFullscreen: boolean;
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
@@ -472,19 +465,16 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
handleClickOn(view, pos, node, nodePos, event) {
|
handleClickOn(view, pos, node, nodePos, event) {
|
||||||
const target = event.target as HTMLElement;
|
const target = event.target as HTMLElement;
|
||||||
|
|
||||||
// Check if click is on a summary element inside details
|
|
||||||
const summary = target.closest("summary");
|
const summary = target.closest("summary");
|
||||||
if (summary) {
|
if (summary) {
|
||||||
const details = summary.closest('[data-type="details"]');
|
const details = summary.closest('[data-type="details"]');
|
||||||
if (details) {
|
if (details) {
|
||||||
// Toggle the open attribute
|
|
||||||
const isOpen = details.hasAttribute("open");
|
const isOpen = details.hasAttribute("open");
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
details.removeAttribute("open");
|
details.removeAttribute("open");
|
||||||
} else {
|
} else {
|
||||||
details.setAttribute("open", "");
|
details.setAttribute("open", "");
|
||||||
}
|
}
|
||||||
// Also toggle hidden attribute on details content
|
|
||||||
const content = details.querySelector(
|
const content = details.querySelector(
|
||||||
'[data-type="detailsContent"]'
|
'[data-type="detailsContent"]'
|
||||||
);
|
);
|
||||||
@@ -504,7 +494,6 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
onUpdate: ({ editor }) => {
|
onUpdate: ({ editor }) => {
|
||||||
untrack(() => {
|
untrack(() => {
|
||||||
props.updateContent(editor.getHTML());
|
props.updateContent(editor.getHTML());
|
||||||
// Auto-manage references section
|
|
||||||
setTimeout(() => updateReferencesSection(editor), 100);
|
setTimeout(() => updateReferencesSection(editor), 100);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -515,7 +504,6 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
if (hasSelection && !editor.state.selection.empty) {
|
if (hasSelection && !editor.state.selection.empty) {
|
||||||
setShowBubbleMenu(true);
|
setShowBubbleMenu(true);
|
||||||
|
|
||||||
// Position the bubble menu
|
|
||||||
const { view } = editor;
|
const { view } = editor;
|
||||||
const start = view.coordsAtPos(from);
|
const start = view.coordsAtPos(from);
|
||||||
const end = view.coordsAtPos(to);
|
const end = view.coordsAtPos(to);
|
||||||
@@ -530,7 +518,6 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Update editor content when preSet changes (e.g., when data loads), but only if editor exists and content is different
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => props.preSet,
|
() => props.preSet,
|
||||||
@@ -544,14 +531,12 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Auto-manage references section
|
|
||||||
const updateReferencesSection = (editorInstance: any) => {
|
const updateReferencesSection = (editorInstance: any) => {
|
||||||
if (!editorInstance) return;
|
if (!editorInstance) return;
|
||||||
|
|
||||||
const doc = editorInstance.state.doc;
|
const doc = editorInstance.state.doc;
|
||||||
const foundRefs = new Set<string>();
|
const foundRefs = new Set<string>();
|
||||||
|
|
||||||
// Scan document for superscript marks containing [n] patterns
|
|
||||||
doc.descendants((node: any) => {
|
doc.descendants((node: any) => {
|
||||||
if (node.isText && node.marks) {
|
if (node.isText && node.marks) {
|
||||||
const hasSuperscript = node.marks.some(
|
const hasSuperscript = node.marks.some(
|
||||||
@@ -567,7 +552,6 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// If no references found, remove references section if it exists
|
|
||||||
if (foundRefs.size === 0) {
|
if (foundRefs.size === 0) {
|
||||||
let hasReferencesSection = false;
|
let hasReferencesSection = false;
|
||||||
let hrPos = -1;
|
let hrPos = -1;
|
||||||
@@ -581,7 +565,6 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (hasReferencesSection && sectionStartPos > 0) {
|
if (hasReferencesSection && sectionStartPos > 0) {
|
||||||
// Find the HR before References heading
|
|
||||||
doc.nodesBetween(
|
doc.nodesBetween(
|
||||||
Math.max(0, sectionStartPos - 50),
|
Math.max(0, sectionStartPos - 50),
|
||||||
sectionStartPos,
|
sectionStartPos,
|
||||||
@@ -592,7 +575,6 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Delete from HR to end of document
|
|
||||||
if (hrPos >= 0) {
|
if (hrPos >= 0) {
|
||||||
const tr = editorInstance.state.tr;
|
const tr = editorInstance.state.tr;
|
||||||
tr.delete(hrPos, doc.content.size);
|
tr.delete(hrPos, doc.content.size);
|
||||||
@@ -602,7 +584,6 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert Set to sorted array
|
|
||||||
const refNumbers = Array.from(foundRefs).sort((a, b) => {
|
const refNumbers = Array.from(foundRefs).sort((a, b) => {
|
||||||
const numA = parseInt(a);
|
const numA = parseInt(a);
|
||||||
const numB = parseInt(b);
|
const numB = parseInt(b);
|
||||||
@@ -612,7 +593,6 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
return a.localeCompare(b);
|
return a.localeCompare(b);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if References section already exists
|
|
||||||
let referencesHeadingPos = -1;
|
let referencesHeadingPos = -1;
|
||||||
let existingRefs = new Set<string>();
|
let existingRefs = new Set<string>();
|
||||||
|
|
||||||
@@ -620,7 +600,6 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
if (node.type.name === "heading" && node.textContent === "References") {
|
if (node.type.name === "heading" && node.textContent === "References") {
|
||||||
referencesHeadingPos = pos;
|
referencesHeadingPos = pos;
|
||||||
}
|
}
|
||||||
// Check for existing reference list items
|
|
||||||
if (referencesHeadingPos >= 0 && node.type.name === "paragraph") {
|
if (referencesHeadingPos >= 0 && node.type.name === "paragraph") {
|
||||||
const match = node.textContent.match(/^\[(.+?)\]/);
|
const match = node.textContent.match(/^\[(.+?)\]/);
|
||||||
if (match) {
|
if (match) {
|
||||||
@@ -629,7 +608,6 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// If references section doesn't exist, create it
|
|
||||||
if (referencesHeadingPos === -1) {
|
if (referencesHeadingPos === -1) {
|
||||||
const content: any[] = [
|
const content: any[] = [
|
||||||
{ type: "horizontalRule" },
|
{ type: "horizontalRule" },
|
||||||
@@ -640,7 +618,6 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
// Add each reference as a paragraph
|
|
||||||
refNumbers.forEach((refNum) => {
|
refNumbers.forEach((refNum) => {
|
||||||
content.push({
|
content.push({
|
||||||
type: "paragraph",
|
type: "paragraph",
|
||||||
@@ -658,7 +635,6 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Insert at the end
|
|
||||||
const tr = editorInstance.state.tr;
|
const tr = editorInstance.state.tr;
|
||||||
tr.insert(
|
tr.insert(
|
||||||
doc.content.size,
|
doc.content.size,
|
||||||
@@ -666,11 +642,9 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
);
|
);
|
||||||
editorInstance.view.dispatch(tr);
|
editorInstance.view.dispatch(tr);
|
||||||
} else {
|
} else {
|
||||||
// Update existing references section - add missing refs
|
|
||||||
const newRefs = refNumbers.filter((ref) => !existingRefs.has(ref));
|
const newRefs = refNumbers.filter((ref) => !existingRefs.has(ref));
|
||||||
|
|
||||||
if (newRefs.length > 0) {
|
if (newRefs.length > 0) {
|
||||||
// Find position after References heading to insert new refs
|
|
||||||
let insertPos = referencesHeadingPos;
|
let insertPos = referencesHeadingPos;
|
||||||
doc.nodesBetween(
|
doc.nodesBetween(
|
||||||
referencesHeadingPos,
|
referencesHeadingPos,
|
||||||
@@ -781,17 +755,12 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
const { from } = instance.state.selection;
|
const { from } = instance.state.selection;
|
||||||
instance.chain().focus().insertContent(content).run();
|
instance.chain().focus().insertContent(content).run();
|
||||||
|
|
||||||
// Move cursor to the paragraph inside detailsContent
|
|
||||||
// Structure: details (from+1) > detailsSummary > detailsContent > paragraph
|
|
||||||
// We need to position inside the paragraph which is roughly from + title.length + 3 nodes deep
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const { state } = instance;
|
const { state } = instance;
|
||||||
let targetPos = from;
|
let targetPos = from;
|
||||||
|
|
||||||
// Navigate through the document to find the paragraph inside detailsContent
|
|
||||||
state.doc.nodesBetween(from, from + 200, (node, pos) => {
|
state.doc.nodesBetween(from, from + 200, (node, pos) => {
|
||||||
if (node.type.name === "detailsContent") {
|
if (node.type.name === "detailsContent") {
|
||||||
// Position cursor at the start of the first child (paragraph)
|
|
||||||
targetPos = pos + 1;
|
targetPos = pos + 1;
|
||||||
return false; // Stop iteration
|
return false; // Stop iteration
|
||||||
}
|
}
|
||||||
@@ -811,7 +780,6 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
|
|
||||||
instance.chain().focus().toggleCodeBlock().run();
|
instance.chain().focus().toggleCodeBlock().run();
|
||||||
|
|
||||||
// If language specified, update the node attributes
|
|
||||||
if (language) {
|
if (language) {
|
||||||
instance.chain().updateAttributes("codeBlock", { language }).run();
|
instance.chain().updateAttributes("codeBlock", { language }).run();
|
||||||
}
|
}
|
||||||
@@ -869,7 +837,6 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
const { state } = instance;
|
const { state } = instance;
|
||||||
const { selection } = state;
|
const { selection } = state;
|
||||||
|
|
||||||
// Find the row node
|
|
||||||
let rowNode = null;
|
let rowNode = null;
|
||||||
let depth = 0;
|
let depth = 0;
|
||||||
for (let d = selection.$anchor.depth; d > 0; d--) {
|
for (let d = selection.$anchor.depth; d > 0; d--) {
|
||||||
@@ -908,10 +875,8 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
const { state } = instance;
|
const { state } = instance;
|
||||||
const { selection } = state;
|
const { selection } = state;
|
||||||
|
|
||||||
// Get the current cell position
|
|
||||||
const cellPos = selection.$anchor;
|
const cellPos = selection.$anchor;
|
||||||
|
|
||||||
// Find table and column index
|
|
||||||
let tableNode = null;
|
let tableNode = null;
|
||||||
let tableDepth = 0;
|
let tableDepth = 0;
|
||||||
for (let d = cellPos.depth; d > 0; d--) {
|
for (let d = cellPos.depth; d > 0; d--) {
|
||||||
@@ -924,7 +889,6 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (tableNode) {
|
if (tableNode) {
|
||||||
// Find which column we're in
|
|
||||||
let colIndex = 0;
|
let colIndex = 0;
|
||||||
const cellNode = cellPos.node(cellPos.depth);
|
const cellNode = cellPos.node(cellPos.depth);
|
||||||
const rowNode = cellPos.node(cellPos.depth - 1);
|
const rowNode = cellPos.node(cellPos.depth - 1);
|
||||||
@@ -939,7 +903,6 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if this column has content
|
|
||||||
let hasContent = false;
|
let hasContent = false;
|
||||||
tableNode.descendants((node, pos, parent) => {
|
tableNode.descendants((node, pos, parent) => {
|
||||||
if (parent && parent.type.name === "tableRow") {
|
if (parent && parent.type.name === "tableRow") {
|
||||||
@@ -964,7 +927,6 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
instance.chain().focus().deleteColumn().run();
|
instance.chain().focus().deleteColumn().run();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Close language selector on outside click
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (showLanguageSelector()) {
|
if (showLanguageSelector()) {
|
||||||
const handleClickOutside = (e: MouseEvent) => {
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
@@ -985,7 +947,6 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close table menu on outside click
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (showTableMenu()) {
|
if (showTableMenu()) {
|
||||||
const handleClickOutside = (e: MouseEvent) => {
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
@@ -1006,7 +967,6 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close mermaid menu on outside click
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (showMermaidTemplates()) {
|
if (showMermaidTemplates()) {
|
||||||
const handleClickOutside = (e: MouseEvent) => {
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
@@ -1027,7 +987,6 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close conditional config on outside click
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (showConditionalConfig()) {
|
if (showConditionalConfig()) {
|
||||||
const handleClickOutside = (e: MouseEvent) => {
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
@@ -1065,7 +1024,6 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
setShowMermaidTemplates(false);
|
setShowMermaidTemplates(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Conditional block functions
|
|
||||||
const showConditionalConfigurator = (e: MouseEvent) => {
|
const showConditionalConfigurator = (e: MouseEvent) => {
|
||||||
const buttonRect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
const buttonRect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
||||||
setConditionalConfigPosition({
|
setConditionalConfigPosition({
|
||||||
@@ -1073,7 +1031,6 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
left: buttonRect.left
|
left: buttonRect.left
|
||||||
});
|
});
|
||||||
|
|
||||||
// If cursor is in existing conditional, load its values
|
|
||||||
const instance = editor();
|
const instance = editor();
|
||||||
if (instance?.isActive("conditionalBlock")) {
|
if (instance?.isActive("conditionalBlock")) {
|
||||||
const attrs = instance.getAttributes("conditionalBlock");
|
const attrs = instance.getAttributes("conditionalBlock");
|
||||||
@@ -1092,7 +1049,6 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
inline: true
|
inline: true
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Reset to defaults for new conditional
|
|
||||||
setConditionalForm({
|
setConditionalForm({
|
||||||
conditionType: "auth",
|
conditionType: "auth",
|
||||||
conditionValue: "authenticated",
|
conditionValue: "authenticated",
|
||||||
@@ -1112,9 +1068,7 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
conditionalForm();
|
conditionalForm();
|
||||||
|
|
||||||
if (inline) {
|
if (inline) {
|
||||||
// Handle inline conditionals (Mark)
|
|
||||||
if (instance.isActive("conditionalInline")) {
|
if (instance.isActive("conditionalInline")) {
|
||||||
// Update existing inline conditional
|
|
||||||
instance
|
instance
|
||||||
.chain()
|
.chain()
|
||||||
.focus()
|
.focus()
|
||||||
@@ -1126,7 +1080,6 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
} else {
|
} else {
|
||||||
// Apply inline conditional to selection
|
|
||||||
instance
|
instance
|
||||||
.chain()
|
.chain()
|
||||||
.focus()
|
.focus()
|
||||||
@@ -1138,9 +1091,7 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Handle block conditionals (Node)
|
|
||||||
if (instance.isActive("conditionalBlock")) {
|
if (instance.isActive("conditionalBlock")) {
|
||||||
// Update existing conditional
|
|
||||||
instance
|
instance
|
||||||
.chain()
|
.chain()
|
||||||
.focus()
|
.focus()
|
||||||
@@ -1151,7 +1102,6 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
} else {
|
} else {
|
||||||
// Wrap selection in new conditional
|
|
||||||
instance
|
instance
|
||||||
.chain()
|
.chain()
|
||||||
.focus()
|
.focus()
|
||||||
@@ -1167,12 +1117,10 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
setShowConditionalConfig(false);
|
setShowConditionalConfig(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Toggle fullscreen mode
|
|
||||||
const toggleFullscreen = () => {
|
const toggleFullscreen = () => {
|
||||||
setIsFullscreen(!isFullscreen());
|
setIsFullscreen(!isFullscreen());
|
||||||
};
|
};
|
||||||
|
|
||||||
// ESC key to exit fullscreen
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (isFullscreen()) {
|
if (isFullscreen()) {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
@@ -1187,7 +1135,6 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Detect mobile keyboard visibility
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (typeof window === "undefined" || !window.visualViewport) return;
|
if (typeof window === "undefined" || !window.visualViewport) return;
|
||||||
|
|
||||||
@@ -1198,7 +1145,6 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
const currentHeight = viewport.height;
|
const currentHeight = viewport.height;
|
||||||
const heightDiff = initialHeight - currentHeight;
|
const heightDiff = initialHeight - currentHeight;
|
||||||
|
|
||||||
// If viewport height decreased by more than 150px, keyboard is likely open
|
|
||||||
if (heightDiff > 150) {
|
if (heightDiff > 150) {
|
||||||
setKeyboardVisible(true);
|
setKeyboardVisible(true);
|
||||||
setKeyboardHeight(heightDiff);
|
setKeyboardHeight(heightDiff);
|
||||||
@@ -1217,7 +1163,6 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Table Grid Selector Component
|
|
||||||
const TableGridSelector = () => {
|
const TableGridSelector = () => {
|
||||||
const [hoverCell, setHoverCell] = createSignal({ row: 0, col: 0 });
|
const [hoverCell, setHoverCell] = createSignal({ row: 0, col: 0 });
|
||||||
const maxRows = 10;
|
const maxRows = 10;
|
||||||
@@ -1268,7 +1213,6 @@ export default function TextEditor(props: TextEditorProps) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Conditional Configurator Component
|
|
||||||
const ConditionalConfigurator = () => {
|
const ConditionalConfigurator = () => {
|
||||||
return (
|
return (
|
||||||
<div class="bg-mantle border-surface2 w-80 rounded border p-4 shadow-lg">
|
<div class="bg-mantle border-surface2 w-80 rounded border p-4 shadow-lg">
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ export const DarkModeProvider: ParentComponent = (props) => {
|
|||||||
const [isDark, setIsDark] = createSignal(false);
|
const [isDark, setIsDark] = createSignal(false);
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// Check system preference
|
|
||||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
setIsDark(mediaQuery.matches);
|
setIsDark(mediaQuery.matches);
|
||||||
|
|
||||||
|
|||||||
3
src/env/client.ts
vendored
3
src/env/client.ts
vendored
@@ -70,7 +70,6 @@ export const validateClientEnv = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Validate and export environment variables directly
|
// Validate and export environment variables directly
|
||||||
// This happens once at module load time on the client
|
|
||||||
const validateAndExportEnv = (): ClientEnv => {
|
const validateAndExportEnv = (): ClientEnv => {
|
||||||
try {
|
try {
|
||||||
const validated = validateClientEnv(import.meta.env);
|
const validated = validateClientEnv(import.meta.env);
|
||||||
@@ -84,12 +83,10 @@ const validateAndExportEnv = (): ClientEnv => {
|
|||||||
|
|
||||||
export const env = validateAndExportEnv();
|
export const env = validateAndExportEnv();
|
||||||
|
|
||||||
// Helper function to check if a variable is missing
|
|
||||||
export const isMissingEnvVar = (varName: string): boolean => {
|
export const isMissingEnvVar = (varName: string): boolean => {
|
||||||
return !import.meta.env[varName] || import.meta.env[varName]?.trim() === "";
|
return !import.meta.env[varName] || import.meta.env[varName]?.trim() === "";
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to get all missing client environment variables
|
|
||||||
export const getMissingEnvVars = (): string[] => {
|
export const getMissingEnvVars = (): string[] => {
|
||||||
const requiredClientVars = [
|
const requiredClientVars = [
|
||||||
"VITE_DOMAIN",
|
"VITE_DOMAIN",
|
||||||
|
|||||||
3
src/env/server.ts
vendored
3
src/env/server.ts
vendored
@@ -90,7 +90,6 @@ export const validateServerEnv = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Validate and export environment variables directly
|
// Validate and export environment variables directly
|
||||||
// This happens once at module load time on the server
|
|
||||||
const validateAndExportEnv = (): ServerEnv => {
|
const validateAndExportEnv = (): ServerEnv => {
|
||||||
try {
|
try {
|
||||||
const validated = validateServerEnv(process.env);
|
const validated = validateServerEnv(process.env);
|
||||||
@@ -104,12 +103,10 @@ const validateAndExportEnv = (): ServerEnv => {
|
|||||||
|
|
||||||
export const env = validateAndExportEnv();
|
export const env = validateAndExportEnv();
|
||||||
|
|
||||||
// Helper function to check if a variable is missing
|
|
||||||
export const isMissingEnvVar = (varName: string): boolean => {
|
export const isMissingEnvVar = (varName: string): boolean => {
|
||||||
return !process.env[varName] || process.env[varName]?.trim() === "";
|
return !process.env[varName] || process.env[varName]?.trim() === "";
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to get all missing server environment variables
|
|
||||||
export const getMissingEnvVars = (): string[] => {
|
export const getMissingEnvVars = (): string[] => {
|
||||||
const requiredServerVars = [
|
const requiredServerVars = [
|
||||||
"NODE_ENV",
|
"NODE_ENV",
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ const getUserProfile = query(async (): Promise<UserProfile | null> => {
|
|||||||
|
|
||||||
const user = res.rows[0] as any;
|
const user = res.rows[0] as any;
|
||||||
|
|
||||||
// Transform database User to UserProfile
|
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
email: user.email ?? undefined,
|
email: user.email ?? undefined,
|
||||||
@@ -63,10 +62,8 @@ export default function AccountPage() {
|
|||||||
|
|
||||||
const userData = createAsync(() => getUserProfile(), { deferStream: true });
|
const userData = createAsync(() => getUserProfile(), { deferStream: true });
|
||||||
|
|
||||||
// Local user state for client-side updates
|
|
||||||
const [user, setUser] = createSignal<UserProfile | null>(null);
|
const [user, setUser] = createSignal<UserProfile | null>(null);
|
||||||
|
|
||||||
// Form loading states
|
|
||||||
const [emailButtonLoading, setEmailButtonLoading] = createSignal(false);
|
const [emailButtonLoading, setEmailButtonLoading] = createSignal(false);
|
||||||
const [displayNameButtonLoading, setDisplayNameButtonLoading] =
|
const [displayNameButtonLoading, setDisplayNameButtonLoading] =
|
||||||
createSignal(false);
|
createSignal(false);
|
||||||
@@ -76,7 +73,6 @@ export default function AccountPage() {
|
|||||||
const [profileImageSetLoading, setProfileImageSetLoading] =
|
const [profileImageSetLoading, setProfileImageSetLoading] =
|
||||||
createSignal(false);
|
createSignal(false);
|
||||||
|
|
||||||
// Password state
|
|
||||||
const [passwordsMatch, setPasswordsMatch] = createSignal(false);
|
const [passwordsMatch, setPasswordsMatch] = createSignal(false);
|
||||||
const [showPasswordLengthWarning, setShowPasswordLengthWarning] =
|
const [showPasswordLengthWarning, setShowPasswordLengthWarning] =
|
||||||
createSignal(false);
|
createSignal(false);
|
||||||
@@ -86,19 +82,16 @@ export default function AccountPage() {
|
|||||||
const [passwordError, setPasswordError] = createSignal(false);
|
const [passwordError, setPasswordError] = createSignal(false);
|
||||||
const [passwordDeletionError, setPasswordDeletionError] = createSignal(false);
|
const [passwordDeletionError, setPasswordDeletionError] = createSignal(false);
|
||||||
|
|
||||||
// Show/hide password toggles
|
|
||||||
const [showOldPasswordInput, setShowOldPasswordInput] = createSignal(false);
|
const [showOldPasswordInput, setShowOldPasswordInput] = createSignal(false);
|
||||||
const [showPasswordInput, setShowPasswordInput] = createSignal(false);
|
const [showPasswordInput, setShowPasswordInput] = createSignal(false);
|
||||||
const [showPasswordConfInput, setShowPasswordConfInput] = createSignal(false);
|
const [showPasswordConfInput, setShowPasswordConfInput] = createSignal(false);
|
||||||
|
|
||||||
// Success messages
|
|
||||||
const [showImageSuccess, setShowImageSuccess] = createSignal(false);
|
const [showImageSuccess, setShowImageSuccess] = createSignal(false);
|
||||||
const [showEmailSuccess, setShowEmailSuccess] = createSignal(false);
|
const [showEmailSuccess, setShowEmailSuccess] = createSignal(false);
|
||||||
const [showDisplayNameSuccess, setShowDisplayNameSuccess] =
|
const [showDisplayNameSuccess, setShowDisplayNameSuccess] =
|
||||||
createSignal(false);
|
createSignal(false);
|
||||||
const [showPasswordSuccess, setShowPasswordSuccess] = createSignal(false);
|
const [showPasswordSuccess, setShowPasswordSuccess] = createSignal(false);
|
||||||
|
|
||||||
// Profile image state
|
|
||||||
const [profileImage, setProfileImage] = createSignal<Blob | undefined>(
|
const [profileImage, setProfileImage] = createSignal<Blob | undefined>(
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
@@ -109,7 +102,6 @@ export default function AccountPage() {
|
|||||||
createSignal(false);
|
createSignal(false);
|
||||||
const [preSetHolder, setPreSetHolder] = createSignal<string | null>(null);
|
const [preSetHolder, setPreSetHolder] = createSignal<string | null>(null);
|
||||||
|
|
||||||
// Form refs
|
|
||||||
let oldPasswordRef: HTMLInputElement | undefined;
|
let oldPasswordRef: HTMLInputElement | undefined;
|
||||||
let newPasswordRef: HTMLInputElement | undefined;
|
let newPasswordRef: HTMLInputElement | undefined;
|
||||||
let newPasswordConfRef: HTMLInputElement | undefined;
|
let newPasswordConfRef: HTMLInputElement | undefined;
|
||||||
@@ -117,10 +109,8 @@ export default function AccountPage() {
|
|||||||
let displayNameRef: HTMLInputElement | undefined;
|
let displayNameRef: HTMLInputElement | undefined;
|
||||||
let deleteAccountPasswordRef: HTMLInputElement | undefined;
|
let deleteAccountPasswordRef: HTMLInputElement | undefined;
|
||||||
|
|
||||||
// Helper to get current user (from SSR data or local state)
|
|
||||||
const currentUser = () => user() || userData();
|
const currentUser = () => user() || userData();
|
||||||
|
|
||||||
// Initialize preSetHolder when userData loads
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const userProfile = userData();
|
const userProfile = userData();
|
||||||
if (userProfile?.image && !preSetHolder()) {
|
if (userProfile?.image && !preSetHolder()) {
|
||||||
@@ -308,7 +298,6 @@ export default function AccountPage() {
|
|||||||
if (response.ok && result.result?.data?.success) {
|
if (response.ok && result.result?.data?.success) {
|
||||||
setShowPasswordSuccess(true);
|
setShowPasswordSuccess(true);
|
||||||
setTimeout(() => setShowPasswordSuccess(false), 3000);
|
setTimeout(() => setShowPasswordSuccess(false), 3000);
|
||||||
// Clear form
|
|
||||||
if (oldPasswordRef) oldPasswordRef.value = "";
|
if (oldPasswordRef) oldPasswordRef.value = "";
|
||||||
if (newPasswordRef) newPasswordRef.value = "";
|
if (newPasswordRef) newPasswordRef.value = "";
|
||||||
if (newPasswordConfRef) newPasswordConfRef.value = "";
|
if (newPasswordConfRef) newPasswordConfRef.value = "";
|
||||||
@@ -362,7 +351,6 @@ export default function AccountPage() {
|
|||||||
}
|
}
|
||||||
setShowPasswordSuccess(true);
|
setShowPasswordSuccess(true);
|
||||||
setTimeout(() => setShowPasswordSuccess(false), 3000);
|
setTimeout(() => setShowPasswordSuccess(false), 3000);
|
||||||
// Clear form
|
|
||||||
if (newPasswordRef) newPasswordRef.value = "";
|
if (newPasswordRef) newPasswordRef.value = "";
|
||||||
if (newPasswordConfRef) newPasswordConfRef.value = "";
|
if (newPasswordConfRef) newPasswordConfRef.value = "";
|
||||||
} else {
|
} else {
|
||||||
@@ -395,7 +383,6 @@ export default function AccountPage() {
|
|||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (response.ok && result.result?.data?.success) {
|
if (response.ok && result.result?.data?.success) {
|
||||||
// Redirect to login
|
|
||||||
navigate("/login");
|
navigate("/login");
|
||||||
} else {
|
} else {
|
||||||
setPasswordDeletionError(true);
|
setPasswordDeletionError(true);
|
||||||
@@ -425,7 +412,6 @@ export default function AccountPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Password validation helpers
|
|
||||||
const checkPasswordLength = (password: string) => {
|
const checkPasswordLength = (password: string) => {
|
||||||
if (password.length >= 8) {
|
if (password.length >= 8) {
|
||||||
setPasswordLengthSufficient(true);
|
setPasswordLengthSufficient(true);
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export async function GET(event: APIEvent) {
|
|||||||
const code = url.searchParams.get("code");
|
const code = url.searchParams.get("code");
|
||||||
const error = url.searchParams.get("error");
|
const error = url.searchParams.get("error");
|
||||||
|
|
||||||
// Handle OAuth error (user denied access, etc.)
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return new Response(null, {
|
return new Response(null, {
|
||||||
status: 302,
|
status: 302,
|
||||||
@@ -15,7 +14,6 @@ export async function GET(event: APIEvent) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Missing authorization code
|
|
||||||
if (!code) {
|
if (!code) {
|
||||||
return new Response(null, {
|
return new Response(null, {
|
||||||
status: 302,
|
status: 302,
|
||||||
@@ -32,13 +30,11 @@ export async function GET(event: APIEvent) {
|
|||||||
const result = await caller.auth.githubCallback({ code });
|
const result = await caller.auth.githubCallback({ code });
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
// Redirect to account page on success
|
|
||||||
return new Response(null, {
|
return new Response(null, {
|
||||||
status: 302,
|
status: 302,
|
||||||
headers: { Location: result.redirectTo || "/account" }
|
headers: { Location: result.redirectTo || "/account" }
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Redirect to login with error
|
|
||||||
return new Response(null, {
|
return new Response(null, {
|
||||||
status: 302,
|
status: 302,
|
||||||
headers: { Location: "/login?error=auth_failed" }
|
headers: { Location: "/login?error=auth_failed" }
|
||||||
@@ -47,7 +43,6 @@ export async function GET(event: APIEvent) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("GitHub OAuth callback error:", error);
|
console.error("GitHub OAuth callback error:", error);
|
||||||
|
|
||||||
// Handle specific TRPC errors
|
|
||||||
if (error && typeof error === "object" && "code" in error) {
|
if (error && typeof error === "object" && "code" in error) {
|
||||||
const trpcError = error as { code: string; message?: string };
|
const trpcError = error as { code: string; message?: string };
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export async function GET(event: APIEvent) {
|
|||||||
const code = url.searchParams.get("code");
|
const code = url.searchParams.get("code");
|
||||||
const error = url.searchParams.get("error");
|
const error = url.searchParams.get("error");
|
||||||
|
|
||||||
// Handle OAuth error (user denied access, etc.)
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return new Response(null, {
|
return new Response(null, {
|
||||||
status: 302,
|
status: 302,
|
||||||
@@ -15,7 +14,6 @@ export async function GET(event: APIEvent) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Missing authorization code
|
|
||||||
if (!code) {
|
if (!code) {
|
||||||
return new Response(null, {
|
return new Response(null, {
|
||||||
status: 302,
|
status: 302,
|
||||||
@@ -32,13 +30,11 @@ export async function GET(event: APIEvent) {
|
|||||||
const result = await caller.auth.googleCallback({ code });
|
const result = await caller.auth.googleCallback({ code });
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
// Redirect to account page on success
|
|
||||||
return new Response(null, {
|
return new Response(null, {
|
||||||
status: 302,
|
status: 302,
|
||||||
headers: { Location: result.redirectTo || "/account" }
|
headers: { Location: result.redirectTo || "/account" }
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Redirect to login with error
|
|
||||||
return new Response(null, {
|
return new Response(null, {
|
||||||
status: 302,
|
status: 302,
|
||||||
headers: { Location: "/login?error=auth_failed" }
|
headers: { Location: "/login?error=auth_failed" }
|
||||||
@@ -47,7 +43,6 @@ export async function GET(event: APIEvent) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Google OAuth callback error:", error);
|
console.error("Google OAuth callback error:", error);
|
||||||
|
|
||||||
// Handle specific TRPC errors
|
|
||||||
if (error && typeof error === "object" && "code" in error) {
|
if (error && typeof error === "object" && "code" in error) {
|
||||||
const trpcError = error as { code: string; message?: string };
|
const trpcError = error as { code: string; message?: string };
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ export async function GET(event: APIEvent) {
|
|||||||
// Parse rememberMe parameter
|
// Parse rememberMe parameter
|
||||||
const rememberMe = rememberMeParam === "true";
|
const rememberMe = rememberMeParam === "true";
|
||||||
|
|
||||||
// Missing required parameters
|
|
||||||
if (!email || !token) {
|
if (!email || !token) {
|
||||||
return new Response(null, {
|
return new Response(null, {
|
||||||
status: 302,
|
status: 302,
|
||||||
@@ -32,13 +31,11 @@ export async function GET(event: APIEvent) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
// Redirect to account page on success
|
|
||||||
return new Response(null, {
|
return new Response(null, {
|
||||||
status: 302,
|
status: 302,
|
||||||
headers: { Location: result.redirectTo || "/account" },
|
headers: { Location: result.redirectTo || "/account" },
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Redirect to login with error
|
|
||||||
return new Response(null, {
|
return new Response(null, {
|
||||||
status: 302,
|
status: 302,
|
||||||
headers: { Location: "/login?error=auth_failed" },
|
headers: { Location: "/login?error=auth_failed" },
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export async function GET(event: APIEvent) {
|
|||||||
const email = url.searchParams.get("email");
|
const email = url.searchParams.get("email");
|
||||||
const token = url.searchParams.get("token");
|
const token = url.searchParams.get("token");
|
||||||
|
|
||||||
// Missing required parameters
|
|
||||||
if (!email || !token) {
|
if (!email || !token) {
|
||||||
return new Response(
|
return new Response(
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export async function POST() {
|
|||||||
expires: new Date(0) // Set expiry to past date
|
expires: new Date(0) // Set expiry to past date
|
||||||
});
|
});
|
||||||
|
|
||||||
// Redirect to home page
|
|
||||||
return new Response(null, {
|
return new Response(null, {
|
||||||
status: 302,
|
status: 302,
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import type { Comment, CommentReaction, UserPublicData } from "~/types/comment";
|
|||||||
import { TerminalSplash } from "~/components/TerminalSplash";
|
import { TerminalSplash } from "~/components/TerminalSplash";
|
||||||
import { api } from "~/lib/api";
|
import { api } from "~/lib/api";
|
||||||
|
|
||||||
// Server function to fetch post by title
|
|
||||||
const getPostByTitle = query(
|
const getPostByTitle = query(
|
||||||
async (
|
async (
|
||||||
title: string,
|
title: string,
|
||||||
@@ -48,7 +47,6 @@ const getPostByTitle = query(
|
|||||||
const post = postResults.rows[0] as any;
|
const post = postResults.rows[0] as any;
|
||||||
|
|
||||||
if (!post) {
|
if (!post) {
|
||||||
// Check if post exists but is unpublished
|
|
||||||
const existQuery = "SELECT id FROM Post WHERE title = ?";
|
const existQuery = "SELECT id FROM Post WHERE title = ?";
|
||||||
const existRes = await conn.execute({
|
const existRes = await conn.execute({
|
||||||
sql: existQuery,
|
sql: existQuery,
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import TagSelector from "~/components/blog/TagSelector";
|
|||||||
import PostSorting from "~/components/blog/PostSorting";
|
import PostSorting from "~/components/blog/PostSorting";
|
||||||
import { TerminalSplash } from "~/components/TerminalSplash";
|
import { TerminalSplash } from "~/components/TerminalSplash";
|
||||||
|
|
||||||
// Server function to fetch all posts
|
|
||||||
const getPosts = query(async () => {
|
const getPosts = query(async () => {
|
||||||
"use server";
|
"use server";
|
||||||
const { ConnectionFactory, getPrivilegeLevel } =
|
const { ConnectionFactory, getPrivilegeLevel } =
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ const getContactData = query(async () => {
|
|||||||
return { remainingTime };
|
return { remainingTime };
|
||||||
}, "contact-data");
|
}, "contact-data");
|
||||||
|
|
||||||
// Server action for form submission
|
|
||||||
const sendContactEmail = action(async (formData: FormData) => {
|
const sendContactEmail = action(async (formData: FormData) => {
|
||||||
"use server";
|
"use server";
|
||||||
const name = formData.get("name") as string;
|
const name = formData.get("name") as string;
|
||||||
@@ -65,7 +64,6 @@ const sendContactEmail = action(async (formData: FormData) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check rate limit
|
|
||||||
const contactExp = getCookie("contactRequestSent");
|
const contactExp = getCookie("contactRequestSent");
|
||||||
if (contactExp) {
|
if (contactExp) {
|
||||||
const expires = new Date(contactExp);
|
const expires = new Date(contactExp);
|
||||||
@@ -189,7 +187,6 @@ export default function ContactPage() {
|
|||||||
setCountDown(serverData.remainingTime);
|
setCountDown(serverData.remainingTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for existing timer
|
|
||||||
const timer = getClientCookie("contactRequestSent");
|
const timer = getClientCookie("contactRequestSent");
|
||||||
if (timer) {
|
if (timer) {
|
||||||
timerIdRef = setInterval(() => calcRemainder(timer), 1000);
|
timerIdRef = setInterval(() => calcRemainder(timer), 1000);
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ export default function LoginPage() {
|
|||||||
const register = () => searchParams.mode === "register";
|
const register = () => searchParams.mode === "register";
|
||||||
const usePassword = () => searchParams.auth === "password";
|
const usePassword = () => searchParams.auth === "password";
|
||||||
|
|
||||||
// State management
|
|
||||||
const [error, setError] = createSignal("");
|
const [error, setError] = createSignal("");
|
||||||
const [loading, setLoading] = createSignal(false);
|
const [loading, setLoading] = createSignal(false);
|
||||||
const [countDown, setCountDown] = createSignal(0);
|
const [countDown, setCountDown] = createSignal(0);
|
||||||
@@ -58,7 +57,6 @@ export default function LoginPage() {
|
|||||||
createSignal(false);
|
createSignal(false);
|
||||||
const [passwordBlurred, setPasswordBlurred] = createSignal(false);
|
const [passwordBlurred, setPasswordBlurred] = createSignal(false);
|
||||||
|
|
||||||
// Form refs
|
|
||||||
let emailRef: HTMLInputElement | undefined;
|
let emailRef: HTMLInputElement | undefined;
|
||||||
let passwordRef: HTMLInputElement | undefined;
|
let passwordRef: HTMLInputElement | undefined;
|
||||||
let passwordConfRef: HTMLInputElement | undefined;
|
let passwordConfRef: HTMLInputElement | undefined;
|
||||||
@@ -70,7 +68,6 @@ export default function LoginPage() {
|
|||||||
const githubClientId = env.VITE_GITHUB_CLIENT_ID;
|
const githubClientId = env.VITE_GITHUB_CLIENT_ID;
|
||||||
const domain = env.VITE_DOMAIN || "https://www.freno.me";
|
const domain = env.VITE_DOMAIN || "https://www.freno.me";
|
||||||
|
|
||||||
// Calculate remaining time from cookie
|
|
||||||
const calcRemainder = (timer: string) => {
|
const calcRemainder = (timer: string) => {
|
||||||
const expires = new Date(timer);
|
const expires = new Date(timer);
|
||||||
const remaining = expires.getTime() - Date.now();
|
const remaining = expires.getTime() - Date.now();
|
||||||
@@ -86,7 +83,6 @@ export default function LoginPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check for existing timer on mount
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const timer = getClientCookie("emailLoginLinkRequested");
|
const timer = getClientCookie("emailLoginLinkRequested");
|
||||||
if (timer) {
|
if (timer) {
|
||||||
@@ -103,7 +99,6 @@ export default function LoginPage() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check for OAuth/callback errors in URL
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const errorParam = searchParams.error;
|
const errorParam = searchParams.error;
|
||||||
if (errorParam) {
|
if (errorParam) {
|
||||||
@@ -121,7 +116,6 @@ export default function LoginPage() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Form submission handler
|
|
||||||
const formHandler = async (e: Event) => {
|
const formHandler = async (e: Event) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -273,7 +267,6 @@ export default function LoginPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Countdown timer render function
|
|
||||||
const renderTime = ({ remainingTime }: { remainingTime: number }) => {
|
const renderTime = ({ remainingTime }: { remainingTime: number }) => {
|
||||||
return (
|
return (
|
||||||
<div class="timer">
|
<div class="timer">
|
||||||
@@ -282,7 +275,6 @@ export default function LoginPage() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Password validation helpers
|
|
||||||
const checkForMatch = (newPassword: string, newPasswordConf: string) => {
|
const checkForMatch = (newPassword: string, newPasswordConf: string) => {
|
||||||
setPasswordsMatch(newPassword === newPasswordConf);
|
setPasswordsMatch(newPassword === newPasswordConf);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ export default function PasswordResetPage() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
// State management
|
|
||||||
const [passwordBlurred, setPasswordBlurred] = createSignal(false);
|
const [passwordBlurred, setPasswordBlurred] = createSignal(false);
|
||||||
const [passwordChangeLoading, setPasswordChangeLoading] = createSignal(false);
|
const [passwordChangeLoading, setPasswordChangeLoading] = createSignal(false);
|
||||||
const [passwordsMatch, setPasswordsMatch] = createSignal(false);
|
const [passwordsMatch, setPasswordsMatch] = createSignal(false);
|
||||||
@@ -25,21 +24,18 @@ export default function PasswordResetPage() {
|
|||||||
const [showPasswordInput, setShowPasswordInput] = createSignal(false);
|
const [showPasswordInput, setShowPasswordInput] = createSignal(false);
|
||||||
const [showPasswordConfInput, setShowPasswordConfInput] = createSignal(false);
|
const [showPasswordConfInput, setShowPasswordConfInput] = createSignal(false);
|
||||||
|
|
||||||
// Form refs
|
|
||||||
let newPasswordRef: HTMLInputElement | undefined;
|
let newPasswordRef: HTMLInputElement | undefined;
|
||||||
let newPasswordConfRef: HTMLInputElement | undefined;
|
let newPasswordConfRef: HTMLInputElement | undefined;
|
||||||
|
|
||||||
// Get token from URL
|
// Get token from URL
|
||||||
const token = searchParams.token;
|
const token = searchParams.token;
|
||||||
|
|
||||||
// Redirect to request page if no token
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
navigate("/login/request-password-reset");
|
navigate("/login/request-password-reset");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Form submission handler
|
|
||||||
const setNewPasswordTrigger = async (e: Event) => {
|
const setNewPasswordTrigger = async (e: Event) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setShowRequestNewEmail(false);
|
setShowRequestNewEmail(false);
|
||||||
@@ -93,7 +89,6 @@ export default function PasswordResetPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if passwords match
|
|
||||||
const checkForMatch = (newPassword: string, newPasswordConf: string) => {
|
const checkForMatch = (newPassword: string, newPasswordConf: string) => {
|
||||||
if (newPassword === newPasswordConf) {
|
if (newPassword === newPasswordConf) {
|
||||||
setPasswordsMatch(true);
|
setPasswordsMatch(true);
|
||||||
@@ -102,7 +97,6 @@ export default function PasswordResetPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check password length
|
|
||||||
const checkPasswordLength = (password: string) => {
|
const checkPasswordLength = (password: string) => {
|
||||||
if (password.length >= 8) {
|
if (password.length >= 8) {
|
||||||
setPasswordLengthSufficient(true);
|
setPasswordLengthSufficient(true);
|
||||||
@@ -115,7 +109,6 @@ export default function PasswordResetPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle password blur
|
|
||||||
const passwordLengthBlurCheck = () => {
|
const passwordLengthBlurCheck = () => {
|
||||||
if (
|
if (
|
||||||
!passwordLengthSufficient() &&
|
!passwordLengthSufficient() &&
|
||||||
@@ -127,7 +120,6 @@ export default function PasswordResetPage() {
|
|||||||
setPasswordBlurred(true);
|
setPasswordBlurred(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle new password change
|
|
||||||
const handleNewPasswordChange = (e: Event) => {
|
const handleNewPasswordChange = (e: Event) => {
|
||||||
const target = e.target as HTMLInputElement;
|
const target = e.target as HTMLInputElement;
|
||||||
checkPasswordLength(target.value);
|
checkPasswordLength(target.value);
|
||||||
@@ -136,7 +128,6 @@ export default function PasswordResetPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle password confirmation change
|
|
||||||
const handlePasswordConfChange = (e: Event) => {
|
const handlePasswordConfChange = (e: Event) => {
|
||||||
const target = e.target as HTMLInputElement;
|
const target = e.target as HTMLInputElement;
|
||||||
if (newPasswordRef) {
|
if (newPasswordRef) {
|
||||||
@@ -144,7 +135,6 @@ export default function PasswordResetPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle password blur
|
|
||||||
const handlePasswordBlur = () => {
|
const handlePasswordBlur = () => {
|
||||||
passwordLengthBlurCheck();
|
passwordLengthBlurCheck();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,17 +8,14 @@ import { getClientCookie } from "~/lib/cookies.client";
|
|||||||
export default function RequestPasswordResetPage() {
|
export default function RequestPasswordResetPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// State management
|
|
||||||
const [loading, setLoading] = createSignal(false);
|
const [loading, setLoading] = createSignal(false);
|
||||||
const [countDown, setCountDown] = createSignal(0);
|
const [countDown, setCountDown] = createSignal(0);
|
||||||
const [showSuccessMessage, setShowSuccessMessage] = createSignal(false);
|
const [showSuccessMessage, setShowSuccessMessage] = createSignal(false);
|
||||||
const [error, setError] = createSignal("");
|
const [error, setError] = createSignal("");
|
||||||
|
|
||||||
// Form refs
|
|
||||||
let emailRef: HTMLInputElement | undefined;
|
let emailRef: HTMLInputElement | undefined;
|
||||||
let timerInterval: number | undefined;
|
let timerInterval: number | undefined;
|
||||||
|
|
||||||
// Calculate remaining time from cookie
|
|
||||||
const calcRemainder = (timer: string) => {
|
const calcRemainder = (timer: string) => {
|
||||||
const expires = new Date(timer);
|
const expires = new Date(timer);
|
||||||
const remaining = expires.getTime() - Date.now();
|
const remaining = expires.getTime() - Date.now();
|
||||||
@@ -34,7 +31,6 @@ export default function RequestPasswordResetPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check for existing timer on mount
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const timer = getClientCookie("passwordResetRequested");
|
const timer = getClientCookie("passwordResetRequested");
|
||||||
if (timer) {
|
if (timer) {
|
||||||
@@ -51,7 +47,6 @@ export default function RequestPasswordResetPage() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Form submission handler
|
|
||||||
const requestPasswordResetTrigger = async (e: Event) => {
|
const requestPasswordResetTrigger = async (e: Event) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError("");
|
setError("");
|
||||||
|
|||||||
@@ -432,9 +432,8 @@ export const authRouter = createTRPCRouter({
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (rememberMe) {
|
if (rememberMe) {
|
||||||
cookieOptions.maxAge = 60 * 60 * 24 * 14; // 14 days
|
cookieOptions.maxAge = 60 * 60 * 24 * 14;
|
||||||
}
|
}
|
||||||
// If rememberMe is false, cookie will be session-only (no maxAge)
|
|
||||||
|
|
||||||
setCookie(
|
setCookie(
|
||||||
ctx.event.nativeEvent,
|
ctx.event.nativeEvent,
|
||||||
@@ -591,7 +590,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// If provider is unknown/null, update it to "email" since they're logging in with password
|
|
||||||
if (
|
if (
|
||||||
!user.provider ||
|
!user.provider ||
|
||||||
!["email", "google", "github", "apple"].includes(user.provider)
|
!["email", "google", "github", "apple"].includes(user.provider)
|
||||||
@@ -669,7 +667,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
.setExpirationTime("15m")
|
.setExpirationTime("15m")
|
||||||
.sign(secret);
|
.sign(secret);
|
||||||
|
|
||||||
// Send email
|
|
||||||
const domain = env.VITE_DOMAIN || "https://freno.me";
|
const domain = env.VITE_DOMAIN || "https://freno.me";
|
||||||
const htmlContent = `<html>
|
const htmlContent = `<html>
|
||||||
<head>
|
<head>
|
||||||
@@ -754,7 +751,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
const { email } = input;
|
const { email } = input;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check rate limiting
|
|
||||||
const requested = getCookie(
|
const requested = getCookie(
|
||||||
ctx.event.nativeEvent,
|
ctx.event.nativeEvent,
|
||||||
"passwordResetRequested"
|
"passwordResetRequested"
|
||||||
@@ -777,20 +773,16 @@ export const authRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (res.rows.length === 0) {
|
if (res.rows.length === 0) {
|
||||||
// Don't reveal if user exists
|
|
||||||
return { success: true, message: "email sent" };
|
return { success: true, message: "email sent" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = res.rows[0] as unknown as User;
|
const user = res.rows[0] as unknown as User;
|
||||||
|
|
||||||
// Create JWT token with user ID (15min expiry)
|
|
||||||
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
|
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
|
||||||
const token = await new SignJWT({ id: user.id })
|
const token = await new SignJWT({ id: user.id })
|
||||||
.setProtectedHeader({ alg: "HS256" })
|
.setProtectedHeader({ alg: "HS256" })
|
||||||
.setExpirationTime("15m")
|
.setExpirationTime("15m")
|
||||||
.sign(secret);
|
.sign(secret);
|
||||||
|
|
||||||
// Send email
|
|
||||||
const domain = env.VITE_DOMAIN || "https://freno.me";
|
const domain = env.VITE_DOMAIN || "https://freno.me";
|
||||||
const htmlContent = `<html>
|
const htmlContent = `<html>
|
||||||
<head>
|
<head>
|
||||||
@@ -832,7 +824,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
|
|
||||||
await sendEmail(email, "password reset", htmlContent);
|
await sendEmail(email, "password reset", htmlContent);
|
||||||
|
|
||||||
// Set rate limit cookie (5 minutes)
|
|
||||||
const exp = new Date(Date.now() + 5 * 60 * 1000);
|
const exp = new Date(Date.now() + 5 * 60 * 1000);
|
||||||
setCookie(
|
setCookie(
|
||||||
ctx.event.nativeEvent,
|
ctx.event.nativeEvent,
|
||||||
@@ -870,7 +861,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Reset password with token
|
|
||||||
resetPassword: publicProcedure
|
resetPassword: publicProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -890,7 +880,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Verify JWT token
|
|
||||||
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
|
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
|
||||||
const { payload } = await jwtVerify(token, secret);
|
const { payload } = await jwtVerify(token, secret);
|
||||||
|
|
||||||
@@ -904,7 +893,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
const conn = ConnectionFactory();
|
const conn = ConnectionFactory();
|
||||||
const passwordHash = await hashPassword(newPassword);
|
const passwordHash = await hashPassword(newPassword);
|
||||||
|
|
||||||
// Get user to check current provider
|
|
||||||
const userRes = await conn.execute({
|
const userRes = await conn.execute({
|
||||||
sql: "SELECT provider FROM User WHERE id = ?",
|
sql: "SELECT provider FROM User WHERE id = ?",
|
||||||
args: [payload.id]
|
args: [payload.id]
|
||||||
@@ -919,7 +907,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const currentProvider = (userRes.rows[0] as any).provider;
|
const currentProvider = (userRes.rows[0] as any).provider;
|
||||||
|
|
||||||
// Only update provider to "email" if it's null, undefined, or not a known OAuth provider
|
|
||||||
if (
|
if (
|
||||||
!currentProvider ||
|
!currentProvider ||
|
||||||
!["google", "github", "apple"].includes(currentProvider)
|
!["google", "github", "apple"].includes(currentProvider)
|
||||||
@@ -929,14 +916,12 @@ export const authRouter = createTRPCRouter({
|
|||||||
args: [passwordHash, "email", payload.id]
|
args: [passwordHash, "email", payload.id]
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Keep existing OAuth provider, just update password
|
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: "UPDATE User SET password_hash = ? WHERE id = ?",
|
sql: "UPDATE User SET password_hash = ? WHERE id = ?",
|
||||||
args: [passwordHash, payload.id]
|
args: [passwordHash, payload.id]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear any session cookies
|
|
||||||
setCookie(ctx.event.nativeEvent, "emailToken", "", {
|
setCookie(ctx.event.nativeEvent, "emailToken", "", {
|
||||||
maxAge: 0,
|
maxAge: 0,
|
||||||
path: "/"
|
path: "/"
|
||||||
@@ -959,14 +944,12 @@ export const authRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Resend email verification
|
|
||||||
resendEmailVerification: publicProcedure
|
resendEmailVerification: publicProcedure
|
||||||
.input(z.object({ email: z.string().email() }))
|
.input(z.object({ email: z.string().email() }))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const { email } = input;
|
const { email } = input;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check rate limiting
|
|
||||||
const requested = getCookie(
|
const requested = getCookie(
|
||||||
ctx.event.nativeEvent,
|
ctx.event.nativeEvent,
|
||||||
"emailVerificationRequested"
|
"emailVerificationRequested"
|
||||||
@@ -998,14 +981,12 @@ export const authRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create JWT token (15min expiry)
|
|
||||||
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
|
const secret = new TextEncoder().encode(env.JWT_SECRET_KEY);
|
||||||
const token = await new SignJWT({ email })
|
const token = await new SignJWT({ email })
|
||||||
.setProtectedHeader({ alg: "HS256" })
|
.setProtectedHeader({ alg: "HS256" })
|
||||||
.setExpirationTime("15m")
|
.setExpirationTime("15m")
|
||||||
.sign(secret);
|
.sign(secret);
|
||||||
|
|
||||||
// Send email
|
|
||||||
const domain = env.VITE_DOMAIN || "https://freno.me";
|
const domain = env.VITE_DOMAIN || "https://freno.me";
|
||||||
const htmlContent = `<html>
|
const htmlContent = `<html>
|
||||||
<head>
|
<head>
|
||||||
@@ -1044,7 +1025,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
|
|
||||||
await sendEmail(email, "freno.me email verification", htmlContent);
|
await sendEmail(email, "freno.me email verification", htmlContent);
|
||||||
|
|
||||||
// Set rate limit cookie
|
|
||||||
setCookie(
|
setCookie(
|
||||||
ctx.event.nativeEvent,
|
ctx.event.nativeEvent,
|
||||||
"emailVerificationRequested",
|
"emailVerificationRequested",
|
||||||
@@ -1081,7 +1061,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Sign out
|
|
||||||
signOut: publicProcedure.mutation(async ({ ctx }) => {
|
signOut: publicProcedure.mutation(async ({ ctx }) => {
|
||||||
setCookie(ctx.event.nativeEvent, "userIDToken", "", {
|
setCookie(ctx.event.nativeEvent, "userIDToken", "", {
|
||||||
maxAge: 0,
|
maxAge: 0,
|
||||||
|
|||||||
@@ -162,7 +162,6 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
requestingUser: ctx.userId
|
requestingUser: ctx.userId
|
||||||
});
|
});
|
||||||
|
|
||||||
// User can only delete their own comments with "user" type
|
|
||||||
if (input.deletionType === "user" && !isOwner && !isAdmin) {
|
if (input.deletionType === "user" && !isOwner && !isAdmin) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "FORBIDDEN",
|
code: "FORBIDDEN",
|
||||||
@@ -170,7 +169,6 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only admins can do admin or database deletion
|
|
||||||
if (
|
if (
|
||||||
(input.deletionType === "admin" ||
|
(input.deletionType === "admin" ||
|
||||||
input.deletionType === "database") &&
|
input.deletionType === "database") &&
|
||||||
@@ -184,14 +182,11 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
|
|
||||||
if (input.deletionType === "database") {
|
if (input.deletionType === "database") {
|
||||||
console.log("[deleteComment] Performing database deletion");
|
console.log("[deleteComment] Performing database deletion");
|
||||||
// Full deletion - remove from database
|
|
||||||
// First delete reactions
|
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: "DELETE FROM CommentReaction WHERE comment_id = ?",
|
sql: "DELETE FROM CommentReaction WHERE comment_id = ?",
|
||||||
args: [input.commentID]
|
args: [input.commentID]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Then delete the comment
|
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: "DELETE FROM Comment WHERE id = ?",
|
sql: "DELETE FROM Comment WHERE id = ?",
|
||||||
args: [input.commentID]
|
args: [input.commentID]
|
||||||
@@ -205,7 +200,6 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
};
|
};
|
||||||
} else if (input.deletionType === "admin") {
|
} else if (input.deletionType === "admin") {
|
||||||
console.log("[deleteComment] Performing admin deletion");
|
console.log("[deleteComment] Performing admin deletion");
|
||||||
// Admin delete - replace body with admin message
|
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: "UPDATE Comment SET body = ?, commenter_id = ? WHERE id = ?",
|
sql: "UPDATE Comment SET body = ?, commenter_id = ? WHERE id = ?",
|
||||||
args: ["[deleted by admin]", "", input.commentID]
|
args: ["[deleted by admin]", "", input.commentID]
|
||||||
@@ -219,7 +213,6 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
console.log("[deleteComment] Performing user deletion");
|
console.log("[deleteComment] Performing user deletion");
|
||||||
// User delete - replace body with user message
|
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: "UPDATE Comment SET body = ?, commenter_id = ? WHERE id = ?",
|
sql: "UPDATE Comment SET body = ?, commenter_id = ? WHERE id = ?",
|
||||||
args: ["[deleted]", "", input.commentID]
|
args: ["[deleted]", "", input.commentID]
|
||||||
@@ -249,7 +242,6 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
const conn = ConnectionFactory();
|
const conn = ConnectionFactory();
|
||||||
// Join with Post table to get post titles along with comments
|
|
||||||
const query = `
|
const query = `
|
||||||
SELECT c.*, p.title as post_title
|
SELECT c.*, p.title as post_title
|
||||||
FROM Comment c
|
FROM Comment c
|
||||||
@@ -270,10 +262,6 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Post Routes
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
getPostById: publicProcedure
|
getPostById: publicProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -288,7 +276,6 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
async () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
const conn = ConnectionFactory();
|
const conn = ConnectionFactory();
|
||||||
// Single query with JOIN to get post and tags in one go
|
|
||||||
const query = `
|
const query = `
|
||||||
SELECT p.*, t.value as tag_value
|
SELECT p.*, t.value as tag_value
|
||||||
FROM Post p
|
FROM Post p
|
||||||
@@ -301,7 +288,6 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (results.rows[0]) {
|
if (results.rows[0]) {
|
||||||
// Group tags by post ID
|
|
||||||
const post = results.rows[0];
|
const post = results.rows[0];
|
||||||
const tags = results.rows
|
const tags = results.rows
|
||||||
.filter((row) => row.tag_value)
|
.filter((row) => row.tag_value)
|
||||||
@@ -339,7 +325,6 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
try {
|
try {
|
||||||
const conn = ConnectionFactory();
|
const conn = ConnectionFactory();
|
||||||
|
|
||||||
// Get post by title with JOINs to get all related data in one query
|
|
||||||
const postQuery = `
|
const postQuery = `
|
||||||
SELECT
|
SELECT
|
||||||
p.*,
|
p.*,
|
||||||
@@ -364,7 +349,6 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const postRow = postResults.rows[0];
|
const postRow = postResults.rows[0];
|
||||||
|
|
||||||
// Return structured data with proper formatting
|
|
||||||
return {
|
return {
|
||||||
post: postRow,
|
post: postRow,
|
||||||
comments: [], // Comments are not included in this optimized query - would need separate call if needed
|
comments: [], // Comments are not included in this optimized query - would need separate call if needed
|
||||||
@@ -426,7 +410,6 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
await conn.execute(tagQuery);
|
await conn.execute(tagQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invalidate blog cache
|
|
||||||
cache.deleteByPrefix("blog-");
|
cache.deleteByPrefix("blog-");
|
||||||
|
|
||||||
return { data: results.lastInsertRowid };
|
return { data: results.lastInsertRowid };
|
||||||
@@ -502,7 +485,6 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const results = await conn.execute({ sql: query, args: params });
|
const results = await conn.execute({ sql: query, args: params });
|
||||||
|
|
||||||
// Handle tags
|
|
||||||
const deleteTagsQuery = `DELETE FROM Tag WHERE post_id = ?`;
|
const deleteTagsQuery = `DELETE FROM Tag WHERE post_id = ?`;
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: deleteTagsQuery,
|
sql: deleteTagsQuery,
|
||||||
@@ -516,7 +498,6 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
await conn.execute(tagQuery);
|
await conn.execute(tagQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invalidate blog cache
|
|
||||||
cache.deleteByPrefix("blog-");
|
cache.deleteByPrefix("blog-");
|
||||||
|
|
||||||
return { data: results.lastInsertRowid };
|
return { data: results.lastInsertRowid };
|
||||||
@@ -535,31 +516,26 @@ export const databaseRouter = createTRPCRouter({
|
|||||||
try {
|
try {
|
||||||
const conn = ConnectionFactory();
|
const conn = ConnectionFactory();
|
||||||
|
|
||||||
// Delete associated tags first
|
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: "DELETE FROM Tag WHERE post_id = ?",
|
sql: "DELETE FROM Tag WHERE post_id = ?",
|
||||||
args: [input.id.toString()]
|
args: [input.id.toString()]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete associated likes
|
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: "DELETE FROM PostLike WHERE post_id = ?",
|
sql: "DELETE FROM PostLike WHERE post_id = ?",
|
||||||
args: [input.id.toString()]
|
args: [input.id.toString()]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete associated comments
|
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: "DELETE FROM Comment WHERE post_id = ?",
|
sql: "DELETE FROM Comment WHERE post_id = ?",
|
||||||
args: [input.id]
|
args: [input.id]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Finally delete the post
|
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: "DELETE FROM Post WHERE id = ?",
|
sql: "DELETE FROM Post WHERE id = ?",
|
||||||
args: [input.id]
|
args: [input.id]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Invalidate blog cache
|
|
||||||
cache.deleteByPrefix("blog-");
|
cache.deleteByPrefix("blog-");
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
APIError
|
APIError
|
||||||
} from "~/server/fetch-utils";
|
} from "~/server/fetch-utils";
|
||||||
|
|
||||||
// Types for commits
|
|
||||||
interface GitCommit {
|
interface GitCommit {
|
||||||
sha: string;
|
sha: string;
|
||||||
message: string;
|
message: string;
|
||||||
@@ -26,7 +25,6 @@ interface ContributionDay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const gitActivityRouter = createTRPCRouter({
|
export const gitActivityRouter = createTRPCRouter({
|
||||||
// Get recent commits from GitHub
|
|
||||||
getGitHubCommits: publicProcedure
|
getGitHubCommits: publicProcedure
|
||||||
.input(z.object({ limit: z.number().default(3) }))
|
.input(z.object({ limit: z.number().default(3) }))
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
@@ -34,7 +32,6 @@ export const gitActivityRouter = createTRPCRouter({
|
|||||||
`github-commits-${input.limit}`,
|
`github-commits-${input.limit}`,
|
||||||
10 * 60 * 1000, // 10 minutes
|
10 * 60 * 1000, // 10 minutes
|
||||||
async () => {
|
async () => {
|
||||||
// Get user's repositories sorted by most recently pushed
|
|
||||||
const reposResponse = await fetchWithTimeout(
|
const reposResponse = await fetchWithTimeout(
|
||||||
`https://api.github.com/users/MikeFreno/repos?sort=pushed&per_page=10`,
|
`https://api.github.com/users/MikeFreno/repos?sort=pushed&per_page=10`,
|
||||||
{
|
{
|
||||||
@@ -50,7 +47,6 @@ export const gitActivityRouter = createTRPCRouter({
|
|||||||
const repos = await reposResponse.json();
|
const repos = await reposResponse.json();
|
||||||
const allCommits: GitCommit[] = [];
|
const allCommits: GitCommit[] = [];
|
||||||
|
|
||||||
// Fetch recent commits from each repo
|
|
||||||
for (const repo of repos) {
|
for (const repo of repos) {
|
||||||
if (allCommits.length >= input.limit * 3) break; // Get extra to sort later
|
if (allCommits.length >= input.limit * 3) break; // Get extra to sort later
|
||||||
|
|
||||||
@@ -69,7 +65,6 @@ export const gitActivityRouter = createTRPCRouter({
|
|||||||
if (commitsResponse.ok) {
|
if (commitsResponse.ok) {
|
||||||
const commits = await commitsResponse.json();
|
const commits = await commitsResponse.json();
|
||||||
for (const commit of commits) {
|
for (const commit of commits) {
|
||||||
// Filter for commits by the authenticated user
|
|
||||||
if (
|
if (
|
||||||
commit.author?.login === "MikeFreno" ||
|
commit.author?.login === "MikeFreno" ||
|
||||||
commit.commit?.author?.email?.includes("mike")
|
commit.commit?.author?.email?.includes("mike")
|
||||||
@@ -91,7 +86,6 @@ export const gitActivityRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Log individual repo failures but continue with others
|
|
||||||
if (
|
if (
|
||||||
error instanceof NetworkError ||
|
error instanceof NetworkError ||
|
||||||
error instanceof TimeoutError
|
error instanceof TimeoutError
|
||||||
@@ -108,7 +102,6 @@ export const gitActivityRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by date and return the most recent
|
|
||||||
allCommits.sort(
|
allCommits.sort(
|
||||||
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
|
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
|
||||||
);
|
);
|
||||||
@@ -117,7 +110,6 @@ export const gitActivityRouter = createTRPCRouter({
|
|||||||
},
|
},
|
||||||
{ maxStaleMs: 24 * 60 * 60 * 1000 } // Accept stale data up to 24 hours old
|
{ maxStaleMs: 24 * 60 * 60 * 1000 } // Accept stale data up to 24 hours old
|
||||||
).catch((error) => {
|
).catch((error) => {
|
||||||
// Final fallback - return empty array if everything fails
|
|
||||||
if (error instanceof NetworkError) {
|
if (error instanceof NetworkError) {
|
||||||
console.error("GitHub API unavailable (network error)");
|
console.error("GitHub API unavailable (network error)");
|
||||||
} else if (error instanceof TimeoutError) {
|
} else if (error instanceof TimeoutError) {
|
||||||
@@ -133,7 +125,6 @@ export const gitActivityRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Get recent commits from Gitea
|
|
||||||
getGiteaCommits: publicProcedure
|
getGiteaCommits: publicProcedure
|
||||||
.input(z.object({ limit: z.number().default(3) }))
|
.input(z.object({ limit: z.number().default(3) }))
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
@@ -141,7 +132,6 @@ export const gitActivityRouter = createTRPCRouter({
|
|||||||
`gitea-commits-${input.limit}`,
|
`gitea-commits-${input.limit}`,
|
||||||
10 * 60 * 1000, // 10 minutes
|
10 * 60 * 1000, // 10 minutes
|
||||||
async () => {
|
async () => {
|
||||||
// First, get user's repositories
|
|
||||||
const reposResponse = await fetchWithTimeout(
|
const reposResponse = await fetchWithTimeout(
|
||||||
`${env.GITEA_URL}/api/v1/users/Mike/repos?limit=100`,
|
`${env.GITEA_URL}/api/v1/users/Mike/repos?limit=100`,
|
||||||
{
|
{
|
||||||
@@ -157,7 +147,6 @@ export const gitActivityRouter = createTRPCRouter({
|
|||||||
const repos = await reposResponse.json();
|
const repos = await reposResponse.json();
|
||||||
const allCommits: GitCommit[] = [];
|
const allCommits: GitCommit[] = [];
|
||||||
|
|
||||||
// Fetch recent commits from each repo
|
|
||||||
for (const repo of repos) {
|
for (const repo of repos) {
|
||||||
if (allCommits.length >= input.limit * 3) break; // Get extra to sort later
|
if (allCommits.length >= input.limit * 3) break; // Get extra to sort later
|
||||||
|
|
||||||
@@ -199,7 +188,6 @@ export const gitActivityRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Log individual repo failures but continue with others
|
|
||||||
if (
|
if (
|
||||||
error instanceof NetworkError ||
|
error instanceof NetworkError ||
|
||||||
error instanceof TimeoutError
|
error instanceof TimeoutError
|
||||||
@@ -216,7 +204,6 @@ export const gitActivityRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by date and return the most recent
|
|
||||||
allCommits.sort(
|
allCommits.sort(
|
||||||
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
|
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
|
||||||
);
|
);
|
||||||
@@ -225,7 +212,6 @@ export const gitActivityRouter = createTRPCRouter({
|
|||||||
},
|
},
|
||||||
{ maxStaleMs: 24 * 60 * 60 * 1000 }
|
{ maxStaleMs: 24 * 60 * 60 * 1000 }
|
||||||
).catch((error) => {
|
).catch((error) => {
|
||||||
// Final fallback - return empty array if everything fails
|
|
||||||
if (error instanceof NetworkError) {
|
if (error instanceof NetworkError) {
|
||||||
console.error("Gitea API unavailable (network error)");
|
console.error("Gitea API unavailable (network error)");
|
||||||
} else if (error instanceof TimeoutError) {
|
} else if (error instanceof TimeoutError) {
|
||||||
@@ -245,7 +231,6 @@ export const gitActivityRouter = createTRPCRouter({
|
|||||||
"github-activity",
|
"github-activity",
|
||||||
10 * 60 * 1000,
|
10 * 60 * 1000,
|
||||||
async () => {
|
async () => {
|
||||||
// Use GitHub GraphQL API for contribution data
|
|
||||||
const query = `
|
const query = `
|
||||||
query($userName: String!) {
|
query($userName: String!) {
|
||||||
user(login: $userName) {
|
user(login: $userName) {
|
||||||
@@ -287,7 +272,6 @@ export const gitActivityRouter = createTRPCRouter({
|
|||||||
throw new APIError("GraphQL query failed", 500, "GraphQL Error");
|
throw new APIError("GraphQL query failed", 500, "GraphQL Error");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract contribution days from the response
|
|
||||||
const contributions: ContributionDay[] = [];
|
const contributions: ContributionDay[] = [];
|
||||||
const weeks =
|
const weeks =
|
||||||
data.data?.user?.contributionsCollection?.contributionCalendar
|
data.data?.user?.contributionsCollection?.contributionCalendar
|
||||||
@@ -327,7 +311,6 @@ export const gitActivityRouter = createTRPCRouter({
|
|||||||
"gitea-activity",
|
"gitea-activity",
|
||||||
10 * 60 * 1000,
|
10 * 60 * 1000,
|
||||||
async () => {
|
async () => {
|
||||||
// Get user's repositories
|
|
||||||
const reposResponse = await fetchWithTimeout(
|
const reposResponse = await fetchWithTimeout(
|
||||||
`${env.GITEA_URL}/api/v1/user/repos?limit=100`,
|
`${env.GITEA_URL}/api/v1/user/repos?limit=100`,
|
||||||
{
|
{
|
||||||
@@ -343,7 +326,6 @@ export const gitActivityRouter = createTRPCRouter({
|
|||||||
const repos = await reposResponse.json();
|
const repos = await reposResponse.json();
|
||||||
const contributionsByDay = new Map<string, number>();
|
const contributionsByDay = new Map<string, number>();
|
||||||
|
|
||||||
// Get commits from each repo (last 3 months to avoid too many API calls)
|
|
||||||
const threeMonthsAgo = new Date();
|
const threeMonthsAgo = new Date();
|
||||||
threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3);
|
threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3);
|
||||||
|
|
||||||
@@ -373,7 +355,6 @@ export const gitActivityRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Log individual repo failures but continue with others
|
|
||||||
if (
|
if (
|
||||||
error instanceof NetworkError ||
|
error instanceof NetworkError ||
|
||||||
error instanceof TimeoutError
|
error instanceof TimeoutError
|
||||||
@@ -387,7 +368,6 @@ export const gitActivityRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to array format
|
|
||||||
const contributions: ContributionDay[] = Array.from(
|
const contributions: ContributionDay[] = Array.from(
|
||||||
contributionsByDay.entries()
|
contributionsByDay.entries()
|
||||||
).map(([date, count]) => ({ date, count }));
|
).map(([date, count]) => ({ date, count }));
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ import potions from "~/lineage-json/item-route/potions.json";
|
|||||||
import poison from "~/lineage-json/item-route/poison.json";
|
import poison from "~/lineage-json/item-route/poison.json";
|
||||||
import staves from "~/lineage-json/item-route/staves.json";
|
import staves from "~/lineage-json/item-route/staves.json";
|
||||||
|
|
||||||
// Misc data imports
|
|
||||||
import activities from "~/lineage-json/misc-route/activities.json";
|
import activities from "~/lineage-json/misc-route/activities.json";
|
||||||
import investments from "~/lineage-json/misc-route/investments.json";
|
import investments from "~/lineage-json/misc-route/investments.json";
|
||||||
import jobs from "~/lineage-json/misc-route/jobs.json";
|
import jobs from "~/lineage-json/misc-route/jobs.json";
|
||||||
|
|||||||
@@ -96,7 +96,6 @@ export const miscRouter = createTRPCRouter({
|
|||||||
credentials: credentials
|
credentials: credentials
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sanitize the title and filename for S3 key (replace spaces with hyphens, remove special chars)
|
|
||||||
const sanitizeForS3 = (str: string) => {
|
const sanitizeForS3 = (str: string) => {
|
||||||
return str
|
return str
|
||||||
.replace(/\s+/g, "-") // Replace spaces with hyphens
|
.replace(/\s+/g, "-") // Replace spaces with hyphens
|
||||||
@@ -262,7 +261,6 @@ export const miscRouter = createTRPCRouter({
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
// Check if contact request was recently sent
|
|
||||||
const contactExp = getCookie("contactRequestSent");
|
const contactExp = getCookie("contactRequestSent");
|
||||||
let remaining = 0;
|
let remaining = 0;
|
||||||
|
|
||||||
@@ -314,7 +312,6 @@ export const miscRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Set cookie to prevent spam (60 second cooldown)
|
|
||||||
const exp = new Date(Date.now() + 1 * 60 * 1000);
|
const exp = new Date(Date.now() + 1 * 60 * 1000);
|
||||||
setCookie("contactRequestSent", exp.toUTCString(), {
|
setCookie("contactRequestSent", exp.toUTCString(), {
|
||||||
expires: exp,
|
expires: exp,
|
||||||
@@ -365,7 +362,6 @@ export const miscRouter = createTRPCRouter({
|
|||||||
sendDeletionRequestEmail: publicProcedure
|
sendDeletionRequestEmail: publicProcedure
|
||||||
.input(z.object({ email: z.string().email() }))
|
.input(z.object({ email: z.string().email() }))
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
// Check if deletion request was recently sent
|
|
||||||
const deletionExp = getCookie("deletionRequestSent");
|
const deletionExp = getCookie("deletionRequestSent");
|
||||||
let remaining = 0;
|
let remaining = 0;
|
||||||
|
|
||||||
@@ -445,7 +441,6 @@ export const miscRouter = createTRPCRouter({
|
|||||||
)
|
)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Set cookie to prevent spam (60 second cooldown)
|
|
||||||
const exp = new Date(Date.now() + 1 * 60 * 1000);
|
const exp = new Date(Date.now() + 1 * 60 * 1000);
|
||||||
setCookie("deletionRequestSent", exp.toUTCString(), {
|
setCookie("deletionRequestSent", exp.toUTCString(), {
|
||||||
expires: exp,
|
expires: exp,
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import type { User } from "~/types/user";
|
|||||||
import { toUserProfile } from "~/types/user";
|
import { toUserProfile } from "~/types/user";
|
||||||
|
|
||||||
export const userRouter = createTRPCRouter({
|
export const userRouter = createTRPCRouter({
|
||||||
// Get current user profile
|
|
||||||
getProfile: publicProcedure.query(async ({ ctx }) => {
|
getProfile: publicProcedure.query(async ({ ctx }) => {
|
||||||
const userId = await getUserID(ctx.event.nativeEvent);
|
const userId = await getUserID(ctx.event.nativeEvent);
|
||||||
|
|
||||||
@@ -41,7 +40,6 @@ export const userRouter = createTRPCRouter({
|
|||||||
return toUserProfile(user);
|
return toUserProfile(user);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Update email
|
|
||||||
updateEmail: publicProcedure
|
updateEmail: publicProcedure
|
||||||
.input(z.object({ email: z.string().email() }))
|
.input(z.object({ email: z.string().email() }))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
@@ -62,7 +60,6 @@ export const userRouter = createTRPCRouter({
|
|||||||
args: [email, 0, userId]
|
args: [email, 0, userId]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch updated user
|
|
||||||
const res = await conn.execute({
|
const res = await conn.execute({
|
||||||
sql: "SELECT * FROM User WHERE id = ?",
|
sql: "SELECT * FROM User WHERE id = ?",
|
||||||
args: [userId]
|
args: [userId]
|
||||||
@@ -70,7 +67,6 @@ export const userRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const user = res.rows[0] as unknown as User;
|
const user = res.rows[0] as unknown as User;
|
||||||
|
|
||||||
// Set email cookie for verification flow
|
|
||||||
setCookie(ctx.event.nativeEvent, "emailToken", email, {
|
setCookie(ctx.event.nativeEvent, "emailToken", email, {
|
||||||
path: "/"
|
path: "/"
|
||||||
});
|
});
|
||||||
@@ -78,7 +74,6 @@ export const userRouter = createTRPCRouter({
|
|||||||
return toUserProfile(user);
|
return toUserProfile(user);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Update display name
|
|
||||||
updateDisplayName: publicProcedure
|
updateDisplayName: publicProcedure
|
||||||
.input(z.object({ displayName: z.string().min(1).max(50) }))
|
.input(z.object({ displayName: z.string().min(1).max(50) }))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
@@ -99,7 +94,6 @@ export const userRouter = createTRPCRouter({
|
|||||||
args: [displayName, userId]
|
args: [displayName, userId]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch updated user
|
|
||||||
const res = await conn.execute({
|
const res = await conn.execute({
|
||||||
sql: "SELECT * FROM User WHERE id = ?",
|
sql: "SELECT * FROM User WHERE id = ?",
|
||||||
args: [userId]
|
args: [userId]
|
||||||
@@ -109,7 +103,6 @@ export const userRouter = createTRPCRouter({
|
|||||||
return toUserProfile(user);
|
return toUserProfile(user);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Update profile image
|
|
||||||
updateProfileImage: publicProcedure
|
updateProfileImage: publicProcedure
|
||||||
.input(z.object({ imageUrl: z.string() }))
|
.input(z.object({ imageUrl: z.string() }))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
@@ -130,7 +123,6 @@ export const userRouter = createTRPCRouter({
|
|||||||
args: [imageUrl, userId]
|
args: [imageUrl, userId]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch updated user
|
|
||||||
const res = await conn.execute({
|
const res = await conn.execute({
|
||||||
sql: "SELECT * FROM User WHERE id = ?",
|
sql: "SELECT * FROM User WHERE id = ?",
|
||||||
args: [userId]
|
args: [userId]
|
||||||
@@ -140,7 +132,6 @@ export const userRouter = createTRPCRouter({
|
|||||||
return toUserProfile(user);
|
return toUserProfile(user);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Change password (requires old password)
|
|
||||||
changePassword: publicProcedure
|
changePassword: publicProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -202,14 +193,12 @@ export const userRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update password
|
|
||||||
const newPasswordHash = await hashPassword(newPassword);
|
const newPasswordHash = await hashPassword(newPassword);
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: "UPDATE User SET password_hash = ? WHERE id = ?",
|
sql: "UPDATE User SET password_hash = ? WHERE id = ?",
|
||||||
args: [newPasswordHash, userId]
|
args: [newPasswordHash, userId]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear session cookies (force re-login)
|
|
||||||
setCookie(ctx.event.nativeEvent, "emailToken", "", {
|
setCookie(ctx.event.nativeEvent, "emailToken", "", {
|
||||||
maxAge: 0,
|
maxAge: 0,
|
||||||
path: "/"
|
path: "/"
|
||||||
@@ -222,7 +211,6 @@ export const userRouter = createTRPCRouter({
|
|||||||
return { success: true, message: "success" };
|
return { success: true, message: "success" };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Set password (for OAuth users who don't have password)
|
|
||||||
setPassword: publicProcedure
|
setPassword: publicProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -271,14 +259,12 @@ export const userRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set password
|
|
||||||
const passwordHash = await hashPassword(newPassword);
|
const passwordHash = await hashPassword(newPassword);
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: "UPDATE User SET password_hash = ? WHERE id = ?",
|
sql: "UPDATE User SET password_hash = ? WHERE id = ?",
|
||||||
args: [passwordHash, userId]
|
args: [passwordHash, userId]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear session cookies (force re-login)
|
|
||||||
setCookie(ctx.event.nativeEvent, "emailToken", "", {
|
setCookie(ctx.event.nativeEvent, "emailToken", "", {
|
||||||
maxAge: 0,
|
maxAge: 0,
|
||||||
path: "/"
|
path: "/"
|
||||||
@@ -291,7 +277,6 @@ export const userRouter = createTRPCRouter({
|
|||||||
return { success: true, message: "success" };
|
return { success: true, message: "success" };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Delete account (anonymize data)
|
|
||||||
deleteAccount: publicProcedure
|
deleteAccount: publicProcedure
|
||||||
.input(z.object({ password: z.string() }))
|
.input(z.object({ password: z.string() }))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
@@ -337,7 +322,6 @@ export const userRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Anonymize user data (don't hard delete)
|
|
||||||
await conn.execute({
|
await conn.execute({
|
||||||
sql: `UPDATE User SET
|
sql: `UPDATE User SET
|
||||||
email = ?,
|
email = ?,
|
||||||
@@ -350,7 +334,6 @@ export const userRouter = createTRPCRouter({
|
|||||||
args: [null, 0, null, "user deleted", null, null, userId]
|
args: [null, 0, null, "user deleted", null, null, userId]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear session cookies
|
|
||||||
setCookie(ctx.event.nativeEvent, "emailToken", "", {
|
setCookie(ctx.event.nativeEvent, "emailToken", "", {
|
||||||
maxAge: 0,
|
maxAge: 0,
|
||||||
path: "/"
|
path: "/"
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ export const t = initTRPC.context<Context>().create();
|
|||||||
export const createTRPCRouter = t.router;
|
export const createTRPCRouter = t.router;
|
||||||
export const publicProcedure = t.procedure;
|
export const publicProcedure = t.procedure;
|
||||||
|
|
||||||
// Middleware to enforce authentication
|
|
||||||
const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
|
const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
|
||||||
if (!ctx.userId || ctx.privilegeLevel === "anonymous") {
|
if (!ctx.userId || ctx.privilegeLevel === "anonymous") {
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED", message: "Not authenticated" });
|
throw new TRPCError({ code: "UNAUTHORIZED", message: "Not authenticated" });
|
||||||
@@ -63,7 +62,6 @@ const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Middleware to enforce admin access
|
|
||||||
const enforceUserIsAdmin = t.middleware(({ ctx, next }) => {
|
const enforceUserIsAdmin = t.middleware(({ ctx, next }) => {
|
||||||
if (ctx.privilegeLevel !== "admin") {
|
if (ctx.privilegeLevel !== "admin") {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
// Manual test file for fetch-utils error handling
|
// Manual test file for fetch-utils error handling
|
||||||
// Run with: bun run src/server/fetch-utils.test.ts
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
fetchWithTimeout,
|
fetchWithTimeout,
|
||||||
|
|||||||
Reference in New Issue
Block a user