aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCodeWithShreyans <[email protected]>2025-09-04 16:46:47 +0000
committerCodeWithShreyans <[email protected]>2025-09-04 16:46:47 +0000
commit84fea4a6981385145dd84a58a610b4782caec047 (patch)
tree3d3f84d0ce15a096086c28e86e863ea99d25006b
parentfeat: openai python sdk (#409) (diff)
downloadsupermemory-84fea4a6981385145dd84a58a610b4782caec047.tar.xz
supermemory-84fea4a6981385145dd84a58a610b4782caec047.zip
feat: add mcp migrate route (#410)shreyans/09-03-feat_add_mcp_migrate_route
-rw-r--r--apps/web/app/page.tsx299
-rw-r--r--apps/web/app/upgrade-mcp/page.tsx324
-rw-r--r--apps/web/components/menu.tsx222
-rw-r--r--apps/web/components/views/profile.tsx80
-rw-r--r--apps/web/middleware.ts37
-rw-r--r--packages/ui/pages/login.tsx213
6 files changed, 795 insertions, 380 deletions
diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx
index 5018c337..6bf8cf6f 100644
--- a/apps/web/app/page.tsx
+++ b/apps/web/app/page.tsx
@@ -1,14 +1,14 @@
-"use client";
-
-import { useIsMobile } from "@hooks/use-mobile";
-import { useAuth } from "@lib/auth-context";
-import { $fetch } from "@repo/lib/api";
-import { MemoryGraph } from "@repo/ui/memory-graph";
-import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api";
-import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
-import { Logo, LogoFull } from "@ui/assets/Logo";
-import { Button } from "@ui/components/button";
-import { GlassMenuEffect } from "@ui/other/glass-effect";
+"use client"
+
+import { useIsMobile } from "@hooks/use-mobile"
+import { useAuth } from "@lib/auth-context"
+import { $fetch } from "@repo/lib/api"
+import { MemoryGraph } from "@repo/ui/memory-graph"
+import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
+import { useInfiniteQuery, useQuery } from "@tanstack/react-query"
+import { Logo, LogoFull } from "@ui/assets/Logo"
+import { Button } from "@ui/components/button"
+import { GlassMenuEffect } from "@ui/other/glass-effect"
import {
HelpCircle,
LayoutGrid,
@@ -16,59 +16,59 @@ import {
LoaderIcon,
MessageSquare,
Unplug,
-} from "lucide-react";
-import { AnimatePresence, motion } from "motion/react";
-import Link from "next/link";
-import { useRouter } from "next/navigation";
-import { useCallback, useEffect, useMemo, useState } from "react";
-import type { z } from "zod";
-import { ConnectAIModal } from "@/components/connect-ai-modal";
-import { InstallPrompt } from "@/components/install-prompt";
-import { MemoryListView } from "@/components/memory-list-view";
-import Menu from "@/components/menu";
-import { ProjectSelector } from "@/components/project-selector";
-import { ReferralUpgradeModal } from "@/components/referral-upgrade-modal";
-import type { TourStep } from "@/components/tour";
-import { TourAlertDialog, useTour } from "@/components/tour";
-import { AddMemoryView } from "@/components/views/add-memory";
-import { ChatRewrite } from "@/components/views/chat";
-import { TOUR_STEP_IDS, TOUR_STORAGE_KEY } from "@/lib/tour-constants";
-import { useViewMode } from "@/lib/view-mode-context";
-import { useChatOpen, useProject } from "@/stores";
-import { useGraphHighlights } from "@/stores/highlights";
-
-type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>;
-type DocumentWithMemories = DocumentsResponse["documents"][0];
+} from "lucide-react"
+import { AnimatePresence, motion } from "motion/react"
+import Link from "next/link"
+import { useRouter } from "next/navigation"
+import { useCallback, useEffect, useMemo, useState } from "react"
+import type { z } from "zod"
+import { ConnectAIModal } from "@/components/connect-ai-modal"
+import { InstallPrompt } from "@/components/install-prompt"
+import { MemoryListView } from "@/components/memory-list-view"
+import Menu from "@/components/menu"
+import { ProjectSelector } from "@/components/project-selector"
+import { ReferralUpgradeModal } from "@/components/referral-upgrade-modal"
+import type { TourStep } from "@/components/tour"
+import { TourAlertDialog, useTour } from "@/components/tour"
+import { AddMemoryView } from "@/components/views/add-memory"
+import { ChatRewrite } from "@/components/views/chat"
+import { TOUR_STEP_IDS, TOUR_STORAGE_KEY } from "@/lib/tour-constants"
+import { useViewMode } from "@/lib/view-mode-context"
+import { useChatOpen, useProject } from "@/stores"
+import { useGraphHighlights } from "@/stores/highlights"
+
+type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
+type DocumentWithMemories = DocumentsResponse["documents"][0]
const MemoryGraphPage = () => {
- const { documentIds: allHighlightDocumentIds } = useGraphHighlights();
- const isMobile = useIsMobile();
- const { viewMode, setViewMode, isInitialized } = useViewMode();
- const { selectedProject } = useProject();
- const { setSteps, isTourCompleted } = useTour();
- const { isOpen, setIsOpen } = useChatOpen();
- const [injectedDocs, setInjectedDocs] = useState<DocumentWithMemories[]>([]);
- const [showAddMemoryView, setShowAddMemoryView] = useState(false);
- const [showReferralModal, setShowReferralModal] = useState(false);
- const [showConnectAIModal, setShowConnectAIModal] = useState(false);
- const [isHelpHovered, setIsHelpHovered] = useState(false);
+ const { documentIds: allHighlightDocumentIds } = useGraphHighlights()
+ const isMobile = useIsMobile()
+ const { viewMode, setViewMode, isInitialized } = useViewMode()
+ const { selectedProject } = useProject()
+ const { setSteps, isTourCompleted } = useTour()
+ const { isOpen, setIsOpen } = useChatOpen()
+ const [injectedDocs, setInjectedDocs] = useState<DocumentWithMemories[]>([])
+ const [showAddMemoryView, setShowAddMemoryView] = useState(false)
+ const [showReferralModal, setShowReferralModal] = useState(false)
+ const [showConnectAIModal, setShowConnectAIModal] = useState(false)
+ const [isHelpHovered, setIsHelpHovered] = useState(false)
// Fetch projects meta to detect experimental flag
const { data: projectsMeta = [] } = useQuery({
queryKey: ["projects"],
queryFn: async () => {
- const response = await $fetch("@get/projects");
- return response.data?.projects ?? [];
+ const response = await $fetch("@get/projects")
+ return response.data?.projects ?? []
},
staleTime: 5 * 60 * 1000,
- });
+ })
const isCurrentProjectExperimental = !!projectsMeta.find(
(p: any) => p.containerTag === selectedProject,
- )?.isExperimental;
+ )?.isExperimental
// Tour state
- const [showTourDialog, setShowTourDialog] = useState(false);
+ const [showTourDialog, setShowTourDialog] = useState(false)
// Define tour steps with useMemo to prevent recreation
const tourSteps: TourStep[] = useMemo(() => {
@@ -201,37 +201,37 @@ const MemoryGraphPage = () => {
selectorId: TOUR_STEP_IDS.FLOATING_CHAT,
position: "left",
},
- ];
- }, []);
+ ]
+ }, [])
// Check if tour has been completed before
useEffect(() => {
- const hasCompletedTour = localStorage.getItem(TOUR_STORAGE_KEY) === "true";
+ const hasCompletedTour = localStorage.getItem(TOUR_STORAGE_KEY) === "true"
if (!hasCompletedTour && !isTourCompleted) {
const timer = setTimeout(() => {
- setShowTourDialog(true);
- setShowConnectAIModal(false);
- }, 1000); // Show after 1 second
- return () => clearTimeout(timer);
+ setShowTourDialog(true)
+ setShowConnectAIModal(false)
+ }, 1000) // Show after 1 second
+ return () => clearTimeout(timer)
}
- }, [isTourCompleted]);
+ }, [isTourCompleted])
// Set up tour steps
useEffect(() => {
- setSteps(tourSteps);
- }, [setSteps, tourSteps]);
+ setSteps(tourSteps)
+ }, [setSteps, tourSteps])
// Save tour completion to localStorage
useEffect(() => {
if (isTourCompleted) {
- localStorage.setItem(TOUR_STORAGE_KEY, "true");
+ localStorage.setItem(TOUR_STORAGE_KEY, "true")
}
- }, [isTourCompleted]);
+ }, [isTourCompleted])
// Progressive loading via useInfiniteQuery
- const IS_DEV = process.env.NODE_ENV === "development";
- const PAGE_SIZE = IS_DEV ? 100 : 100;
- const MAX_TOTAL = 1000;
+ const IS_DEV = process.env.NODE_ENV === "development"
+ const PAGE_SIZE = IS_DEV ? 100 : 100
+ const MAX_TOTAL = 1000
const {
data,
@@ -253,76 +253,75 @@ const MemoryGraphPage = () => {
containerTags: selectedProject ? [selectedProject] : undefined,
},
disableValidation: true,
- });
+ })
if (response.error) {
- throw new Error(response.error?.message || "Failed to fetch documents");
+ throw new Error(response.error?.message || "Failed to fetch documents")
}
- return response.data;
+ return response.data
},
getNextPageParam: (lastPage, allPages) => {
const loaded = allPages.reduce(
(acc, p) => acc + (p.documents?.length ?? 0),
0,
- );
- if (loaded >= MAX_TOTAL) return undefined;
+ )
+ if (loaded >= MAX_TOTAL) return undefined
- const { currentPage, totalPages } = lastPage.pagination;
+ const { currentPage, totalPages } = lastPage.pagination
if (currentPage < totalPages) {
- return currentPage + 1;
+ return currentPage + 1
}
- return undefined;
+ return undefined
},
staleTime: 5 * 60 * 1000,
- });
+ })
const baseDocuments = useMemo(() => {
return (
data?.pages.flatMap((p: DocumentsResponse) => p.documents ?? []) ?? []
- );
- }, [data]);
+ )
+ }, [data])
const allDocuments = useMemo(() => {
- if (injectedDocs.length === 0) return baseDocuments;
- const byId = new Map<string, DocumentWithMemories>();
- for (const d of injectedDocs) byId.set(d.id, d);
- for (const d of baseDocuments) if (!byId.has(d.id)) byId.set(d.id, d);
- return Array.from(byId.values());
- }, [baseDocuments, injectedDocs]);
+ if (injectedDocs.length === 0) return baseDocuments
+ const byId = new Map<string, DocumentWithMemories>()
+ for (const d of injectedDocs) byId.set(d.id, d)
+ for (const d of baseDocuments) if (!byId.has(d.id)) byId.set(d.id, d)
+ return Array.from(byId.values())
+ }, [baseDocuments, injectedDocs])
- const totalLoaded = allDocuments.length;
- const hasMore = hasNextPage;
- const isLoadingMore = isFetchingNextPage;
+ const totalLoaded = allDocuments.length
+ const hasMore = hasNextPage
+ const isLoadingMore = isFetchingNextPage
const loadMoreDocuments = useCallback(async (): Promise<void> => {
if (hasNextPage && !isFetchingNextPage) {
- await fetchNextPage();
- return;
+ await fetchNextPage()
+ return
}
- return;
- }, [hasNextPage, isFetchingNextPage, fetchNextPage]);
+ return
+ }, [hasNextPage, isFetchingNextPage, fetchNextPage])
// Reset injected docs when project changes
useEffect(() => {
- setInjectedDocs([]);
- }, [selectedProject]);
+ setInjectedDocs([])
+ }, [selectedProject])
// Surgical fetch of missing highlighted documents (customId-based IDs from search)
useEffect(() => {
- if (!isOpen) return;
- if (!allHighlightDocumentIds || allHighlightDocumentIds.length === 0)
- return;
- const present = new Set<string>();
+ if (!isOpen) return
+ if (!allHighlightDocumentIds || allHighlightDocumentIds.length === 0) return
+ const present = new Set<string>()
for (const d of [...baseDocuments, ...injectedDocs]) {
- if (d.id) present.add(d.id);
- if ((d as any).customId) present.add((d as any).customId as string);
+ if (d.id) present.add(d.id)
+ if ((d as any).customId) present.add((d as any).customId as string)
}
const missing = allHighlightDocumentIds.filter(
(id: string) => !present.has(id),
- );
- if (missing.length === 0) return;
- let cancelled = false;
+ )
+ if (missing.length === 0) return
+ let cancelled = false
const run = async () => {
try {
const resp = await $fetch("@post/memories/documents/by-ids", {
@@ -332,32 +331,32 @@ const MemoryGraphPage = () => {
containerTags: selectedProject ? [selectedProject] : undefined,
},
disableValidation: true,
- });
- if (cancelled || (resp as any)?.error) return;
+ })
+ if (cancelled || (resp as any)?.error) return
const extraDocs = (resp as any)?.data?.documents as
| DocumentWithMemories[]
- | undefined;
- if (!extraDocs || extraDocs.length === 0) return;
+ | undefined
+ if (!extraDocs || extraDocs.length === 0) return
setInjectedDocs((prev) => {
const seen = new Set<string>([
...prev.map((d) => d.id),
...baseDocuments.map((d) => d.id),
- ]);
- const merged = [...prev];
+ ])
+ const merged = [...prev]
for (const doc of extraDocs) {
if (!seen.has(doc.id)) {
- merged.push(doc);
- seen.add(doc.id);
+ merged.push(doc)
+ seen.add(doc.id)
}
}
- return merged;
- });
+ return merged
+ })
} catch {}
- };
- void run();
+ }
+ void run()
return () => {
- cancelled = true;
- };
+ cancelled = true
+ }
}, [
isOpen,
allHighlightDocumentIds.join("|"),
@@ -365,39 +364,39 @@ const MemoryGraphPage = () => {
injectedDocs,
selectedProject,
$fetch,
- ]);
+ ])
// Handle view mode change
const handleViewModeChange = useCallback(
(mode: "graph" | "list") => {
- setViewMode(mode);
+ setViewMode(mode)
},
[setViewMode],
- );
+ )
useEffect(() => {
- const hasCompletedTour = localStorage.getItem(TOUR_STORAGE_KEY) === "true";
+ const hasCompletedTour = localStorage.getItem(TOUR_STORAGE_KEY) === "true"
if (hasCompletedTour && allDocuments.length === 0 && !showTourDialog) {
- setShowConnectAIModal(true);
+ setShowConnectAIModal(true)
} else if (showTourDialog) {
- setShowConnectAIModal(false);
+ setShowConnectAIModal(false)
}
- }, [allDocuments.length, showTourDialog]);
+ }, [allDocuments.length, showTourDialog])
// Prevent body scrolling
useEffect(() => {
- document.body.style.overflow = "hidden";
- document.body.style.height = "100vh";
- document.documentElement.style.overflow = "hidden";
- document.documentElement.style.height = "100vh";
+ document.body.style.overflow = "hidden"
+ document.body.style.height = "100vh"
+ document.documentElement.style.overflow = "hidden"
+ document.documentElement.style.height = "100vh"
return () => {
- document.body.style.overflow = "";
- document.body.style.height = "";
- document.documentElement.style.overflow = "";
- document.documentElement.style.height = "";
- };
- }, []);
+ document.body.style.overflow = ""
+ document.body.style.height = ""
+ document.documentElement.style.overflow = ""
+ document.documentElement.style.height = ""
+ }
+ }, [])
return (
<div className="relative h-screen bg-[#0f1419] overflow-hidden touch-none">
@@ -528,9 +527,9 @@ const MemoryGraphPage = () => {
<button
className="text-sm text-blue-400 hover:text-blue-300 transition-colors underline"
onClick={(e) => {
- e.stopPropagation();
- setShowAddMemoryView(true);
- setShowConnectAIModal(false);
+ e.stopPropagation()
+ setShowAddMemoryView(true)
+ setShowConnectAIModal(false)
}}
type="button"
>
@@ -584,9 +583,9 @@ const MemoryGraphPage = () => {
<button
className="text-sm text-blue-400 hover:text-blue-300 transition-colors underline"
onClick={(e) => {
- e.stopPropagation();
- setShowAddMemoryView(true);
- setShowConnectAIModal(false);
+ e.stopPropagation()
+ setShowAddMemoryView(true)
+ setShowConnectAIModal(false)
}}
type="button"
>
@@ -737,26 +736,26 @@ const MemoryGraphPage = () => {
onClose={() => setShowReferralModal(false)}
/>
</div>
- );
-};
+ )
+}
// Wrapper component to handle auth and waitlist checks
export default function Page() {
- const router = useRouter();
- const { user } = useAuth();
+ const router = useRouter()
+ const { user } = useAuth()
useEffect(() => {
// save the token for chrome extension
- const url = new URL(window.location.href);
- const rawToken = url.searchParams.get("token");
+ const url = new URL(window.location.href)
+ const rawToken = url.searchParams.get("token")
if (rawToken) {
- const encodedToken = encodeURIComponent(rawToken);
- window.postMessage({ token: encodedToken }, "*");
- url.searchParams.delete("token");
- window.history.replaceState({}, "", url.toString());
+ const encodedToken = encodeURIComponent(rawToken)
+ window.postMessage({ token: encodedToken }, "*")
+ url.searchParams.delete("token")
+ window.history.replaceState({}, "", url.toString())
}
- }, []);
+ }, [])
// Show loading state while checking authentication and waitlist status
if (!user) {
@@ -767,7 +766,7 @@ export default function Page() {
<p className="text-white/60">Loading...</p>
</div>
</div>
- );
+ )
}
// If we have a user and they have access, show the main component
@@ -776,5 +775,5 @@ export default function Page() {
<MemoryGraphPage />
<InstallPrompt />
</>
- );
+ )
}
diff --git a/apps/web/app/upgrade-mcp/page.tsx b/apps/web/app/upgrade-mcp/page.tsx
new file mode 100644
index 00000000..baffcbd2
--- /dev/null
+++ b/apps/web/app/upgrade-mcp/page.tsx
@@ -0,0 +1,324 @@
+"use client"
+
+import { $fetch } from "@lib/api"
+import { Button } from "@ui/components/button"
+import { Input } from "@ui/components/input"
+import { GlassMenuEffect } from "@ui/other/glass-effect"
+import { useMutation } from "@tanstack/react-query"
+import { useRouter, useSearchParams } from "next/navigation"
+import { useEffect, useState } from "react"
+import { toast } from "sonner"
+import { Spinner } from "@/components/spinner"
+import { motion, AnimatePresence } from "motion/react"
+import { Logo, LogoFull } from "@ui/assets/Logo"
+import { ArrowRight, CheckCircle, Upload, Zap } from "lucide-react"
+import Link from "next/link"
+import { useSession } from "@lib/auth"
+
+interface MigrateMCPRequest {
+ userId: string
+ projectId: string
+}
+
+interface MigrateMCPResponse {
+ success: boolean
+ migratedCount: number
+ message: string
+ documentIds?: string[]
+}
+
+export default function MigrateMCPPage() {
+ const router = useRouter()
+ const searchParams = useSearchParams()
+ const [mcpUrl, setMcpUrl] = useState("")
+ const [projectId, setProjectId] = useState("default")
+
+ const session = useSession()
+
+ // Extract MCP URL from query parameter
+ useEffect(() => {
+ const urlParam = searchParams.get("url")
+ if (urlParam) {
+ setMcpUrl(urlParam)
+ }
+ }, [searchParams])
+
+ useEffect(() => {
+ console.log("session", session)
+ if (!session.isPending && !session.data) {
+ const redirectUrl = new URL("/login", window.location.href)
+ redirectUrl.searchParams.set("redirect", window.location.href)
+ router.push(redirectUrl.toString())
+ return
+ }
+ }, [session, router])
+
+ // Extract userId from MCP URL
+ const getUserIdFromUrl = (url: string) => {
+ return url.split("/").at(-2) || ""
+ }
+
+ const migrateMutation = useMutation({
+ mutationFn: async (data: MigrateMCPRequest) => {
+ const response = await $fetch("@post/memories/migrate-mcp", {
+ body: data,
+ })
+
+ if (response.error) {
+ throw new Error(
+ response.error?.message || "Failed to migrate documents",
+ )
+ }
+
+ return response.data
+ },
+ onSuccess: (data: MigrateMCPResponse) => {
+ toast.success("Migration completed successfully", {
+ description: data.message,
+ })
+ // Redirect to home page after successful migration
+ setTimeout(() => {
+ router.push("/?open=mcp")
+ }, 2000) // Wait 2 seconds to show the success message
+ },
+ onError: (error: Error) => {
+ toast.error("Migration failed", {
+ description: error.message || "An unexpected error occurred",
+ })
+ },
+ })
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault()
+
+ const userId = getUserIdFromUrl(mcpUrl)
+ if (!userId) {
+ toast.error("Please enter a valid MCP URL")
+ return
+ }
+
+ migrateMutation.mutate({
+ userId,
+ projectId: projectId.trim() || "default",
+ })
+ }
+
+ return (
+ <div className="min-h-screen bg-[#0f1419] overflow-hidden relative">
+ {/* Background elements */}
+ <div className="absolute inset-0 bg-gradient-to-br from-blue-900/10 via-transparent to-purple-900/10" />
+
+ {/* Top navigation */}
+ <motion.div
+ initial={{ opacity: 0, y: -20 }}
+ animate={{ opacity: 1, y: 0 }}
+ className="absolute top-0 left-0 right-0 z-10 p-6 flex items-center justify-between"
+ >
+ <Link
+ href="/"
+ className="pointer-events-auto hover:opacity-80 transition-opacity"
+ >
+ <LogoFull className="h-8 hidden md:block text-white" />
+ <Logo className="h-8 md:hidden text-white" />
+ </Link>
+ </motion.div>
+
+ {/* Main content */}
+ <div className="flex items-center justify-center min-h-screen p-4 relative z-10">
+ <motion.div
+ initial={{ opacity: 0, y: 20, scale: 0.95 }}
+ animate={{ opacity: 1, y: 0, scale: 1 }}
+ transition={{
+ type: "spring",
+ stiffness: 300,
+ damping: 25,
+ delay: 0.1,
+ }}
+ className="w-full max-w-lg relative"
+ >
+ {/* Glass card with effect */}
+ <div className="relative rounded-2xl overflow-hidden">
+ <GlassMenuEffect rounded="rounded-2xl" />
+
+ <div className="relative z-10 p-8 md:p-10">
+ {/* Header */}
+ <motion.div
+ initial={{ opacity: 0, y: 10 }}
+ animate={{ opacity: 1, y: 0 }}
+ transition={{ delay: 0.2 }}
+ className="text-center mb-8"
+ >
+ <div className="flex items-center justify-center mb-4">
+ <div className="relative">
+ <div className="absolute inset-0 bg-blue-500/20 rounded-full blur-xl" />
+ <div className="relative bg-blue-500/10 p-3 rounded-full border border-blue-500/20">
+ <Zap className="w-6 h-6 text-blue-400" />
+ </div>
+ </div>
+ </div>
+ <h1 className="text-2xl md:text-3xl font-bold text-white mb-2">
+ Upgrade supermemory MCP
+ </h1>
+ <p className="text-slate-400 text-sm md:text-base">
+ Migrate your documents to the new MCP server
+ </p>
+ </motion.div>
+
+ {/* Form */}
+ <motion.form
+ initial={{ opacity: 0, y: 20 }}
+ animate={{ opacity: 1, y: 0 }}
+ transition={{ delay: 0.3 }}
+ onSubmit={handleSubmit}
+ className="space-y-6"
+ >
+ <div className="space-y-2">
+ <label
+ htmlFor="mcpUrl"
+ className="text-sm font-medium text-slate-200 flex items-center gap-2"
+ >
+ <Upload className="w-4 h-4" />
+ MCP URL
+ </label>
+ <div className="relative">
+ <Input
+ id="mcpUrl"
+ type="url"
+ value={mcpUrl}
+ onChange={(e) => setMcpUrl(e.target.value)}
+ placeholder="https://mcp.supermemory.ai/userId/sse"
+ className="bg-white/5 border-white/10 text-white placeholder:text-slate-500 focus:border-blue-500/50 focus:ring-blue-500/20 transition-all duration-200 pl-4 pr-4 py-3 rounded-xl"
+ disabled={migrateMutation.isPending}
+ />
+ </div>
+ </div>
+
+ <div className="space-y-2">
+ <label
+ htmlFor="projectId"
+ className="text-sm font-medium text-slate-200"
+ >
+ Project ID
+ </label>
+ <div className="relative">
+ <Input
+ id="projectId"
+ type="text"
+ value={projectId}
+ onChange={(e) => setProjectId(e.target.value)}
+ placeholder="Project ID (default: 'default')"
+ className="bg-white/5 border-white/10 text-white placeholder:text-slate-500 focus:border-blue-500/50 focus:ring-blue-500/20 transition-all duration-200 pl-4 pr-4 py-3 rounded-xl"
+ disabled={migrateMutation.isPending}
+ />
+ </div>
+ </div>
+
+ <motion.div
+ whileHover={{ scale: 1.01 }}
+ whileTap={{ scale: 0.99 }}
+ >
+ <Button
+ type="submit"
+ className="w-full bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white border-0 py-3 rounded-xl font-medium shadow-lg hover:shadow-blue-500/25 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
+ disabled={
+ migrateMutation.isPending || !getUserIdFromUrl(mcpUrl)
+ }
+ size="lg"
+ >
+ {migrateMutation.isPending ? (
+ <>
+ <Spinner className="mr-2 w-4 h-4" />
+ Migrating documents...
+ </>
+ ) : (
+ <>
+ Start Upgrade
+ <ArrowRight className="ml-2 w-4 h-4" />
+ </>
+ )}
+ </Button>
+ </motion.div>
+ </motion.form>
+
+ {/* Success/Error States */}
+ <AnimatePresence mode="wait">
+ {migrateMutation.isSuccess && migrateMutation.data && (
+ <motion.div
+ key="success"
+ initial={{ opacity: 0, y: 20, scale: 0.95 }}
+ animate={{ opacity: 1, y: 0, scale: 1 }}
+ exit={{ opacity: 0, y: -10, scale: 0.95 }}
+ transition={{ type: "spring", stiffness: 300, damping: 25 }}
+ className="mt-6"
+ >
+ <div className="relative rounded-xl overflow-hidden">
+ <div className="absolute inset-0 bg-gradient-to-r from-green-500/10 to-emerald-500/10" />
+ <div className="relative p-4 border border-green-500/20 rounded-xl">
+ <div className="text-green-400">
+ <div className="flex items-center gap-2 mb-2">
+ <CheckCircle className="w-5 h-5" />
+ <p className="font-medium">
+ Migration completed successfully!
+ </p>
+ </div>
+ <p className="text-sm text-green-300/80 mb-3">
+ Migrated {migrateMutation.data.migratedCount}{" "}
+ documents
+ </p>
+ {migrateMutation.data.documentIds &&
+ migrateMutation.data.documentIds.length > 0 && (
+ <details className="mt-3">
+ <summary className="cursor-pointer hover:text-green-300 transition-colors text-sm font-medium">
+ View migrated document IDs
+ </summary>
+ <div className="mt-3 space-y-2 max-h-40 overflow-y-auto">
+ {migrateMutation.data.documentIds.map(
+ (id) => (
+ <code
+ key={id}
+ className="block text-xs bg-black/30 px-3 py-2 rounded-lg border border-green-500/10 text-green-200"
+ >
+ {id}
+ </code>
+ ),
+ )}
+ </div>
+ </details>
+ )}
+ </div>
+ </div>
+ </div>
+ </motion.div>
+ )}
+
+ {migrateMutation.isError && (
+ <motion.div
+ key="error"
+ initial={{ opacity: 0, y: 20, scale: 0.95 }}
+ animate={{ opacity: 1, y: 0, scale: 1 }}
+ exit={{ opacity: 0, y: -10, scale: 0.95 }}
+ transition={{ type: "spring", stiffness: 300, damping: 25 }}
+ className="mt-6"
+ >
+ <div className="relative rounded-xl overflow-hidden">
+ <div className="absolute inset-0 bg-gradient-to-r from-red-500/10 to-pink-500/10" />
+ <div className="relative p-4 border border-red-500/20 rounded-xl">
+ <div className="text-red-400">
+ <p className="font-medium mb-1">Migration failed</p>
+ <p className="text-sm text-red-300/80">
+ {migrateMutation.error?.message ||
+ "An unexpected error occurred"}
+ </p>
+ </div>
+ </div>
+ </div>
+ </motion.div>
+ )}
+ </AnimatePresence>
+ </div>
+ </div>
+ </motion.div>
+ </div>
+ </div>
+ )
+}
diff --git a/apps/web/components/menu.tsx b/apps/web/components/menu.tsx
index 6eb81128..db012ab7 100644
--- a/apps/web/components/menu.tsx
+++ b/apps/web/components/menu.tsx
@@ -1,27 +1,28 @@
-"use client";
+"use client"
-import { useIsMobile } from "@hooks/use-mobile";
+import { useIsMobile } from "@hooks/use-mobile"
import {
fetchConsumerProProduct,
fetchMemoriesFeature,
-} from "@repo/lib/queries";
-import { Button } from "@repo/ui/components/button";
-import { ConnectAIModal } from "./connect-ai-modal";
-import { HeadingH2Bold } from "@repo/ui/text/heading/heading-h2-bold";
-import { GlassMenuEffect } from "@ui/other/glass-effect";
-import { useCustomer } from "autumn-js/react";
-import { MessageSquareMore, Plus, Puzzle, User, X } from "lucide-react";
-import { AnimatePresence, LayoutGroup, motion } from "motion/react";
-import { useEffect, useState } from "react";
-import { Drawer } from "vaul";
-import { useMobilePanel } from "@/lib/mobile-panel-context";
-import { TOUR_STEP_IDS } from "@/lib/tour-constants";
-import { useChatOpen } from "@/stores";
-import { ProjectSelector } from "./project-selector";
-import { useTour } from "./tour";
-import { AddMemoryExpandedView, AddMemoryView } from "./views/add-memory";
-import { IntegrationsView } from "./views/integrations";
-import { ProfileView } from "./views/profile";
+} from "@repo/lib/queries"
+import { Button } from "@repo/ui/components/button"
+import { ConnectAIModal } from "./connect-ai-modal"
+import { HeadingH2Bold } from "@repo/ui/text/heading/heading-h2-bold"
+import { GlassMenuEffect } from "@ui/other/glass-effect"
+import { useCustomer } from "autumn-js/react"
+import { MessageSquareMore, Plus, Puzzle, User, X } from "lucide-react"
+import { AnimatePresence, LayoutGroup, motion } from "motion/react"
+import { useRouter, useSearchParams } from "next/navigation"
+import { useCallback, useEffect, useState } from "react"
+import { Drawer } from "vaul"
+import { useMobilePanel } from "@/lib/mobile-panel-context"
+import { TOUR_STEP_IDS } from "@/lib/tour-constants"
+import { useChatOpen } from "@/stores"
+import { ProjectSelector } from "./project-selector"
+import { useTour } from "./tour"
+import { AddMemoryExpandedView, AddMemoryView } from "./views/add-memory"
+import { IntegrationsView } from "./views/integrations"
+import { ProfileView } from "./views/profile"
const MCPIcon = ({ className }: { className?: string }) => {
return (
@@ -36,45 +37,67 @@ const MCPIcon = ({ className }: { className?: string }) => {
<path d="M15.688 2.343a2.588 2.588 0 00-3.61 0l-9.626 9.44a.863.863 0 01-1.203 0 .823.823 0 010-1.18l9.626-9.44a4.313 4.313 0 016.016 0 4.116 4.116 0 011.204 3.54 4.3 4.3 0 013.609 1.18l.05.05a4.115 4.115 0 010 5.9l-8.706 8.537a.274.274 0 000 .393l1.788 1.754a.823.823 0 010 1.18.863.863 0 01-1.203 0l-1.788-1.753a1.92 1.92 0 010-2.754l8.706-8.538a2.47 2.47 0 000-3.54l-.05-.049a2.588 2.588 0 00-3.607-.003l-7.172 7.034-.002.002-.098.097a.863.863 0 01-1.204 0 .823.823 0 010-1.18l7.273-7.133a2.47 2.47 0 00-.003-3.537z" />
<path d="M14.485 4.703a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a4.115 4.115 0 000 5.9 4.314 4.314 0 006.016 0l7.12-6.982a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a2.588 2.588 0 01-3.61 0 2.47 2.47 0 010-3.54l7.12-6.982z" />
</svg>
- );
-};
+ )
+}
function Menu({ id }: { id?: string }) {
- const [isHovered, setIsHovered] = useState(false);
+ const router = useRouter()
+ const searchParams = useSearchParams()
+ const openParam = searchParams.get("open")
+
+ // Valid view names that can be opened via URL parameter
+ const validViews = [
+ "addUrl",
+ "mcp",
+ "projects",
+ "profile",
+ "integrations",
+ ] as const
+ type ValidView = (typeof validViews)[number]
+
+ const [isHovered, setIsHovered] = useState(false)
const [expandedView, setExpandedView] = useState<
"addUrl" | "mcp" | "projects" | "profile" | "integrations" | null
- >(null);
- const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
- const [isCollapsing, setIsCollapsing] = useState(false);
- const [showAddMemoryView, setShowAddMemoryView] = useState(false);
- const [showConnectAIModal, setShowConnectAIModal] = useState(false);
- const isMobile = useIsMobile();
- const { activePanel, setActivePanel } = useMobilePanel();
- const { setMenuExpanded } = useTour();
- const autumn = useCustomer();
- const { setIsOpen } = useChatOpen();
+ >(null)
+ const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
+ const [isCollapsing, setIsCollapsing] = useState(false)
+ const [showAddMemoryView, setShowAddMemoryView] = useState(false)
+ const [showConnectAIModal, setShowConnectAIModal] = useState(false)
+ const isMobile = useIsMobile()
+ const { activePanel, setActivePanel } = useMobilePanel()
+ const { setMenuExpanded } = useTour()
+ const autumn = useCustomer()
+ const { setIsOpen } = useChatOpen()
- const { data: memoriesCheck } = fetchMemoriesFeature(autumn);
+ const { data: memoriesCheck } = fetchMemoriesFeature(autumn)
- const memoriesUsed = memoriesCheck?.usage ?? 0;
- const memoriesLimit = memoriesCheck?.included_usage ?? 0;
+ const memoriesUsed = memoriesCheck?.usage ?? 0
+ const memoriesLimit = memoriesCheck?.included_usage ?? 0
- const { data: proCheck } = fetchConsumerProProduct(autumn);
+ const { data: proCheck } = fetchConsumerProProduct(autumn)
useEffect(() => {
if (memoriesCheck) {
- console.log({ memoriesCheck });
+ console.log({ memoriesCheck })
}
if (proCheck) {
- console.log({ proCheck });
+ console.log({ proCheck })
}
- }, [memoriesCheck, proCheck]);
+ }, [memoriesCheck, proCheck])
- const isProUser = proCheck?.allowed ?? false;
+ // Function to clear the 'open' parameter from URL
+ const clearOpenParam = useCallback(() => {
+ const newSearchParams = new URLSearchParams(searchParams.toString())
+ newSearchParams.delete("open")
+ const newUrl = `${window.location.pathname}${newSearchParams.toString() ? `?${newSearchParams.toString()}` : ""}`
+ router.replace(newUrl)
+ }, [searchParams, router])
+
+ const isProUser = proCheck?.allowed ?? false
const shouldShowLimitWarning =
- !isProUser && memoriesUsed >= memoriesLimit * 0.8;
+ !isProUser && memoriesUsed >= memoriesLimit * 0.8
// Map menu item keys to tour IDs
const menuItemTourIds: Record<string, string> = {
@@ -82,7 +105,7 @@ function Menu({ id }: { id?: string }) {
projects: TOUR_STEP_IDS.MENU_PROJECTS,
mcp: TOUR_STEP_IDS.MENU_MCP,
integrations: "", // No tour ID for integrations yet
- };
+ }
const menuItems = [
{
@@ -115,61 +138,102 @@ function Menu({ id }: { id?: string }) {
key: "profile" as const,
disabled: false,
},
- ];
+ ]
const handleMenuItemClick = (
key: "chat" | "addUrl" | "mcp" | "projects" | "profile" | "integrations",
) => {
if (key === "chat") {
- setIsOpen(true);
- setIsMobileMenuOpen(false);
+ setIsOpen(true)
+ setIsMobileMenuOpen(false)
if (isMobile) {
- setActivePanel("chat");
+ setActivePanel("chat")
}
} else if (key === "mcp") {
// Open ConnectAIModal directly for MCP
- setIsMobileMenuOpen(false);
- setExpandedView(null);
- setShowConnectAIModal(true);
+ setIsMobileMenuOpen(false)
+ setExpandedView(null)
+ setShowConnectAIModal(true)
} else {
if (expandedView === key) {
- setIsCollapsing(true);
- setExpandedView(null);
+ setIsCollapsing(true)
+ setExpandedView(null)
} else if (key === "addUrl") {
- setShowAddMemoryView(true);
- setExpandedView(null);
+ setShowAddMemoryView(true)
+ setExpandedView(null)
} else {
- setExpandedView(key);
+ setExpandedView(key)
}
if (isMobile) {
- setActivePanel("menu");
+ setActivePanel("menu")
}
}
- };
+ }
+
+ // Handle initial view opening based on URL parameter
+ useEffect(() => {
+ if (openParam) {
+ if (openParam === "chat") {
+ setIsOpen(true)
+ setIsMobileMenuOpen(false)
+ if (isMobile) {
+ setActivePanel("chat")
+ }
+ } else if (openParam === "mcp") {
+ // Open ConnectAIModal directly for MCP
+ setIsMobileMenuOpen(false)
+ setExpandedView(null)
+ setShowConnectAIModal(true)
+ } else if (openParam === "addUrl") {
+ setShowAddMemoryView(true)
+ setExpandedView(null)
+ if (isMobile) {
+ setIsMobileMenuOpen(true)
+ setActivePanel("menu")
+ }
+ } else if (validViews.includes(openParam as ValidView)) {
+ // For other valid views like "profile", "integrations"
+ setExpandedView(openParam as ValidView)
+ if (isMobile) {
+ setIsMobileMenuOpen(true)
+ setActivePanel("menu")
+ }
+ }
+
+ // Clear the parameter from URL after performing any action
+ clearOpenParam()
+ }
+ }, [
+ openParam,
+ isMobile,
+ setIsOpen,
+ setActivePanel,
+ validViews,
+ clearOpenParam,
+ ])
// Watch for active panel changes on mobile
useEffect(() => {
if (isMobile && activePanel !== "menu" && activePanel !== null) {
// Another panel became active, close the menu
- setIsMobileMenuOpen(false);
- setExpandedView(null);
+ setIsMobileMenuOpen(false)
+ setExpandedView(null)
}
- }, [isMobile, activePanel]);
+ }, [isMobile, activePanel])
// Notify tour provider about expansion state changes
useEffect(() => {
const isExpanded = isMobile
? isMobileMenuOpen || !!expandedView
- : isHovered || !!expandedView;
- setMenuExpanded(isExpanded);
- }, [isMobile, isMobileMenuOpen, isHovered, expandedView, setMenuExpanded]);
+ : isHovered || !!expandedView
+ setMenuExpanded(isExpanded)
+ }, [isMobile, isMobileMenuOpen, isHovered, expandedView, setMenuExpanded])
// Calculate width based on state
- const menuWidth = expandedView || isCollapsing ? 600 : isHovered ? 160 : 56;
+ const menuWidth = expandedView || isCollapsing ? 600 : isHovered ? 160 : 56
// Dynamic z-index for mobile based on active panel
- const mobileZIndex =
- isMobile && activePanel === "menu" ? "z-[70]" : "z-[100]";
+ const mobileZIndex = isMobile && activePanel === "menu" ? "z-[70]" : "z-[100]"
return (
<>
@@ -378,8 +442,8 @@ function Menu({ id }: { id?: string }) {
<Button
className="text-white/70 hover:text-white transition-colors duration-200"
onClick={() => {
- setIsCollapsing(true);
- setExpandedView(null);
+ setIsCollapsing(true)
+ setExpandedView(null)
}}
size="icon"
variant="ghost"
@@ -418,9 +482,9 @@ function Menu({ id }: { id?: string }) {
open={isMobileMenuOpen || !!expandedView}
onOpenChange={(open) => {
if (!open) {
- setIsMobileMenuOpen(false);
- setExpandedView(null);
- setActivePanel(null);
+ setIsMobileMenuOpen(false)
+ setExpandedView(null)
+ setActivePanel(null)
}
}}
>
@@ -433,8 +497,8 @@ function Menu({ id }: { id?: string }) {
className="w-14 h-14 flex items-center justify-center text-white rounded-full shadow-2xl"
initial={{ scale: 0.8, opacity: 0 }}
onClick={() => {
- setIsMobileMenuOpen(true);
- setActivePanel("menu");
+ setIsMobileMenuOpen(true)
+ setActivePanel("menu")
}}
transition={{
duration: 0.3,
@@ -530,13 +594,13 @@ function Menu({ id }: { id?: string }) {
initial={{ opacity: 0, y: 10 }}
layout
onClick={() => {
- handleMenuItemClick(item.key);
+ handleMenuItemClick(item.key)
if (
item.key !== "mcp" &&
item.key !== "profile" &&
item.key !== "integrations"
) {
- setIsMobileMenuOpen(false);
+ setIsMobileMenuOpen(false)
}
}}
type="button"
@@ -600,8 +664,8 @@ function Menu({ id }: { id?: string }) {
<Button
className="text-white/70 hover:text-white transition-colors duration-200"
onClick={() => {
- setIsCollapsing(true);
- setExpandedView(null);
+ setIsCollapsing(true)
+ setExpandedView(null)
}}
size="icon"
variant="ghost"
@@ -643,7 +707,7 @@ function Menu({ id }: { id?: string }) {
<Button className="hidden">Connect AI Assistant</Button>
</ConnectAIModal>
</>
- );
+ )
}
-export default Menu;
+export default Menu
diff --git a/apps/web/components/views/profile.tsx b/apps/web/components/views/profile.tsx
index 336b9416..b19706f7 100644
--- a/apps/web/components/views/profile.tsx
+++ b/apps/web/components/views/profile.tsx
@@ -1,15 +1,15 @@
-"use client";
+"use client"
-import { authClient } from "@lib/auth";
-import { useAuth } from "@lib/auth-context";
+import { authClient } from "@lib/auth"
+import { useAuth } from "@lib/auth-context"
import {
fetchConnectionsFeature,
fetchMemoriesFeature,
fetchSubscriptionStatus,
-} from "@lib/queries";
-import { Button } from "@repo/ui/components/button";
-import { HeadingH3Bold } from "@repo/ui/text/heading/heading-h3-bold";
-import { useCustomer } from "autumn-js/react";
+} from "@lib/queries"
+import { Button } from "@repo/ui/components/button"
+import { HeadingH3Bold } from "@repo/ui/text/heading/heading-h3-bold"
+import { useCustomer } from "autumn-js/react"
import {
CheckCircle,
CreditCard,
@@ -17,27 +17,27 @@ import {
LogOut,
User,
X,
-} from "lucide-react";
-import { motion } from "motion/react";
-import Link from "next/link";
-import { usePathname, useRouter } from "next/navigation";
-import { useState } from "react";
-import { analytics } from "@/lib/analytics";
+} from "lucide-react"
+import { motion } from "motion/react"
+import Link from "next/link"
+import { usePathname, useRouter } from "next/navigation"
+import { useState } from "react"
+import { analytics } from "@/lib/analytics"
export function ProfileView() {
- const router = useRouter();
- const pathname = usePathname();
- const { user: session, org } = useAuth();
- const organizations = org;
- const autumn = useCustomer();
- const [isLoading, setIsLoading] = useState(false);
+ const router = useRouter()
+ const pathname = usePathname()
+ const { user: session, org } = useAuth()
+ const organizations = org
+ const autumn = useCustomer()
+ const [isLoading, setIsLoading] = useState(false)
- const { data: memoriesCheck } = fetchMemoriesFeature(autumn as any);
- const memoriesUsed = memoriesCheck?.usage ?? 0;
- const memoriesLimit = memoriesCheck?.included_usage ?? 0;
+ const { data: memoriesCheck } = fetchMemoriesFeature(autumn as any)
+ const memoriesUsed = memoriesCheck?.usage ?? 0
+ const memoriesLimit = memoriesCheck?.included_usage ?? 0
- const { data: connectionsCheck } = fetchConnectionsFeature(autumn as any);
- const connectionsUsed = connectionsCheck?.usage ?? 0;
+ const { data: connectionsCheck } = fetchConnectionsFeature(autumn as any)
+ const connectionsUsed = connectionsCheck?.usage ?? 0
// Fetch subscription status with React Query
const {
@@ -45,36 +45,36 @@ export function ProfileView() {
consumer_pro: null,
},
isLoading: isCheckingStatus,
- } = fetchSubscriptionStatus(autumn as any);
+ } = fetchSubscriptionStatus(autumn as any)
- const isPro = status.consumer_pro;
+ const isPro = status.consumer_pro
const handleLogout = () => {
- analytics.userSignedOut();
- authClient.signOut();
- router.push("/login");
- };
+ analytics.userSignedOut()
+ authClient.signOut()
+ router.push("/login")
+ }
const handleUpgrade = async () => {
- setIsLoading(true);
+ setIsLoading(true)
try {
await autumn.attach({
productId: "consumer_pro",
successUrl: "https://app.supermemory.ai/",
- });
- window.location.reload();
+ })
+ window.location.reload()
} catch (error) {
- console.error(error);
- setIsLoading(false);
+ console.error(error)
+ setIsLoading(false)
}
- };
+ }
// Handle manage billing
const handleManageBilling = async () => {
await autumn.openBillingPortal({
returnUrl: "https://app.supermemory.ai",
- });
- };
+ })
+ }
if (session?.isAnonymous) {
return (
@@ -99,7 +99,7 @@ export function ProfileView() {
</motion.div>
</motion.div>
</div>
- );
+ )
}
return (
@@ -282,5 +282,5 @@ export function ProfileView() {
Sign Out
</Button>
</div>
- );
+ )
}
diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts
index 71c7fc1d..9862462d 100644
--- a/apps/web/middleware.ts
+++ b/apps/web/middleware.ts
@@ -1,29 +1,30 @@
-import { $fetch } from "@lib/api";
-import { getSessionCookie } from "better-auth/cookies";
-import { NextResponse } from "next/server";
+import { getSessionCookie } from "better-auth/cookies"
+import { NextResponse } from "next/server"
export default async function middleware(request: Request) {
- console.debug("[MIDDLEWARE] === MIDDLEWARE START ===");
- const url = new URL(request.url);
- console.debug("[MIDDLEWARE] Path:", url.pathname);
- console.debug("[MIDDLEWARE] Method:", request.method);
+ console.debug("[MIDDLEWARE] === MIDDLEWARE START ===")
+ const url = new URL(request.url)
+ console.debug("[MIDDLEWARE] Path:", url.pathname)
+ console.debug("[MIDDLEWARE] Method:", request.method)
- const sessionCookie = getSessionCookie(request);
- console.debug("[MIDDLEWARE] Session cookie exists:", !!sessionCookie);
+ const sessionCookie = getSessionCookie(request)
+ console.debug("[MIDDLEWARE] Session cookie exists:", !!sessionCookie)
// Always allow access to login and waitlist pages
- const publicPaths = ["/login"];
+ const publicPaths = ["/login"]
if (publicPaths.includes(url.pathname)) {
- console.debug("[MIDDLEWARE] Public path, allowing access");
- return NextResponse.next();
+ console.debug("[MIDDLEWARE] Public path, allowing access")
+ return NextResponse.next()
}
// If no session cookie and not on a public path, redirect to login
if (!sessionCookie) {
console.debug(
"[MIDDLEWARE] No session cookie and not on public path, redirecting to /login",
- );
- return NextResponse.redirect(new URL("/login", request.url));
+ )
+ const url = new URL("/login", request.url)
+ url.searchParams.set("redirect", request.url)
+ return NextResponse.redirect(url)
}
// TEMPORARILY DISABLED: Waitlist check
@@ -39,13 +40,13 @@ export default async function middleware(request: Request) {
// }
// }
- console.debug("[MIDDLEWARE] Passing through to next handler");
- console.debug("[MIDDLEWARE] === MIDDLEWARE END ===");
- return NextResponse.next();
+ console.debug("[MIDDLEWARE] Passing through to next handler")
+ console.debug("[MIDDLEWARE] === MIDDLEWARE END ===")
+ return NextResponse.next()
}
export const config = {
matcher: [
"/((?!_next/static|_next/image|images|icon.png|monitoring|opengraph-image.png|ingest|api|login|api/emails).*)",
],
-};
+}
diff --git a/packages/ui/pages/login.tsx b/packages/ui/pages/login.tsx
index fcd48eae..74765da1 100644
--- a/packages/ui/pages/login.tsx
+++ b/packages/ui/pages/login.tsx
@@ -1,26 +1,26 @@
-"use client"
+"use client";
-import { signIn } from "@lib/auth"
-import { usePostHog } from "@lib/posthog"
-import { LogoFull } from "@repo/ui/assets/Logo"
-import { TextSeparator } from "@repo/ui/components/text-separator"
-import { ExternalAuthButton } from "@ui/button/external-auth"
-import { Button } from "@ui/components/button"
-import { Badge } from "@ui/components/badge"
+import { signIn } from "@lib/auth";
+import { usePostHog } from "@lib/posthog";
+import { LogoFull } from "@repo/ui/assets/Logo";
+import { TextSeparator } from "@repo/ui/components/text-separator";
+import { ExternalAuthButton } from "@ui/button/external-auth";
+import { Button } from "@ui/components/button";
+import { Badge } from "@ui/components/badge";
import {
Carousel,
CarouselContent,
CarouselItem,
-} from "@ui/components/carousel"
-import { LabeledInput } from "@ui/input/labeled-input"
-import { HeadingH1Medium } from "@ui/text/heading/heading-h1-medium"
-import { HeadingH3Medium } from "@ui/text/heading/heading-h3-medium"
-import { Label1Regular } from "@ui/text/label/label-1-regular"
-import { Title1Bold } from "@ui/text/title/title-1-bold"
-import Autoplay from "embla-carousel-autoplay"
-import Image from "next/image"
-import { useRouter, useSearchParams } from "next/navigation"
-import { useState, useEffect } from "react"
+} from "@ui/components/carousel";
+import { LabeledInput } from "@ui/input/labeled-input";
+import { HeadingH1Medium } from "@ui/text/heading/heading-h1-medium";
+import { HeadingH3Medium } from "@ui/text/heading/heading-h3-medium";
+import { Label1Regular } from "@ui/text/label/label-1-regular";
+import { Title1Bold } from "@ui/text/title/title-1-bold";
+import Autoplay from "embla-carousel-autoplay";
+import Image from "next/image";
+import { useRouter, useSearchParams } from "next/navigation";
+import { useState, useEffect } from "react";
export function LoginPage({
heroText = "The unified memory API for the AI era.",
@@ -29,101 +29,122 @@ export function LoginPage({
"Trusted by Open Source, enterprise and developers.",
],
}) {
- const [email, setEmail] = useState("")
- const [submittedEmail, setSubmittedEmail] = useState<string | null>(null)
- const [isLoading, setIsLoading] = useState(false)
- const [isLoadingEmail, setIsLoadingEmail] = useState(false)
- const [error, setError] = useState<string | null>(null)
- const [lastUsedMethod, setLastUsedMethod] = useState<string | null>(null)
- const router = useRouter()
+ const [email, setEmail] = useState("");
+ const [submittedEmail, setSubmittedEmail] = useState<string | null>(null);
+ const [isLoading, setIsLoading] = useState(false);
+ const [isLoadingEmail, setIsLoadingEmail] = useState(false);
+ const [error, setError] = useState<string | null>(null);
+ const [lastUsedMethod, setLastUsedMethod] = useState<string | null>(null);
+ const router = useRouter();
- const posthog = usePostHog()
+ const posthog = usePostHog();
- const params = useSearchParams()
+ const params = useSearchParams();
+
+ // Get redirect URL from query params
+ const redirectUrl = params.get("redirect");
+
+ // Create callback URL that includes redirect parameter if provided
+ const getCallbackURL = () => {
+ const origin = window.location.origin;
+ if (redirectUrl) {
+ // Validate that the redirect URL is safe (same origin or allow external based on your security requirements)
+ try {
+ const url = new URL(redirectUrl, origin);
+ return url.toString();
+ } catch {
+ // If redirect URL is invalid, fall back to origin
+ return origin;
+ }
+ }
+ return origin;
+ };
// Load last used method from localStorage on mount
useEffect(() => {
- const savedMethod = localStorage.getItem('supermemory-last-login-method')
- setLastUsedMethod(savedMethod)
- }, [])
+ const savedMethod = localStorage.getItem("supermemory-last-login-method");
+ setLastUsedMethod(savedMethod);
+ }, []);
// Record the pending login method (will be committed after successful auth)
function setPendingLoginMethod(method: string) {
try {
- localStorage.setItem('supermemory-pending-login-method', method)
- localStorage.setItem('supermemory-pending-login-timestamp', String(Date.now()))
- } catch { }
+ localStorage.setItem("supermemory-pending-login-method", method);
+ localStorage.setItem(
+ "supermemory-pending-login-timestamp",
+ String(Date.now()),
+ );
+ } catch {}
}
// If we land back on this page with an error, clear any pending marker
useEffect(() => {
if (params.get("error")) {
try {
- localStorage.removeItem('supermemory-pending-login-method')
- localStorage.removeItem('supermemory-pending-login-timestamp')
- } catch { }
+ localStorage.removeItem("supermemory-pending-login-method");
+ localStorage.removeItem("supermemory-pending-login-timestamp");
+ } catch {}
}
- }, [params])
-
-
+ }, [params]);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
- e.preventDefault()
- setIsLoading(true)
- setIsLoadingEmail(true)
- setError(null)
+ e.preventDefault();
+ setIsLoading(true);
+ setIsLoadingEmail(true);
+ setError(null);
// Track login attempt
posthog.capture("login_attempt", {
method: "magic_link",
email_domain: email.split("@")[1] || "unknown",
- })
+ });
try {
await signIn.magicLink({
- callbackURL: window.location.origin,
+ callbackURL: getCallbackURL(),
email,
- })
- setSubmittedEmail(email)
- setPendingLoginMethod('magic_link')
+ });
+ setSubmittedEmail(email);
+ setPendingLoginMethod("magic_link");
// Track successful magic link send
posthog.capture("login_magic_link_sent", {
email_domain: email.split("@")[1] || "unknown",
- })
+ });
} catch (error) {
- console.error(error)
+ console.error(error);
// Track login failure
posthog.capture("login_failed", {
method: "magic_link",
error: error instanceof Error ? error.message : "Unknown error",
email_domain: email.split("@")[1] || "unknown",
- })
+ });
setError(
error instanceof Error
? error.message
: "Failed to send login link. Please try again.",
- )
- setIsLoading(false)
- setIsLoadingEmail(false)
- return
+ );
+ setIsLoading(false);
+ setIsLoadingEmail(false);
+ return;
}
- setIsLoading(false)
- setIsLoadingEmail(false)
- }
+ setIsLoading(false);
+ setIsLoadingEmail(false);
+ };
const handleSubmitToken = async (event: React.FormEvent<HTMLFormElement>) => {
- event.preventDefault()
- setIsLoading(true)
+ event.preventDefault();
+ setIsLoading(true);
- const formData = new FormData(event.currentTarget)
- const token = formData.get("token") as string
+ const formData = new FormData(event.currentTarget);
+ const token = formData.get("token") as string;
+ const callbackURL = getCallbackURL();
router.push(
- `${process.env.NEXT_PUBLIC_BACKEND_URL}/api/auth/magic-link/verify?token=${token}&callbackURL=${encodeURIComponent(window.location.host)}`,
- )
- }
+ `${process.env.NEXT_PUBLIC_BACKEND_URL}/api/auth/magic-link/verify?token=${token}&callbackURL=${encodeURIComponent(callbackURL)}`,
+ );
+ };
return (
<section className="min-h-screen flex flex-col lg:grid lg:grid-cols-12 items-center justify-center p-4 sm:p-6 md:p-8 lg:px-[5rem] lg:py-[3.125rem] gap-6 lg:gap-[5rem] max-w-[400rem] mx-auto">
@@ -225,8 +246,8 @@ export function LoginPage({
disabled: isLoading,
id: "email",
onChange: (e) => {
- setEmail(e.target.value)
- error && setError(null)
+ setEmail(e.target.value);
+ error && setError(null);
},
required: true,
value: email,
@@ -241,9 +262,11 @@ export function LoginPage({
? "Sending login link..."
: "Log in to supermemory"}
</Button>
- {lastUsedMethod === 'magic_link' && (
+ {lastUsedMethod === "magic_link" && (
<div className="absolute -top-2 -right-2">
- <Badge variant="default" className="text-xs">Last used</Badge>
+ <Badge variant="default" className="text-xs">
+ Last used
+ </Badge>
</div>
)}
</div>
@@ -251,14 +274,14 @@ export function LoginPage({
</form>
{process.env.NEXT_PUBLIC_HOST_ID === "supermemory" ||
- !process.env.NEXT_PUBLIC_GOOGLE_AUTH_ENABLED ||
- !process.env.NEXT_PUBLIC_GITHUB_AUTH_ENABLED ? (
+ !process.env.NEXT_PUBLIC_GOOGLE_AUTH_ENABLED ||
+ !process.env.NEXT_PUBLIC_GITHUB_AUTH_ENABLED ? (
<TextSeparator text="OR" />
) : null}
<div className="flex flex-col sm:flex-row flex-wrap gap-3 lg:gap-4">
{process.env.NEXT_PUBLIC_HOST_ID === "supermemory" ||
- !process.env.NEXT_PUBLIC_GOOGLE_AUTH_ENABLED ? (
+ !process.env.NEXT_PUBLIC_GOOGLE_AUTH_ENABLED ? (
<div className="relative flex-grow">
<ExternalAuthButton
authIcon={
@@ -293,32 +316,34 @@ export function LoginPage({
className="w-full"
disabled={isLoading}
onClick={() => {
- if (isLoading) return
- setIsLoading(true)
+ if (isLoading) return;
+ setIsLoading(true);
posthog.capture("login_attempt", {
method: "social",
provider: "google",
- })
- setPendingLoginMethod('google')
+ });
+ setPendingLoginMethod("google");
signIn
.social({
- callbackURL: window.location.origin,
+ callbackURL: getCallbackURL(),
provider: "google",
})
.finally(() => {
- setIsLoading(false)
- })
+ setIsLoading(false);
+ });
}}
/>
- {lastUsedMethod === 'google' && (
+ {lastUsedMethod === "google" && (
<div className="absolute -top-2 -right-2">
- <Badge variant="default" className="text-xs">Last used</Badge>
+ <Badge variant="default" className="text-xs">
+ Last used
+ </Badge>
</div>
)}
</div>
) : null}
{process.env.NEXT_PUBLIC_HOST_ID === "supermemory" ||
- !process.env.NEXT_PUBLIC_GITHUB_AUTH_ENABLED ? (
+ !process.env.NEXT_PUBLIC_GITHUB_AUTH_ENABLED ? (
<div className="relative flex-grow">
<ExternalAuthButton
authIcon={
@@ -355,26 +380,28 @@ export function LoginPage({
className="w-full"
disabled={isLoading}
onClick={() => {
- if (isLoading) return
- setIsLoading(true)
+ if (isLoading) return;
+ setIsLoading(true);
posthog.capture("login_attempt", {
method: "social",
provider: "github",
- })
- setPendingLoginMethod('github')
+ });
+ setPendingLoginMethod("github");
signIn
.social({
- callbackURL: window.location.origin,
+ callbackURL: getCallbackURL(),
provider: "github",
})
.finally(() => {
- setIsLoading(false)
- })
+ setIsLoading(false);
+ });
}}
/>
- {lastUsedMethod === 'github' && (
+ {lastUsedMethod === "github" && (
<div className="absolute -top-2 -right-2">
- <Badge variant="default" className="text-xs">Last used</Badge>
+ <Badge variant="default" className="text-xs">
+ Last used
+ </Badge>
</div>
)}
</div>
@@ -403,5 +430,5 @@ export function LoginPage({
</div>
)}
</section>
- )
-} \ No newline at end of file
+ );
+}