Login sequence and inventory/protocol storage groundwork
This commit is contained in:
60
App.tsx
60
App.tsx
@@ -2,16 +2,21 @@ import { useState } from "react";
|
||||
import { Dashboard } from "./components/Dashboard";
|
||||
import { Inventory } from "./components/Inventory";
|
||||
import { ProtocolChecker } from "./components/ProtocolChecker";
|
||||
import { useSession, signIn, signOut } from "./lib/auth-client";
|
||||
import { Button } from "./components/ui/button";
|
||||
import { Card, CardContent } from "./components/ui/card";
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Package,
|
||||
FileCheck
|
||||
FileCheck,
|
||||
LogOut,
|
||||
} from "lucide-react";
|
||||
const logo = "/logo.png";
|
||||
|
||||
type Tab = "dashboard" | "inventory" | "protocol";
|
||||
|
||||
export default function App() {
|
||||
const { data: session, isPending } = useSession();
|
||||
const [activeTab, setActiveTab] = useState<Tab>("dashboard");
|
||||
|
||||
const navItems = [
|
||||
@@ -20,6 +25,46 @@ export default function App() {
|
||||
{ id: "protocol" as Tab, label: "Protocol Checker", icon: FileCheck },
|
||||
];
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center bg-secondary">
|
||||
<img src={logo} alt="LabWise" className="h-12 opacity-50 animate-pulse" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center bg-secondary">
|
||||
<Card className="w-full max-w-sm">
|
||||
<CardContent className="p-8 space-y-6">
|
||||
<div className="flex justify-center">
|
||||
<img src={logo} alt="LabWise" className="h-12" />
|
||||
</div>
|
||||
<p className="text-center text-muted-foreground text-sm">
|
||||
Sign in to access your lab inventory and protocols.
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => signIn.social({ provider: "google", callbackURL: "/" })}
|
||||
>
|
||||
Sign in with Google
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => signIn.social({ provider: "github", callbackURL: "/" })}
|
||||
>
|
||||
Sign in with GitHub
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-secondary">
|
||||
{/* Sidebar */}
|
||||
@@ -47,6 +92,19 @@ export default function App() {
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="p-4 border-t border-border">
|
||||
<div className="text-xs text-muted-foreground mb-2 truncate px-1">
|
||||
{session.user.email}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => signOut()}
|
||||
className="w-full flex items-center gap-3 px-4 py-2 rounded-lg text-muted-foreground hover:bg-muted transition-colors text-sm"
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
<span>Sign out</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useState, useRef } from "react";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { chemicalsApi } from "../lib/api";
|
||||
import type { ChemicalInventory } from "../shared/types";
|
||||
import { Card } from "./ui/card";
|
||||
import { Button } from "./ui/button";
|
||||
import { Input } from "./ui/input";
|
||||
@@ -34,47 +36,6 @@ import {
|
||||
SelectValue,
|
||||
} from "./ui/select";
|
||||
|
||||
interface ChemicalInventory {
|
||||
id: string;
|
||||
// Required fields (red)
|
||||
piFirstName: string;
|
||||
physicalState: string;
|
||||
chemicalName: string;
|
||||
bldgCode: string;
|
||||
lab: string;
|
||||
storageLocation: string;
|
||||
storageDevice: string;
|
||||
numberOfContainers: string;
|
||||
amountPerContainer: string;
|
||||
unitOfMeasure: string;
|
||||
casNumber: string;
|
||||
// Nice to have fields
|
||||
chemicalFormula?: string;
|
||||
molecularWeight?: string;
|
||||
vendor?: string;
|
||||
catalogNumber?: string;
|
||||
foundInCatalog?: string;
|
||||
poNumber?: string;
|
||||
receiptDate?: string;
|
||||
openDate?: string;
|
||||
maxOnHand?: string;
|
||||
expirationDate?: string;
|
||||
contact?: string;
|
||||
comments?: string;
|
||||
dateEntered?: string;
|
||||
permitNumber?: string;
|
||||
barcode?: string;
|
||||
lastChanged?: string;
|
||||
concentration?: string;
|
||||
chemicalNumber?: string;
|
||||
lotNumber?: string;
|
||||
multipleCAS?: string;
|
||||
msds?: string;
|
||||
// Special fields
|
||||
percentageFull?: number; // blue
|
||||
needsManualEntry?: string[]; // yellow highlight
|
||||
scannedImage?: string;
|
||||
}
|
||||
|
||||
const storageDeviceOptions = [
|
||||
"Aerosol Can",
|
||||
@@ -102,71 +63,15 @@ export function Inventory() {
|
||||
const [shareDialogOpen, setShareDialogOpen] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [inventory, setInventory] = useState<ChemicalInventory[]>([
|
||||
{
|
||||
id: "1",
|
||||
piFirstName: "Dr. Smith",
|
||||
physicalState: "Liquid",
|
||||
chemicalName: "Acetone",
|
||||
bldgCode: "BLD-A",
|
||||
lab: "Lab 201",
|
||||
storageLocation: "Cabinet A-3",
|
||||
storageDevice: "Glass Bottle",
|
||||
numberOfContainers: "2",
|
||||
amountPerContainer: "1",
|
||||
unitOfMeasure: "L",
|
||||
casNumber: "67-64-1",
|
||||
chemicalFormula: "C3H6O",
|
||||
molecularWeight: "58.08",
|
||||
vendor: "Sigma-Aldrich",
|
||||
catalogNumber: "179124",
|
||||
expirationDate: "2025-06-15",
|
||||
lotNumber: "SLCD1234",
|
||||
percentageFull: 65,
|
||||
dateEntered: "2024-11-15",
|
||||
openDate: "2024-11-15"
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
piFirstName: "Dr. Johnson",
|
||||
physicalState: "Solid",
|
||||
chemicalName: "Sodium Hydroxide",
|
||||
bldgCode: "BLD-B",
|
||||
lab: "Lab 305",
|
||||
storageLocation: "Cabinet B-1",
|
||||
storageDevice: "Plastic Bottle",
|
||||
numberOfContainers: "1",
|
||||
amountPerContainer: "500",
|
||||
unitOfMeasure: "g",
|
||||
casNumber: "1310-73-2",
|
||||
chemicalFormula: "NaOH",
|
||||
molecularWeight: "39.997",
|
||||
vendor: "Fisher Scientific",
|
||||
expirationDate: "2025-10-22",
|
||||
lotNumber: "FS9876",
|
||||
percentageFull: 15,
|
||||
dateEntered: "2024-10-22"
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
piFirstName: "Dr. Smith",
|
||||
physicalState: "Liquid",
|
||||
chemicalName: "Hydrochloric Acid",
|
||||
bldgCode: "BLD-A",
|
||||
lab: "Lab 201",
|
||||
storageLocation: "Acid Cabinet",
|
||||
storageDevice: "Glass Bottle",
|
||||
numberOfContainers: "1",
|
||||
amountPerContainer: "1",
|
||||
unitOfMeasure: "L",
|
||||
casNumber: "7647-01-0",
|
||||
chemicalFormula: "HCl",
|
||||
vendor: "Fisher Scientific",
|
||||
expirationDate: "2024-12-10",
|
||||
percentageFull: 80,
|
||||
dateEntered: "2024-08-15"
|
||||
}
|
||||
]);
|
||||
const [inventory, setInventory] = useState<ChemicalInventory[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
chemicalsApi.list()
|
||||
.then(data => setInventory(data))
|
||||
.catch(console.error)
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
const handlePhotoCapture = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
@@ -211,38 +116,30 @@ export function Inventory() {
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
const handleAddFromScan = () => {
|
||||
const handleAddFromScan = async () => {
|
||||
if (extractedData) {
|
||||
const newChemical: ChemicalInventory = {
|
||||
id: Date.now().toString(),
|
||||
piFirstName: extractedData.piFirstName || "",
|
||||
physicalState: extractedData.physicalState || "",
|
||||
chemicalName: extractedData.chemicalName || "",
|
||||
bldgCode: extractedData.bldgCode || "",
|
||||
lab: extractedData.lab || "",
|
||||
storageLocation: extractedData.storageLocation || "",
|
||||
storageDevice: extractedData.storageDevice || "Glass Bottle",
|
||||
numberOfContainers: extractedData.numberOfContainers || "1",
|
||||
amountPerContainer: extractedData.amountPerContainer || "",
|
||||
unitOfMeasure: extractedData.unitOfMeasure || "",
|
||||
casNumber: extractedData.casNumber || "",
|
||||
chemicalFormula: extractedData.chemicalFormula,
|
||||
molecularWeight: extractedData.molecularWeight,
|
||||
vendor: extractedData.vendor,
|
||||
catalogNumber: extractedData.catalogNumber,
|
||||
expirationDate: extractedData.expirationDate,
|
||||
lotNumber: extractedData.lotNumber,
|
||||
concentration: extractedData.concentration,
|
||||
percentageFull: extractedData.percentageFull,
|
||||
dateEntered: new Date().toISOString().split('T')[0],
|
||||
needsManualEntry: extractedData.needsManualEntry,
|
||||
scannedImage: extractedData.scannedImage
|
||||
};
|
||||
|
||||
setInventory([newChemical, ...inventory]);
|
||||
setCapturedImage(null);
|
||||
setExtractedData(null);
|
||||
setIsPhotoDialogOpen(false);
|
||||
try {
|
||||
const saved = await chemicalsApi.create({
|
||||
...extractedData,
|
||||
piFirstName: extractedData.piFirstName || "",
|
||||
physicalState: extractedData.physicalState || "",
|
||||
chemicalName: extractedData.chemicalName || "",
|
||||
bldgCode: extractedData.bldgCode || "",
|
||||
lab: extractedData.lab || "",
|
||||
storageLocation: extractedData.storageLocation || "",
|
||||
storageDevice: extractedData.storageDevice || "Glass Bottle",
|
||||
numberOfContainers: extractedData.numberOfContainers || "1",
|
||||
amountPerContainer: extractedData.amountPerContainer || "",
|
||||
unitOfMeasure: extractedData.unitOfMeasure || "",
|
||||
casNumber: extractedData.casNumber || "",
|
||||
});
|
||||
setInventory([saved, ...inventory]);
|
||||
setCapturedImage(null);
|
||||
setExtractedData(null);
|
||||
setIsPhotoDialogOpen(false);
|
||||
} catch (err) {
|
||||
console.error("Failed to save chemical:", err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -615,6 +512,9 @@ export function Inventory() {
|
||||
</div>
|
||||
|
||||
{/* Spreadsheet Table */}
|
||||
{isLoading && (
|
||||
<p className="text-center text-muted-foreground py-8">Loading inventory...</p>
|
||||
)}
|
||||
<Card className="overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { protocolsApi } from "../lib/api";
|
||||
import { Card } from "./ui/card";
|
||||
import { Button } from "./ui/button";
|
||||
import { Textarea } from "./ui/textarea";
|
||||
@@ -18,13 +19,7 @@ import {
|
||||
import { Alert, AlertDescription } from "./ui/alert";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs";
|
||||
|
||||
interface SafetyIssue {
|
||||
type: "critical" | "warning" | "suggestion";
|
||||
category: string;
|
||||
message: string;
|
||||
source?: string;
|
||||
sourceUrl?: string;
|
||||
}
|
||||
import type { SafetyIssue } from "../shared/types";
|
||||
|
||||
interface ChatMessage {
|
||||
id: string;
|
||||
@@ -85,12 +80,21 @@ export function ProtocolChecker() {
|
||||
}
|
||||
];
|
||||
|
||||
const handleAnalyze = () => {
|
||||
const handleAnalyze = async () => {
|
||||
if (!protocol.trim()) return;
|
||||
setIsAnalyzing(true);
|
||||
setTimeout(() => {
|
||||
setIsAnalyzing(false);
|
||||
try {
|
||||
const saved = await protocolsApi.createFromText(
|
||||
`Protocol ${new Date().toLocaleDateString()}`,
|
||||
protocol
|
||||
);
|
||||
await protocolsApi.saveAnalysis(saved.id, mockIssues);
|
||||
setAnalyzed(true);
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
console.error("Failed to save protocol:", err);
|
||||
} finally {
|
||||
setIsAnalyzing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendMessage = () => {
|
||||
|
||||
63
lib/api.ts
Normal file
63
lib/api.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { ChemicalInventory, SafetyIssue } from '../shared/types';
|
||||
|
||||
async function apiFetch(path: string, options: RequestInit = {}): Promise<Response> {
|
||||
const res = await fetch(path, {
|
||||
...options,
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({ error: res.statusText }));
|
||||
throw new Error(err.error || 'API error');
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
export const chemicalsApi = {
|
||||
list: (): Promise<ChemicalInventory[]> =>
|
||||
apiFetch('/api/chemicals').then(r => r.json()),
|
||||
|
||||
create: (data: Partial<ChemicalInventory>): Promise<ChemicalInventory> =>
|
||||
apiFetch('/api/chemicals', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
}).then(r => r.json()),
|
||||
|
||||
update: (id: string, data: Partial<ChemicalInventory>): Promise<ChemicalInventory> =>
|
||||
apiFetch(`/api/chemicals/${id}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(data),
|
||||
}).then(r => r.json()),
|
||||
|
||||
remove: (id: string): Promise<void> =>
|
||||
apiFetch(`/api/chemicals/${id}`, { method: 'DELETE' }).then(() => undefined),
|
||||
};
|
||||
|
||||
export const protocolsApi = {
|
||||
list: () => apiFetch('/api/protocols').then(r => r.json()),
|
||||
|
||||
create: (formData: FormData) =>
|
||||
fetch('/api/protocols', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: formData, // Don't set Content-Type — browser sets multipart boundary
|
||||
}).then(r => r.json()),
|
||||
|
||||
createFromText: (title: string, content: string) =>
|
||||
apiFetch('/api/protocols', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ title, content }),
|
||||
}).then(r => r.json()),
|
||||
|
||||
saveAnalysis: (id: string, results: SafetyIssue[]) =>
|
||||
apiFetch(`/api/protocols/${id}/analysis`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ analysis_results: results }),
|
||||
}).then(r => r.json()),
|
||||
|
||||
remove: (id: string): Promise<void> =>
|
||||
apiFetch(`/api/protocols/${id}`, { method: 'DELETE' }).then(() => undefined),
|
||||
};
|
||||
7
lib/auth-client.ts
Normal file
7
lib/auth-client.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { createAuthClient } from 'better-auth/react';
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: `${window.location.origin}/api/auth`,
|
||||
});
|
||||
|
||||
export const { signIn, signOut, useSession } = authClient;
|
||||
789
package-lock.json
generated
789
package-lock.json
generated
@@ -35,6 +35,7 @@
|
||||
"@radix-ui/react-toggle-group": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "^4.2.2",
|
||||
"better-auth": "^1.0.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
@@ -57,6 +58,7 @@
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"concurrently": "^9.1.2",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^6.2.0"
|
||||
@@ -353,6 +355,122 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@better-auth/core": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.5.5.tgz",
|
||||
"integrity": "sha512-1oR/2jAp821Dcf67kQYHUoyNcdc1TcShfw4QMK0YTVntuRES5mUOyvEJql5T6eIuLfaqaN4LOF78l0FtF66HXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.1.0",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@better-auth/utils": "0.3.1",
|
||||
"@better-fetch/fetch": "1.1.21",
|
||||
"@cloudflare/workers-types": ">=4",
|
||||
"better-call": "1.3.2",
|
||||
"jose": "^6.1.0",
|
||||
"kysely": "^0.28.5",
|
||||
"nanostores": "^1.0.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@cloudflare/workers-types": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@better-auth/drizzle-adapter": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@better-auth/drizzle-adapter/-/drizzle-adapter-1.5.5.tgz",
|
||||
"integrity": "sha512-HAi9xAP40oDt48QZeYBFTcmg3vt1Jik90GwoRIfangd7VGbxesIIDBJSnvwMbZ52GBIc6+V4FRw9lasNiNrPfw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@better-auth/core": "1.5.5",
|
||||
"@better-auth/utils": "^0.3.0",
|
||||
"drizzle-orm": ">=0.41.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"drizzle-orm": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@better-auth/kysely-adapter": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@better-auth/kysely-adapter/-/kysely-adapter-1.5.5.tgz",
|
||||
"integrity": "sha512-LmHffIVnqbfsxcxckMOoE8MwibWrbVFch+kwPKJ5OFDFv6lin75ufN7ZZ7twH0IMPLT/FcgzaRjP8jRrXRef9g==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@better-auth/core": "1.5.5",
|
||||
"@better-auth/utils": "^0.3.0",
|
||||
"kysely": "^0.27.0 || ^0.28.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@better-auth/memory-adapter": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@better-auth/memory-adapter/-/memory-adapter-1.5.5.tgz",
|
||||
"integrity": "sha512-4X0j1/2L+nsgmObjmy9xEGUFWUv38Qjthp558fwS3DAp6ueWWyCaxaD6VJZ7m5qPNMrsBStO5WGP8CmJTEWm7g==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@better-auth/core": "1.5.5",
|
||||
"@better-auth/utils": "^0.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@better-auth/mongo-adapter": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@better-auth/mongo-adapter/-/mongo-adapter-1.5.5.tgz",
|
||||
"integrity": "sha512-P1J9ljL5X5k740I8Rx1esPWNgWYPdJR5hf2CY7BwDSrQFPUHuzeCg0YhtEEP55niNateTXhBqGAcy0fVOeamZg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@better-auth/core": "1.5.5",
|
||||
"@better-auth/utils": "^0.3.0",
|
||||
"mongodb": "^6.0.0 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@better-auth/prisma-adapter": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@better-auth/prisma-adapter/-/prisma-adapter-1.5.5.tgz",
|
||||
"integrity": "sha512-CliDd78CXHzzwQIXhCdwGr5Ml53i6JdCHWV7PYwTIJz9EAm6qb2RVBdpP3nqEfNjINGM22A6gfleCgCdZkTIZg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@better-auth/core": "1.5.5",
|
||||
"@better-auth/utils": "^0.3.0",
|
||||
"@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
||||
"prisma": "^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@prisma/client": {
|
||||
"optional": true
|
||||
},
|
||||
"prisma": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@better-auth/telemetry": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@better-auth/telemetry/-/telemetry-1.5.5.tgz",
|
||||
"integrity": "sha512-1+lklxArn4IMHuU503RcPdXrSG2tlXt4jnGG3omolmspQ7tktg/Y9XO/yAkYDurtvMn1xJ8X1Ov01Ji/r5s9BQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@better-auth/utils": "0.3.1",
|
||||
"@better-fetch/fetch": "1.1.21"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@better-auth/core": "1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@better-auth/utils": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.3.1.tgz",
|
||||
"integrity": "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@better-fetch/fetch": {
|
||||
"version": "1.1.21",
|
||||
"resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.1.21.tgz",
|
||||
"integrity": "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="
|
||||
},
|
||||
"node_modules/@date-fns/tz": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.4.1.tgz",
|
||||
@@ -889,6 +1007,40 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@mongodb-js/saslprep": {
|
||||
"version": "1.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz",
|
||||
"integrity": "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"sparse-bitfield": "^3.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/ciphers": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.1.1.tgz",
|
||||
"integrity": "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz",
|
||||
"integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/number": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
|
||||
@@ -2855,6 +3007,12 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tabby_ai/hijri-converter": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@tabby_ai/hijri-converter/-/hijri-converter-1.0.5.tgz",
|
||||
@@ -3269,6 +3427,23 @@
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/webidl-conversions": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
|
||||
"integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/whatwg-url": {
|
||||
"version": "11.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz",
|
||||
"integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/webidl-conversions": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
||||
@@ -3290,6 +3465,32 @@
|
||||
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/aria-hidden": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
|
||||
@@ -3315,6 +3516,131 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/better-auth": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.5.5.tgz",
|
||||
"integrity": "sha512-GpVPaV1eqr3mOovKfghJXXk6QvlcVeFbS3z+n+FPDid5rK/2PchnDtiaVCzWyXA9jH2KkirOfl+JhAUvnja0Eg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@better-auth/core": "1.5.5",
|
||||
"@better-auth/drizzle-adapter": "1.5.5",
|
||||
"@better-auth/kysely-adapter": "1.5.5",
|
||||
"@better-auth/memory-adapter": "1.5.5",
|
||||
"@better-auth/mongo-adapter": "1.5.5",
|
||||
"@better-auth/prisma-adapter": "1.5.5",
|
||||
"@better-auth/telemetry": "1.5.5",
|
||||
"@better-auth/utils": "0.3.1",
|
||||
"@better-fetch/fetch": "1.1.21",
|
||||
"@noble/ciphers": "^2.1.1",
|
||||
"@noble/hashes": "^2.0.1",
|
||||
"better-call": "1.3.2",
|
||||
"defu": "^6.1.4",
|
||||
"jose": "^6.1.3",
|
||||
"kysely": "^0.28.11",
|
||||
"nanostores": "^1.1.1",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@lynx-js/react": "*",
|
||||
"@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@tanstack/react-start": "^1.0.0",
|
||||
"@tanstack/solid-start": "^1.0.0",
|
||||
"better-sqlite3": "^12.0.0",
|
||||
"drizzle-kit": ">=0.31.4",
|
||||
"drizzle-orm": ">=0.41.0",
|
||||
"mongodb": "^6.0.0 || ^7.0.0",
|
||||
"mysql2": "^3.0.0",
|
||||
"next": "^14.0.0 || ^15.0.0 || ^16.0.0",
|
||||
"pg": "^8.0.0",
|
||||
"prisma": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0",
|
||||
"solid-js": "^1.0.0",
|
||||
"svelte": "^4.0.0 || ^5.0.0",
|
||||
"vitest": "^2.0.0 || ^3.0.0 || ^4.0.0",
|
||||
"vue": "^3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@lynx-js/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@prisma/client": {
|
||||
"optional": true
|
||||
},
|
||||
"@sveltejs/kit": {
|
||||
"optional": true
|
||||
},
|
||||
"@tanstack/react-start": {
|
||||
"optional": true
|
||||
},
|
||||
"@tanstack/solid-start": {
|
||||
"optional": true
|
||||
},
|
||||
"better-sqlite3": {
|
||||
"optional": true
|
||||
},
|
||||
"drizzle-kit": {
|
||||
"optional": true
|
||||
},
|
||||
"drizzle-orm": {
|
||||
"optional": true
|
||||
},
|
||||
"mongodb": {
|
||||
"optional": true
|
||||
},
|
||||
"mysql2": {
|
||||
"optional": true
|
||||
},
|
||||
"next": {
|
||||
"optional": true
|
||||
},
|
||||
"pg": {
|
||||
"optional": true
|
||||
},
|
||||
"prisma": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"solid-js": {
|
||||
"optional": true
|
||||
},
|
||||
"svelte": {
|
||||
"optional": true
|
||||
},
|
||||
"vitest": {
|
||||
"optional": true
|
||||
},
|
||||
"vue": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/better-call": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/better-call/-/better-call-1.3.2.tgz",
|
||||
"integrity": "sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@better-auth/utils": "^0.3.1",
|
||||
"@better-fetch/fetch": "^1.1.21",
|
||||
"rou3": "^0.7.12",
|
||||
"set-cookie-parser": "^3.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.28.1",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
|
||||
@@ -3349,6 +3675,16 @@
|
||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/bson": {
|
||||
"version": "6.10.4",
|
||||
"resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz",
|
||||
"integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=16.20.1"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001780",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz",
|
||||
@@ -3370,6 +3706,36 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk/node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/class-variance-authority": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
|
||||
@@ -3382,6 +3748,21 @@
|
||||
"url": "https://polar.sh/cva"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
@@ -3407,6 +3788,51 @@
|
||||
"react-dom": "^18 || ^19 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concurrently": {
|
||||
"version": "9.2.1",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz",
|
||||
"integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "4.1.2",
|
||||
"rxjs": "7.8.2",
|
||||
"shell-quote": "1.8.3",
|
||||
"supports-color": "8.1.1",
|
||||
"tree-kill": "1.2.2",
|
||||
"yargs": "17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"conc": "dist/bin/concurrently.js",
|
||||
"concurrently": "dist/bin/concurrently.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
@@ -3581,6 +4007,12 @@
|
||||
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/defu": {
|
||||
"version": "6.1.4",
|
||||
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
|
||||
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
@@ -3642,6 +4074,13 @@
|
||||
"embla-carousel": "8.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.20.1",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz",
|
||||
@@ -3766,6 +4205,16 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-nonce": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
|
||||
@@ -3782,6 +4231,16 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/input-otp": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz",
|
||||
@@ -3801,6 +4260,16 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
||||
@@ -3811,6 +4280,15 @@
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz",
|
||||
"integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -3843,6 +4321,15 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/kysely": {
|
||||
"version": "0.28.13",
|
||||
"resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.13.tgz",
|
||||
"integrity": "sha512-jCkYDvlfzOyHaVsrvR4vnNZxG30oNv2jbbFBjTQAUG8n0h07HW0sZJHk4KAQIRyu9ay+Rg+L8qGa3lwt8Gve9w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||
@@ -4151,6 +4638,71 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/memory-pager": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
|
||||
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/mongodb": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.21.0.tgz",
|
||||
"integrity": "sha512-URyb/VXMjJ4da46OeSXg+puO39XH9DeQpWCslifrRn9JWugy0D+DvvBvkm2WxmHe61O/H19JM66p1z7RHVkZ6A==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@mongodb-js/saslprep": "^1.3.0",
|
||||
"bson": "^6.10.4",
|
||||
"mongodb-connection-string-url": "^3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.20.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@aws-sdk/credential-providers": "^3.188.0",
|
||||
"@mongodb-js/zstd": "^1.1.0 || ^2.0.0",
|
||||
"gcp-metadata": "^5.2.0",
|
||||
"kerberos": "^2.0.1",
|
||||
"mongodb-client-encryption": ">=6.0.0 <7",
|
||||
"snappy": "^7.3.2",
|
||||
"socks": "^2.7.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@aws-sdk/credential-providers": {
|
||||
"optional": true
|
||||
},
|
||||
"@mongodb-js/zstd": {
|
||||
"optional": true
|
||||
},
|
||||
"gcp-metadata": {
|
||||
"optional": true
|
||||
},
|
||||
"kerberos": {
|
||||
"optional": true
|
||||
},
|
||||
"mongodb-client-encryption": {
|
||||
"optional": true
|
||||
},
|
||||
"snappy": {
|
||||
"optional": true
|
||||
},
|
||||
"socks": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-connection-string-url": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz",
|
||||
"integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/whatwg-url": "^11.0.2",
|
||||
"whatwg-url": "^14.1.0 || ^13.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -4177,6 +4729,21 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/nanostores": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/nanostores/-/nanostores-1.2.0.tgz",
|
||||
"integrity": "sha512-F0wCzbsH80G7XXo0Jd9/AVQC7ouWY6idUCTnMwW5t/Rv9W8qmO6endavDwg7TNp5GbugwSukFMVZqzPSrSMndg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^20.0.0 || >=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/next-themes": {
|
||||
"version": "0.4.6",
|
||||
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz",
|
||||
@@ -4269,6 +4836,16 @@
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.2.4",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||
@@ -4486,6 +5063,16 @@
|
||||
"decimal.js-light": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
|
||||
@@ -4531,6 +5118,22 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rou3": {
|
||||
"version": "0.7.12",
|
||||
"resolved": "https://registry.npmjs.org/rou3/-/rou3-0.7.12.tgz",
|
||||
"integrity": "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rxjs": {
|
||||
"version": "7.8.2",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||
@@ -4547,6 +5150,25 @@
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.0.1.tgz",
|
||||
"integrity": "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/shell-quote": {
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
|
||||
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/sonner": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz",
|
||||
@@ -4567,6 +5189,60 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sparse-bitfield": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
|
||||
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"memory-pager": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwind-merge": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.1.tgz",
|
||||
@@ -4621,6 +5297,29 @@
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
|
||||
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"punycode": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tree-kill": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
||||
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"tree-kill": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
@@ -4834,12 +5533,102 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "14.2.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
|
||||
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tr46": "^5.1.0",
|
||||
"webidl-conversions": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^8.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.3",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
10
package.json
10
package.json
@@ -3,7 +3,9 @@
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev": "concurrently -n \"client,server\" -c \"blue,green\" \"vite\" \"npm run dev --prefix server\"",
|
||||
"dev:client": "vite",
|
||||
"dev:server": "npm run dev --prefix server",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
@@ -50,7 +52,8 @@
|
||||
"recharts": "^2.15.2",
|
||||
"sonner": "^2.0.3",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"vaul": "^1.1.2"
|
||||
"vaul": "^1.1.2",
|
||||
"better-auth": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
@@ -59,6 +62,7 @@
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^6.2.0"
|
||||
"vite": "^6.2.0",
|
||||
"concurrently": "^9.1.2"
|
||||
}
|
||||
}
|
||||
|
||||
2363
server/package-lock.json
generated
Normal file
2363
server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
server/package.json
Normal file
28
server/package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "labwise-server",
|
||||
"version": "1.0.0",
|
||||
"type": "commonjs",
|
||||
"scripts": {
|
||||
"dev": "tsx watch --env-file=.env src/index.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"db:migrate": "tsx src/db/migrate.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"better-auth": "^1.5.5",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.21.2",
|
||||
"express-rate-limit": "^7.5.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"pg": "^8.13.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^5.0.1",
|
||||
"@types/multer": "^1.4.12",
|
||||
"@types/node": "^22.13.10",
|
||||
"@types/pg": "^8.11.11",
|
||||
"tsx": "^4.19.3",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
37
server/src/auth/auth.ts
Normal file
37
server/src/auth/auth.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { betterAuth } from 'better-auth';
|
||||
import { kyselyAdapter } from '@better-auth/kysely-adapter';
|
||||
import { Kysely, PostgresDialect } from 'kysely';
|
||||
import { Pool } from 'pg';
|
||||
|
||||
const db = new Kysely({
|
||||
dialect: new PostgresDialect({
|
||||
pool: new Pool({
|
||||
connectionString: process.env.DATABASE_URL ||
|
||||
'postgresql://labwise:labwise_dev_pw@localhost:5432/labwise_db',
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: kyselyAdapter(db, { type: 'postgres' }),
|
||||
|
||||
baseURL: process.env.BETTER_AUTH_URL || 'http://localhost:3001',
|
||||
secret: process.env.BETTER_AUTH_SECRET || 'dev-secret-change-in-production-min32chars!!',
|
||||
|
||||
socialProviders: {
|
||||
google: {
|
||||
clientId: process.env.GOOGLE_CLIENT_ID || '',
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET || '',
|
||||
},
|
||||
microsoft: {
|
||||
clientId: process.env.MICROSOFT_CLIENT_ID || '',
|
||||
clientSecret: process.env.MICROSOFT_CLIENT_SECRET || '',
|
||||
tenantId: process.env.MICROSOFT_TENANT_ID || 'common',
|
||||
},
|
||||
},
|
||||
|
||||
trustedOrigins: [
|
||||
'http://localhost:5173',
|
||||
'https://labwise.wahwa.com',
|
||||
],
|
||||
});
|
||||
30
server/src/auth/middleware.ts
Normal file
30
server/src/auth/middleware.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { auth } from './auth';
|
||||
import { fromNodeHeaders } from 'better-auth/node';
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
user?: { id: string; email: string; name: string };
|
||||
sessionId?: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function requireAuth(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const session = await auth.api.getSession({
|
||||
headers: fromNodeHeaders(req.headers),
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
req.user = session.user;
|
||||
req.sessionId = session.session.id;
|
||||
next();
|
||||
} catch {
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
}
|
||||
16
server/src/auth/rateLimiter.ts
Normal file
16
server/src/auth/rateLimiter.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import rateLimit from 'express-rate-limit';
|
||||
|
||||
export const authRateLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 20,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
message: { error: 'Too many auth attempts, please try again later' },
|
||||
});
|
||||
|
||||
export const apiRateLimiter = rateLimit({
|
||||
windowMs: 60 * 1000, // 1 minute
|
||||
max: 200,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
});
|
||||
15
server/src/db/migrate.ts
Normal file
15
server/src/db/migrate.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { pool } from './pool';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
async function migrate() {
|
||||
const sql = fs.readFileSync(path.join(__dirname, 'schema.sql'), 'utf-8');
|
||||
await pool.query(sql);
|
||||
console.log('Migration complete');
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
migrate().catch(err => {
|
||||
console.error('Migration failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
9
server/src/db/pool.ts
Normal file
9
server/src/db/pool.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Pool } from 'pg';
|
||||
|
||||
export const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL ||
|
||||
'postgresql://labwise:labwise_dev_pw@localhost:5432/labwise_db',
|
||||
max: 10,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 2000,
|
||||
});
|
||||
63
server/src/db/schema.sql
Normal file
63
server/src/db/schema.sql
Normal file
@@ -0,0 +1,63 @@
|
||||
-- Chemical inventory, scoped per user
|
||||
CREATE TABLE IF NOT EXISTS chemicals (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id TEXT NOT NULL,
|
||||
|
||||
-- Required fields
|
||||
pi_first_name TEXT NOT NULL,
|
||||
physical_state TEXT NOT NULL,
|
||||
chemical_name TEXT NOT NULL,
|
||||
bldg_code TEXT NOT NULL,
|
||||
lab TEXT NOT NULL,
|
||||
storage_location TEXT NOT NULL,
|
||||
storage_device TEXT NOT NULL,
|
||||
number_of_containers TEXT NOT NULL,
|
||||
amount_per_container TEXT NOT NULL,
|
||||
unit_of_measure TEXT NOT NULL,
|
||||
cas_number TEXT NOT NULL,
|
||||
|
||||
-- Optional fields
|
||||
chemical_formula TEXT,
|
||||
molecular_weight TEXT,
|
||||
vendor TEXT,
|
||||
catalog_number TEXT,
|
||||
found_in_catalog TEXT,
|
||||
po_number TEXT,
|
||||
receipt_date TEXT,
|
||||
open_date TEXT,
|
||||
max_on_hand TEXT,
|
||||
expiration_date DATE,
|
||||
contact TEXT,
|
||||
comments TEXT,
|
||||
permit_number TEXT,
|
||||
barcode TEXT,
|
||||
concentration TEXT,
|
||||
chemical_number TEXT,
|
||||
lot_number TEXT,
|
||||
multiple_cas TEXT,
|
||||
msds TEXT,
|
||||
percentage_full NUMERIC(5,2),
|
||||
needs_manual_entry TEXT[],
|
||||
scanned_image TEXT,
|
||||
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS chemicals_user_id_idx ON chemicals(user_id);
|
||||
CREATE INDEX IF NOT EXISTS chemicals_cas_number_idx ON chemicals(cas_number);
|
||||
|
||||
-- Protocols with JSONB analysis results
|
||||
CREATE TABLE IF NOT EXISTS protocols (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
content TEXT NOT NULL DEFAULT '',
|
||||
file_url TEXT,
|
||||
analysis_results JSONB,
|
||||
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS protocols_user_id_idx ON protocols(user_id);
|
||||
44
server/src/index.ts
Normal file
44
server/src/index.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import { toNodeHandler } from 'better-auth/node';
|
||||
import { auth } from './auth/auth';
|
||||
import { authRateLimiter, apiRateLimiter } from './auth/rateLimiter';
|
||||
import chemicalsRouter from './routes/chemicals';
|
||||
import protocolsRouter from './routes/protocols';
|
||||
import path from 'path';
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3001;
|
||||
const UPLOADS_DIR = process.env.UPLOADS_DIR || path.join(__dirname, '../uploads');
|
||||
|
||||
// Trust Cloudflare/proxy X-Forwarded-For headers
|
||||
app.set('trust proxy', 1);
|
||||
|
||||
app.use(cors({
|
||||
origin: [
|
||||
'http://localhost:5173',
|
||||
'https://labwise.wahwa.com',
|
||||
],
|
||||
credentials: true,
|
||||
}));
|
||||
|
||||
// Serve uploaded files
|
||||
app.use('/uploads', express.static(UPLOADS_DIR));
|
||||
|
||||
// Better Auth — must come before express.json() so it can read its own body
|
||||
app.use('/api/auth/*', authRateLimiter);
|
||||
app.all('/api/auth/*', toNodeHandler(auth));
|
||||
|
||||
// Body parsing for all other routes
|
||||
app.use(express.json({ limit: '1mb' }));
|
||||
|
||||
// Application routes
|
||||
app.use('/api', apiRateLimiter);
|
||||
app.use('/api/chemicals', chemicalsRouter);
|
||||
app.use('/api/protocols', protocolsRouter);
|
||||
|
||||
app.get('/api/health', (_req, res) => res.json({ ok: true }));
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`LabWise API running on http://localhost:${PORT}`);
|
||||
});
|
||||
106
server/src/routes/chemicals.ts
Normal file
106
server/src/routes/chemicals.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { Router } from 'express';
|
||||
import { pool } from '../db/pool';
|
||||
import { requireAuth } from '../auth/middleware';
|
||||
|
||||
const router = Router();
|
||||
router.use(requireAuth);
|
||||
|
||||
function camelToSnake(str: string): string {
|
||||
return str.replace(/[A-Z]/g, l => `_${l.toLowerCase()}`);
|
||||
}
|
||||
|
||||
// GET /api/chemicals
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM chemicals WHERE user_id = $1 ORDER BY created_at DESC',
|
||||
[req.user!.id]
|
||||
);
|
||||
// Map snake_case columns back to camelCase for the frontend
|
||||
const rows = result.rows.map(snakeToCamel);
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/chemicals
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const b = req.body;
|
||||
const result = await pool.query(`
|
||||
INSERT INTO chemicals (
|
||||
user_id, pi_first_name, physical_state, chemical_name, bldg_code, lab,
|
||||
storage_location, storage_device, number_of_containers, amount_per_container,
|
||||
unit_of_measure, cas_number,
|
||||
chemical_formula, molecular_weight, vendor, catalog_number, lot_number,
|
||||
expiration_date, concentration, percentage_full, needs_manual_entry,
|
||||
scanned_image, comments, barcode, contact
|
||||
) VALUES (
|
||||
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,
|
||||
$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25
|
||||
) RETURNING *`,
|
||||
[
|
||||
req.user!.id, b.piFirstName, b.physicalState, b.chemicalName, b.bldgCode, b.lab,
|
||||
b.storageLocation, b.storageDevice, b.numberOfContainers, b.amountPerContainer,
|
||||
b.unitOfMeasure, b.casNumber,
|
||||
b.chemicalFormula ?? null, b.molecularWeight ?? null, b.vendor ?? null,
|
||||
b.catalogNumber ?? null, b.lotNumber ?? null,
|
||||
b.expirationDate ?? null, b.concentration ?? null,
|
||||
b.percentageFull ?? null, b.needsManualEntry ?? null,
|
||||
b.scannedImage ?? null, b.comments ?? null, b.barcode ?? null, b.contact ?? null,
|
||||
]
|
||||
);
|
||||
res.status(201).json(snakeToCamel(result.rows[0]));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// PATCH /api/chemicals/:id
|
||||
router.patch('/:id', async (req, res) => {
|
||||
try {
|
||||
const fields = Object.keys(req.body);
|
||||
if (fields.length === 0) return res.status(400).json({ error: 'No fields to update' });
|
||||
|
||||
const snakeFields = fields.map(camelToSnake);
|
||||
const setClauses = snakeFields.map((f, i) => `${f} = $${i + 3}`).join(', ');
|
||||
const values = fields.map(f => req.body[f]);
|
||||
|
||||
const result = await pool.query(
|
||||
`UPDATE chemicals SET ${setClauses}, updated_at = NOW()
|
||||
WHERE id = $1 AND user_id = $2 RETURNING *`,
|
||||
[req.params.id, req.user!.id, ...values]
|
||||
);
|
||||
if (result.rows.length === 0) return res.status(404).json({ error: 'Not found' });
|
||||
res.json(snakeToCamel(result.rows[0]));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/chemicals/:id
|
||||
router.delete('/:id', async (req, res) => {
|
||||
try {
|
||||
await pool.query('DELETE FROM chemicals WHERE id = $1 AND user_id = $2',
|
||||
[req.params.id, req.user!.id]);
|
||||
res.status(204).end();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
function snakeToCamel(row: Record<string, unknown>): Record<string, unknown> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(row).map(([k, v]) => [
|
||||
k.replace(/_([a-z])/g, (_, l) => l.toUpperCase()),
|
||||
v,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
export default router;
|
||||
110
server/src/routes/protocols.ts
Normal file
110
server/src/routes/protocols.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { Router } from 'express';
|
||||
import { pool } from '../db/pool';
|
||||
import { requireAuth } from '../auth/middleware';
|
||||
import multer from 'multer';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
const router = Router();
|
||||
router.use(requireAuth);
|
||||
|
||||
const uploadsDir = process.env.UPLOADS_DIR || path.join(__dirname, '../../uploads');
|
||||
fs.mkdirSync(uploadsDir, { recursive: true });
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, _file, cb) => {
|
||||
const userDir = path.join(uploadsDir, (req as any).user.id);
|
||||
fs.mkdirSync(userDir, { recursive: true });
|
||||
cb(null, userDir);
|
||||
},
|
||||
filename: (_req, file, cb) => {
|
||||
const unique = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
|
||||
cb(null, `${unique}${path.extname(file.originalname)}`);
|
||||
},
|
||||
});
|
||||
|
||||
const upload = multer({
|
||||
storage,
|
||||
limits: { fileSize: 10 * 1024 * 1024 },
|
||||
fileFilter: (_req, file, cb) => {
|
||||
const allowed = ['.pdf', '.doc', '.docx', '.txt'];
|
||||
if (allowed.includes(path.extname(file.originalname).toLowerCase())) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Only PDF, DOC, DOCX, TXT files allowed'));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// GET /api/protocols
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM protocols WHERE user_id = $1 ORDER BY created_at DESC',
|
||||
[req.user!.id]
|
||||
);
|
||||
res.json(result.rows);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/protocols
|
||||
router.post('/', upload.single('file'), async (req, res) => {
|
||||
try {
|
||||
const { title, content } = req.body;
|
||||
const userId = (req as any).user.id;
|
||||
const fileUrl = req.file ? `/uploads/${userId}/${req.file.filename}` : null;
|
||||
|
||||
const result = await pool.query(
|
||||
`INSERT INTO protocols (user_id, title, content, file_url)
|
||||
VALUES ($1, $2, $3, $4) RETURNING *`,
|
||||
[req.user!.id, title || 'Untitled Protocol', content || '', fileUrl]
|
||||
);
|
||||
res.status(201).json(result.rows[0]);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// PATCH /api/protocols/:id/analysis
|
||||
router.patch('/:id/analysis', async (req, res) => {
|
||||
try {
|
||||
const { analysis_results } = req.body;
|
||||
const result = await pool.query(
|
||||
`UPDATE protocols SET analysis_results = $1::jsonb, updated_at = NOW()
|
||||
WHERE id = $2 AND user_id = $3 RETURNING *`,
|
||||
[JSON.stringify(analysis_results), req.params.id, req.user!.id]
|
||||
);
|
||||
if (result.rows.length === 0) return res.status(404).json({ error: 'Not found' });
|
||||
res.json(result.rows[0]);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/protocols/:id
|
||||
router.delete('/:id', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'DELETE FROM protocols WHERE id = $1 AND user_id = $2 RETURNING file_url',
|
||||
[req.params.id, req.user!.id]
|
||||
);
|
||||
const fileUrl = result.rows[0]?.file_url;
|
||||
if (fileUrl) {
|
||||
// fileUrl is like /uploads/<userId>/<filename>
|
||||
const relative = fileUrl.replace(/^\/uploads\//, '');
|
||||
const filePath = path.join(uploadsDir, relative);
|
||||
fs.rmSync(filePath, { force: true });
|
||||
}
|
||||
res.status(204).end();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
15
server/tsconfig.json
Normal file
15
server/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
60
shared/types.ts
Normal file
60
shared/types.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
export interface ChemicalInventory {
|
||||
id: string;
|
||||
user_id: string;
|
||||
piFirstName: string;
|
||||
physicalState: string;
|
||||
chemicalName: string;
|
||||
bldgCode: string;
|
||||
lab: string;
|
||||
storageLocation: string;
|
||||
storageDevice: string;
|
||||
numberOfContainers: string;
|
||||
amountPerContainer: string;
|
||||
unitOfMeasure: string;
|
||||
casNumber: string;
|
||||
chemicalFormula?: string;
|
||||
molecularWeight?: string;
|
||||
vendor?: string;
|
||||
catalogNumber?: string;
|
||||
foundInCatalog?: string;
|
||||
poNumber?: string;
|
||||
receiptDate?: string;
|
||||
openDate?: string;
|
||||
maxOnHand?: string;
|
||||
expirationDate?: string;
|
||||
contact?: string;
|
||||
comments?: string;
|
||||
dateEntered?: string;
|
||||
permitNumber?: string;
|
||||
barcode?: string;
|
||||
lastChanged?: string;
|
||||
concentration?: string;
|
||||
chemicalNumber?: string;
|
||||
lotNumber?: string;
|
||||
multipleCAS?: string;
|
||||
msds?: string;
|
||||
percentageFull?: number;
|
||||
needsManualEntry?: string[];
|
||||
scannedImage?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface SafetyIssue {
|
||||
type: "critical" | "warning" | "suggestion";
|
||||
category: string;
|
||||
message: string;
|
||||
source?: string;
|
||||
sourceUrl?: string;
|
||||
}
|
||||
|
||||
export interface Protocol {
|
||||
id: string;
|
||||
user_id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
file_url?: string;
|
||||
analysis_results?: SafetyIssue[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
@@ -7,5 +7,9 @@ export default defineConfig({
|
||||
server: {
|
||||
host: true,
|
||||
allowedHosts: ["labwise.wahwa.com"],
|
||||
proxy: {
|
||||
'/api': { target: 'http://localhost:3001', changeOrigin: true },
|
||||
'/uploads': { target: 'http://localhost:3001', changeOrigin: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user