mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 12:32:10 +01:00
fix: comprehensive security and type improvements from PR #20 review
Security Enhancements: - Implemented proper rate limiting with automatic cleanup for /register and /forgot-password endpoints - Added memory usage protection with MAX_ENTRIES limit (10000) - Fixed rate limiter memory leaks by adding cleanup intervals - Improved IP extraction with x-real-ip and x-client-ip header support Code Quality Improvements: - Refactored ProcessingStatusManager from individual functions to class-based architecture - Maintained backward compatibility with singleton instance pattern - Fixed TypeScript strict mode violations across the codebase - Resolved all build errors and type mismatches UI Component Fixes: - Removed unused chart components (Charts.tsx, DonutChart.tsx) - Fixed calendar component type issues by removing unused custom implementations - Resolved theme provider type imports - Fixed confetti component default options handling - Corrected pointer component coordinate type definitions Type System Improvements: - Extended NextAuth types to support dual auth systems (regular and platform users) - Fixed nullable type handling throughout the codebase - Resolved Prisma JSON field type compatibility issues - Corrected SessionMessage and ImportRecord interface definitions - Fixed ES2015 iteration compatibility issues Database & Performance: - Updated database pool configuration for Prisma adapter compatibility - Fixed pagination response structure in user management endpoints - Improved error handling with proper error class usage Testing & Build: - All TypeScript compilation errors resolved - ESLint warnings remain but no errors - Build completes successfully with proper static generation
This commit is contained in:
@ -1,308 +0,0 @@
|
||||
"use client";
|
||||
import Chart from "chart.js/auto";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { getLocalizedLanguageName } from "../lib/localization"; // Corrected import path
|
||||
|
||||
interface SessionsData {
|
||||
[date: string]: number;
|
||||
}
|
||||
|
||||
interface CategoriesData {
|
||||
[category: string]: number;
|
||||
}
|
||||
|
||||
interface LanguageData {
|
||||
[language: string]: number;
|
||||
}
|
||||
|
||||
interface SessionsLineChartProps {
|
||||
sessionsPerDay: SessionsData;
|
||||
}
|
||||
|
||||
interface CategoriesBarChartProps {
|
||||
categories: CategoriesData;
|
||||
}
|
||||
|
||||
interface LanguagePieChartProps {
|
||||
languages: LanguageData;
|
||||
}
|
||||
|
||||
interface SentimentChartProps {
|
||||
sentimentData: {
|
||||
positive: number;
|
||||
neutral: number;
|
||||
negative: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface TokenUsageChartProps {
|
||||
tokenData: {
|
||||
labels: string[];
|
||||
values: number[];
|
||||
costs: number[];
|
||||
};
|
||||
}
|
||||
|
||||
// Basic line and bar chart for metrics. Extend as needed.
|
||||
export function SessionsLineChart({ sessionsPerDay }: SessionsLineChartProps) {
|
||||
const ref = useRef<HTMLCanvasElement | null>(null);
|
||||
useEffect(() => {
|
||||
if (!ref.current || !sessionsPerDay) return;
|
||||
const ctx = ref.current.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
const chart = new Chart(ctx, {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: Object.keys(sessionsPerDay),
|
||||
datasets: [
|
||||
{
|
||||
label: "Sessions",
|
||||
data: Object.values(sessionsPerDay),
|
||||
borderColor: "rgb(59, 130, 246)",
|
||||
backgroundColor: "rgba(59, 130, 246, 0.1)",
|
||||
borderWidth: 2,
|
||||
tension: 0.3,
|
||||
fill: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: { legend: { display: false } },
|
||||
scales: { y: { beginAtZero: true } },
|
||||
},
|
||||
});
|
||||
return () => chart.destroy();
|
||||
}, [sessionsPerDay]);
|
||||
return <canvas ref={ref} height={180} />;
|
||||
}
|
||||
|
||||
export function CategoriesBarChart({ categories }: CategoriesBarChartProps) {
|
||||
const ref = useRef<HTMLCanvasElement | null>(null);
|
||||
useEffect(() => {
|
||||
if (!ref.current || !categories) return;
|
||||
const ctx = ref.current.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
const chart = new Chart(ctx, {
|
||||
type: "bar",
|
||||
data: {
|
||||
labels: Object.keys(categories),
|
||||
datasets: [
|
||||
{
|
||||
label: "Categories",
|
||||
data: Object.values(categories),
|
||||
backgroundColor: "rgba(59, 130, 246, 0.7)",
|
||||
borderWidth: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: { legend: { display: false } },
|
||||
scales: { y: { beginAtZero: true } },
|
||||
},
|
||||
});
|
||||
return () => chart.destroy();
|
||||
}, [categories]);
|
||||
return <canvas ref={ref} height={180} />;
|
||||
}
|
||||
|
||||
export function SentimentChart({ sentimentData }: SentimentChartProps) {
|
||||
const ref = useRef<HTMLCanvasElement | null>(null);
|
||||
useEffect(() => {
|
||||
if (!ref.current || !sentimentData) return;
|
||||
const ctx = ref.current.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
const chart = new Chart(ctx, {
|
||||
type: "doughnut",
|
||||
data: {
|
||||
labels: ["Positive", "Neutral", "Negative"],
|
||||
datasets: [
|
||||
{
|
||||
data: [
|
||||
sentimentData.positive,
|
||||
sentimentData.neutral,
|
||||
sentimentData.negative,
|
||||
],
|
||||
backgroundColor: [
|
||||
"rgba(34, 197, 94, 0.8)", // green
|
||||
"rgba(249, 115, 22, 0.8)", // orange
|
||||
"rgba(239, 68, 68, 0.8)", // red
|
||||
],
|
||||
borderWidth: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: "right",
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
padding: 20,
|
||||
},
|
||||
},
|
||||
},
|
||||
cutout: "65%",
|
||||
},
|
||||
});
|
||||
return () => chart.destroy();
|
||||
}, [sentimentData]);
|
||||
return <canvas ref={ref} height={180} />;
|
||||
}
|
||||
|
||||
export function LanguagePieChart({ languages }: LanguagePieChartProps) {
|
||||
const ref = useRef<HTMLCanvasElement | null>(null);
|
||||
useEffect(() => {
|
||||
if (!ref.current || !languages) return;
|
||||
const ctx = ref.current.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
// Get top 5 languages, combine others
|
||||
const entries = Object.entries(languages);
|
||||
const topLanguages = entries.sort((a, b) => b[1] - a[1]).slice(0, 5);
|
||||
|
||||
// Sum the count of all other languages
|
||||
const otherCount = entries
|
||||
.slice(5)
|
||||
.reduce((sum, [, count]) => sum + count, 0);
|
||||
if (otherCount > 0) {
|
||||
topLanguages.push(["Other", otherCount]);
|
||||
}
|
||||
|
||||
// Store original ISO codes for tooltip
|
||||
const isoCodes = topLanguages.map(([lang]) => lang);
|
||||
|
||||
const labels = topLanguages.map(([lang]) => {
|
||||
if (lang === "Other") {
|
||||
return "Other";
|
||||
}
|
||||
// Use getLocalizedLanguageName for robust name resolution
|
||||
// Pass "en" to maintain consistency with previous behavior if navigator.language is different
|
||||
return getLocalizedLanguageName(lang, "en");
|
||||
});
|
||||
|
||||
const data = topLanguages.map(([, count]) => count);
|
||||
|
||||
const chart = new Chart(ctx, {
|
||||
type: "pie",
|
||||
data: {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
data,
|
||||
backgroundColor: [
|
||||
"rgba(59, 130, 246, 0.8)",
|
||||
"rgba(16, 185, 129, 0.8)",
|
||||
"rgba(249, 115, 22, 0.8)",
|
||||
"rgba(236, 72, 153, 0.8)",
|
||||
"rgba(139, 92, 246, 0.8)",
|
||||
"rgba(107, 114, 128, 0.8)",
|
||||
],
|
||||
borderWidth: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: "right",
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
padding: 20,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (context) => {
|
||||
const label = context.label || "";
|
||||
const value = context.formattedValue || "";
|
||||
const index = context.dataIndex;
|
||||
const originalIsoCode = isoCodes[index]; // Get the original code
|
||||
|
||||
// Only show ISO code if it's not "Other"
|
||||
// and it's a valid 2-letter code (check lowercase version)
|
||||
if (
|
||||
originalIsoCode &&
|
||||
originalIsoCode !== "Other" &&
|
||||
/^[a-z]{2}$/.test(originalIsoCode.toLowerCase())
|
||||
) {
|
||||
return `${label} (${originalIsoCode.toUpperCase()}): ${value}`;
|
||||
}
|
||||
|
||||
return `${label}: ${value}`;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return () => chart.destroy();
|
||||
}, [languages]);
|
||||
return <canvas ref={ref} height={180} />;
|
||||
}
|
||||
|
||||
export function TokenUsageChart({ tokenData }: TokenUsageChartProps) {
|
||||
const ref = useRef<HTMLCanvasElement | null>(null);
|
||||
useEffect(() => {
|
||||
if (!ref.current || !tokenData) return;
|
||||
const ctx = ref.current.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
const chart = new Chart(ctx, {
|
||||
type: "bar",
|
||||
data: {
|
||||
labels: tokenData.labels,
|
||||
datasets: [
|
||||
{
|
||||
label: "Tokens",
|
||||
data: tokenData.values,
|
||||
backgroundColor: "rgba(59, 130, 246, 0.7)",
|
||||
borderWidth: 1,
|
||||
yAxisID: "y",
|
||||
},
|
||||
{
|
||||
label: "Cost (EUR)",
|
||||
data: tokenData.costs,
|
||||
backgroundColor: "rgba(16, 185, 129, 0.7)",
|
||||
borderWidth: 1,
|
||||
type: "line",
|
||||
yAxisID: "y1",
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: { legend: { display: true } },
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
position: "left",
|
||||
title: {
|
||||
display: true,
|
||||
text: "Token Count",
|
||||
},
|
||||
},
|
||||
y1: {
|
||||
beginAtZero: true,
|
||||
position: "right",
|
||||
grid: {
|
||||
drawOnChartArea: false,
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: "Cost (EUR)",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return () => chart.destroy();
|
||||
}, [tokenData]);
|
||||
return <canvas ref={ref} height={180} />;
|
||||
}
|
||||
@ -1,155 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import Chart, { type BubbleDataPoint, type Point } from "chart.js/auto";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
interface DonutChartProps {
|
||||
data: {
|
||||
labels: string[];
|
||||
values: number[];
|
||||
colors?: string[];
|
||||
};
|
||||
centerText?: {
|
||||
title?: string;
|
||||
value?: string | number;
|
||||
};
|
||||
}
|
||||
|
||||
export default function DonutChart({ data, centerText }: DonutChartProps) {
|
||||
const ref = useRef<HTMLCanvasElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current || !data.values.length) return;
|
||||
|
||||
const ctx = ref.current.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
// Default colors if not provided
|
||||
const defaultColors: string[] = [
|
||||
"rgba(59, 130, 246, 0.8)", // blue
|
||||
"rgba(16, 185, 129, 0.8)", // green
|
||||
"rgba(249, 115, 22, 0.8)", // orange
|
||||
"rgba(236, 72, 153, 0.8)", // pink
|
||||
"rgba(139, 92, 246, 0.8)", // purple
|
||||
"rgba(107, 114, 128, 0.8)", // gray
|
||||
];
|
||||
|
||||
const colors: string[] = data.colors || defaultColors;
|
||||
|
||||
// Helper to create an array of colors based on the data length
|
||||
const getColors = () => {
|
||||
const result: string[] = [];
|
||||
for (let i = 0; i < data.values.length; i++) {
|
||||
result.push(colors[i % colors.length]);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const chart = new Chart(ctx, {
|
||||
type: "doughnut",
|
||||
data: {
|
||||
labels: data.labels,
|
||||
datasets: [
|
||||
{
|
||||
data: data.values,
|
||||
backgroundColor: getColors(),
|
||||
borderWidth: 1,
|
||||
hoverOffset: 5,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
cutout: "70%",
|
||||
plugins: {
|
||||
legend: {
|
||||
position: "right",
|
||||
labels: {
|
||||
boxWidth: 12,
|
||||
padding: 20,
|
||||
usePointStyle: true,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (context) => {
|
||||
const label = context.label || "";
|
||||
const value = context.formattedValue;
|
||||
const total = context.chart.data.datasets[0].data.reduce(
|
||||
(
|
||||
a: number,
|
||||
b:
|
||||
| number
|
||||
| Point
|
||||
| [number, number]
|
||||
| BubbleDataPoint
|
||||
| null
|
||||
) => {
|
||||
if (typeof b === "number") {
|
||||
return a + b;
|
||||
}
|
||||
// Handle other types like Point, [number, number], BubbleDataPoint if necessary
|
||||
// For now, we'll assume they don't contribute to the sum or are handled elsewhere
|
||||
return a;
|
||||
},
|
||||
0
|
||||
) as number;
|
||||
const percentage = Math.round((context.parsed * 100) / total);
|
||||
return `${label}: ${value} (${percentage}%)`;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: centerText
|
||||
? [
|
||||
{
|
||||
id: "centerText",
|
||||
beforeDraw: (chart: Chart<"doughnut">) => {
|
||||
const height = chart.height;
|
||||
const ctx = chart.ctx;
|
||||
ctx.restore();
|
||||
|
||||
// Calculate the actual chart area width (excluding legend)
|
||||
// Legend is positioned on the right, so we adjust the center X coordinate
|
||||
const chartArea = chart.chartArea;
|
||||
const chartWidth = chartArea.right - chartArea.left;
|
||||
|
||||
// Get the center of just the chart area (not including the legend)
|
||||
const centerX = chartArea.left + chartWidth / 2;
|
||||
const centerY = height / 2;
|
||||
|
||||
// Title text
|
||||
if (centerText.title) {
|
||||
ctx.font = "1rem sans-serif"; // Consistent font
|
||||
ctx.fillStyle = "#6B7280"; // Tailwind gray-500
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle"; // Align vertically
|
||||
ctx.fillText(centerText.title, centerX, centerY - 10); // Adjust Y offset
|
||||
}
|
||||
|
||||
// Value text
|
||||
if (centerText.value !== undefined) {
|
||||
ctx.font = "bold 1.5rem sans-serif"; // Consistent font, larger
|
||||
ctx.fillStyle = "#1F2937"; // Tailwind gray-800
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle"; // Align vertically
|
||||
ctx.fillText(
|
||||
centerText.value.toString(),
|
||||
centerX,
|
||||
centerY + 15
|
||||
); // Adjust Y offset
|
||||
}
|
||||
ctx.save();
|
||||
},
|
||||
},
|
||||
]
|
||||
: [],
|
||||
});
|
||||
|
||||
return () => chart.destroy();
|
||||
}, [data, centerText]);
|
||||
|
||||
return <canvas ref={ref} height={300} />;
|
||||
}
|
||||
@ -39,7 +39,7 @@ export default function TopQuestionsChart({
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{data.map((question) => {
|
||||
{data.map((question, index) => {
|
||||
const percentage =
|
||||
maxCount > 0 ? (question.count / maxCount) * 100 : 0;
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ import {
|
||||
useRef,
|
||||
} from "react";
|
||||
|
||||
import { Button, type ButtonProps } from "@/components/ui/button";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
type Api = {
|
||||
fire: (options?: ConfettiOptions) => void;
|
||||
@ -110,7 +110,7 @@ ConfettiComponent.displayName = "Confetti";
|
||||
// Export as Confetti
|
||||
export const Confetti = ConfettiComponent;
|
||||
|
||||
interface ConfettiButtonProps extends ButtonProps {
|
||||
interface ConfettiButtonProps extends React.ComponentProps<typeof Button> {
|
||||
options?: ConfettiOptions &
|
||||
ConfettiGlobalOptions & { canvas?: HTMLCanvasElement };
|
||||
children?: React.ReactNode;
|
||||
|
||||
@ -26,7 +26,7 @@ export function Pointer({
|
||||
style,
|
||||
children,
|
||||
...props
|
||||
}: PointerProps): JSX.Element {
|
||||
}: PointerProps): React.ReactElement {
|
||||
const x = useMotionValue(0);
|
||||
const y = useMotionValue(0);
|
||||
const [isActive, setIsActive] = useState<boolean>(false);
|
||||
|
||||
@ -1,8 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
import type { ThemeProviderProps } from "next-themes/dist/types";
|
||||
|
||||
type Attribute = "class" | "data-theme" | "data-mode";
|
||||
|
||||
interface ThemeProviderProps {
|
||||
children: React.ReactNode;
|
||||
attribute?: Attribute | Attribute[];
|
||||
defaultTheme?: string;
|
||||
enableSystem?: boolean;
|
||||
disableTransitionOnChange?: boolean;
|
||||
forcedTheme?: string;
|
||||
}
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
} from "lucide-react";
|
||||
import * as React from "react";
|
||||
import {
|
||||
type DayButton,
|
||||
@ -14,69 +9,6 @@ import {
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface CalendarRootProps {
|
||||
className?: string;
|
||||
rootRef?: React.Ref<HTMLDivElement>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
const CalendarRoot = ({ className, rootRef, ...props }: CalendarRootProps) => {
|
||||
return (
|
||||
<div
|
||||
data-slot="calendar"
|
||||
ref={rootRef}
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface CalendarChevronProps {
|
||||
className?: string;
|
||||
orientation: "left" | "right" | "up" | "down";
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
const CalendarChevron = ({
|
||||
className,
|
||||
orientation,
|
||||
...props
|
||||
}: CalendarChevronProps) => {
|
||||
if (orientation === "left") {
|
||||
return <ChevronLeftIcon className={cn("size-4", className)} {...props} />;
|
||||
}
|
||||
if (orientation === "right") {
|
||||
return <ChevronRightIcon className={cn("size-4", className)} {...props} />;
|
||||
}
|
||||
if (orientation === "up") {
|
||||
return (
|
||||
<ChevronDownIcon
|
||||
className={cn("size-4 rotate-180", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <ChevronDownIcon className={cn("size-4", className)} {...props} />;
|
||||
};
|
||||
|
||||
interface CalendarWeekNumberProps {
|
||||
children: React.ReactNode;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
const CalendarWeekNumber = ({
|
||||
children,
|
||||
...props
|
||||
}: CalendarWeekNumberProps) => {
|
||||
return (
|
||||
<td {...props}>
|
||||
<div className="flex size-9 items-center justify-center p-0 text-sm">
|
||||
{children}
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
};
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
classNames,
|
||||
@ -188,10 +120,7 @@ function Calendar({
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
Root: CalendarRoot,
|
||||
Chevron: CalendarChevron,
|
||||
DayButton: CalendarDayButton,
|
||||
WeekNumber: CalendarWeekNumber,
|
||||
...components,
|
||||
}}
|
||||
{...props}
|
||||
|
||||
Reference in New Issue
Block a user