@cortexmemory/cli 0.27.1 → 0.27.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/convex.js +1 -1
- package/dist/commands/convex.js.map +1 -1
- package/dist/commands/deploy.d.ts +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +839 -141
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +89 -26
- package/dist/commands/dev.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/utils/app-template-sync.d.ts +95 -0
- package/dist/utils/app-template-sync.d.ts.map +1 -0
- package/dist/utils/app-template-sync.js +445 -0
- package/dist/utils/app-template-sync.js.map +1 -0
- package/dist/utils/deployment-selector.d.ts +21 -0
- package/dist/utils/deployment-selector.d.ts.map +1 -1
- package/dist/utils/deployment-selector.js +32 -0
- package/dist/utils/deployment-selector.js.map +1 -1
- package/dist/utils/init/graph-setup.d.ts.map +1 -1
- package/dist/utils/init/graph-setup.js +13 -2
- package/dist/utils/init/graph-setup.js.map +1 -1
- package/package.json +1 -1
- package/templates/vercel-ai-quickstart/app/api/auth/check/route.ts +30 -0
- package/templates/vercel-ai-quickstart/app/api/auth/login/route.ts +128 -0
- package/templates/vercel-ai-quickstart/app/api/auth/register/route.ts +94 -0
- package/templates/vercel-ai-quickstart/app/api/auth/setup/route.ts +59 -0
- package/templates/vercel-ai-quickstart/app/api/chat/route.ts +139 -3
- package/templates/vercel-ai-quickstart/app/api/chat-v6/route.ts +333 -0
- package/templates/vercel-ai-quickstart/app/api/conversations/route.ts +179 -0
- package/templates/vercel-ai-quickstart/app/globals.css +161 -0
- package/templates/vercel-ai-quickstart/app/page.tsx +110 -11
- package/templates/vercel-ai-quickstart/components/AdminSetup.tsx +139 -0
- package/templates/vercel-ai-quickstart/components/AuthProvider.tsx +283 -0
- package/templates/vercel-ai-quickstart/components/ChatHistorySidebar.tsx +323 -0
- package/templates/vercel-ai-quickstart/components/ChatInterface.tsx +117 -17
- package/templates/vercel-ai-quickstart/components/LoginScreen.tsx +202 -0
- package/templates/vercel-ai-quickstart/jest.config.js +52 -0
- package/templates/vercel-ai-quickstart/lib/agents/memory-agent.ts +165 -0
- package/templates/vercel-ai-quickstart/lib/cortex.ts +27 -0
- package/templates/vercel-ai-quickstart/lib/password.ts +120 -0
- package/templates/vercel-ai-quickstart/lib/versions.ts +60 -0
- package/templates/vercel-ai-quickstart/next.config.js +20 -0
- package/templates/vercel-ai-quickstart/package.json +11 -2
- package/templates/vercel-ai-quickstart/test-api.mjs +272 -0
- package/templates/vercel-ai-quickstart/tests/e2e/chat-memory-flow.test.ts +454 -0
- package/templates/vercel-ai-quickstart/tests/helpers/mock-cortex.ts +263 -0
- package/templates/vercel-ai-quickstart/tests/helpers/setup.ts +48 -0
- package/templates/vercel-ai-quickstart/tests/integration/auth.test.ts +455 -0
- package/templates/vercel-ai-quickstart/tests/integration/conversations.test.ts +461 -0
- package/templates/vercel-ai-quickstart/tests/unit/password.test.ts +228 -0
- package/templates/vercel-ai-quickstart/tsconfig.json +1 -1
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import dynamic from "next/dynamic";
|
|
4
|
-
import { useState } from "react";
|
|
4
|
+
import { useState, useCallback, useEffect } from "react";
|
|
5
5
|
import { useLayerTracking } from "@/lib/layer-tracking";
|
|
6
|
+
import { detectVersions, type VersionInfo } from "@/lib/versions";
|
|
7
|
+
import { AuthProvider, useAuth } from "@/components/AuthProvider";
|
|
8
|
+
import { AdminSetup } from "@/components/AdminSetup";
|
|
9
|
+
import { LoginScreen } from "@/components/LoginScreen";
|
|
6
10
|
|
|
7
11
|
// Dynamic imports to avoid SSR issues with framer-motion
|
|
8
12
|
const ChatInterface = dynamic(
|
|
@@ -33,10 +37,23 @@ const HealthStatus = dynamic(
|
|
|
33
37
|
})),
|
|
34
38
|
{ ssr: false },
|
|
35
39
|
);
|
|
40
|
+
const ChatHistorySidebar = dynamic(
|
|
41
|
+
() =>
|
|
42
|
+
import("@/components/ChatHistorySidebar").then((m) => ({
|
|
43
|
+
default: m.ChatHistorySidebar,
|
|
44
|
+
})),
|
|
45
|
+
{ ssr: false },
|
|
46
|
+
);
|
|
36
47
|
|
|
37
|
-
|
|
48
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
49
|
+
// Main App Content (with auth)
|
|
50
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
51
|
+
|
|
52
|
+
function MainContent() {
|
|
53
|
+
const { isLoading, isAdminSetup, isAuthenticated, user } = useAuth();
|
|
38
54
|
const [memorySpaceId, setMemorySpaceId] = useState("quickstart-demo");
|
|
39
|
-
const [
|
|
55
|
+
const [currentConversationId, setCurrentConversationId] = useState<string | null>(null);
|
|
56
|
+
const [versions, setVersions] = useState<VersionInfo | null>(null);
|
|
40
57
|
const {
|
|
41
58
|
layers,
|
|
42
59
|
isOrchestrating,
|
|
@@ -45,11 +62,64 @@ export default function Home() {
|
|
|
45
62
|
resetLayers,
|
|
46
63
|
} = useLayerTracking();
|
|
47
64
|
|
|
65
|
+
// Detect SDK versions on mount
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
detectVersions().then(setVersions);
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
// Handle new chat
|
|
71
|
+
const handleNewChat = useCallback(() => {
|
|
72
|
+
setCurrentConversationId(null);
|
|
73
|
+
resetLayers();
|
|
74
|
+
}, [resetLayers]);
|
|
75
|
+
|
|
76
|
+
// Handle conversation selection
|
|
77
|
+
const handleSelectConversation = useCallback((conversationId: string) => {
|
|
78
|
+
setCurrentConversationId(conversationId);
|
|
79
|
+
resetLayers();
|
|
80
|
+
}, [resetLayers]);
|
|
81
|
+
|
|
82
|
+
// Handle conversation update (e.g., title change after first message)
|
|
83
|
+
const handleConversationUpdate = useCallback((conversationId: string) => {
|
|
84
|
+
// Update current conversation ID if it was null (new chat created)
|
|
85
|
+
if (!currentConversationId) {
|
|
86
|
+
setCurrentConversationId(conversationId);
|
|
87
|
+
}
|
|
88
|
+
}, [currentConversationId]);
|
|
89
|
+
|
|
90
|
+
// Loading state
|
|
91
|
+
if (isLoading) {
|
|
92
|
+
return (
|
|
93
|
+
<div className="min-h-screen flex items-center justify-center">
|
|
94
|
+
<div className="text-center">
|
|
95
|
+
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gradient-to-br from-cortex-500 to-cortex-700 flex items-center justify-center animate-pulse">
|
|
96
|
+
<span className="text-3xl">🧠</span>
|
|
97
|
+
</div>
|
|
98
|
+
<p className="text-gray-400">Loading Cortex...</p>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// First-run: Admin setup
|
|
105
|
+
if (isAdminSetup === false) {
|
|
106
|
+
return <AdminSetup />;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Not authenticated: Login/Register
|
|
110
|
+
if (!isAuthenticated) {
|
|
111
|
+
return <LoginScreen />;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Get userId from authenticated user
|
|
115
|
+
const userId = user?.id || "demo-user";
|
|
116
|
+
|
|
117
|
+
// Main authenticated interface
|
|
48
118
|
return (
|
|
49
|
-
<main className="
|
|
119
|
+
<main className="h-screen flex flex-col overflow-hidden">
|
|
50
120
|
{/* Header */}
|
|
51
121
|
<header className="border-b border-white/10 px-6 py-4">
|
|
52
|
-
<div className="
|
|
122
|
+
<div className="flex items-center justify-between">
|
|
53
123
|
<div className="flex items-center gap-3">
|
|
54
124
|
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-cortex-500 to-cortex-700 flex items-center justify-center">
|
|
55
125
|
<span className="text-xl">🧠</span>
|
|
@@ -72,20 +142,31 @@ export default function Home() {
|
|
|
72
142
|
</div>
|
|
73
143
|
</header>
|
|
74
144
|
|
|
75
|
-
{/* Main Content */}
|
|
145
|
+
{/* Main Content - Three Column Layout */}
|
|
76
146
|
<div className="flex-1 flex overflow-hidden">
|
|
77
|
-
{/* Chat
|
|
147
|
+
{/* Left: Chat History Sidebar */}
|
|
148
|
+
<ChatHistorySidebar
|
|
149
|
+
memorySpaceId={memorySpaceId}
|
|
150
|
+
currentConversationId={currentConversationId}
|
|
151
|
+
onSelectConversation={handleSelectConversation}
|
|
152
|
+
onNewChat={handleNewChat}
|
|
153
|
+
/>
|
|
154
|
+
|
|
155
|
+
{/* Center: Chat Section */}
|
|
78
156
|
<div className="flex-1 flex flex-col border-r border-white/10">
|
|
79
157
|
<ChatInterface
|
|
80
158
|
memorySpaceId={memorySpaceId}
|
|
81
159
|
userId={userId}
|
|
160
|
+
conversationId={currentConversationId}
|
|
161
|
+
apiEndpoint={versions?.aiSdkMajor === 6 ? "/api/chat-v6" : "/api/chat"}
|
|
82
162
|
onOrchestrationStart={startOrchestration}
|
|
83
163
|
onLayerUpdate={updateLayer}
|
|
84
164
|
onReset={resetLayers}
|
|
165
|
+
onConversationUpdate={handleConversationUpdate}
|
|
85
166
|
/>
|
|
86
167
|
</div>
|
|
87
168
|
|
|
88
|
-
{/* Layer Flow Visualization */}
|
|
169
|
+
{/* Right: Layer Flow Visualization */}
|
|
89
170
|
<div className="w-[480px] flex flex-col bg-black/20">
|
|
90
171
|
<div className="p-4 border-b border-white/10">
|
|
91
172
|
<h2 className="font-semibold flex items-center gap-2">
|
|
@@ -110,11 +191,17 @@ export default function Home() {
|
|
|
110
191
|
|
|
111
192
|
{/* Footer */}
|
|
112
193
|
<footer className="border-t border-white/10 px-6 py-3">
|
|
113
|
-
<div className="
|
|
194
|
+
<div className="flex items-center justify-between text-sm text-gray-500">
|
|
114
195
|
<div className="flex items-center gap-4">
|
|
115
|
-
<span>Cortex SDK
|
|
196
|
+
<span>Cortex SDK {versions ? `v${versions.cortexSdk}` : "..."}</span>
|
|
116
197
|
<span>•</span>
|
|
117
|
-
<span>Vercel AI SDK
|
|
198
|
+
<span>Vercel AI SDK {versions?.aiSdk ?? "..."}</span>
|
|
199
|
+
{versions?.aiSdkMajor === 6 && (
|
|
200
|
+
<>
|
|
201
|
+
<span>•</span>
|
|
202
|
+
<span className="text-cortex-400">Using ToolLoopAgent</span>
|
|
203
|
+
</>
|
|
204
|
+
)}
|
|
118
205
|
</div>
|
|
119
206
|
<a
|
|
120
207
|
href="https://cortexmemory.dev/docs"
|
|
@@ -129,3 +216,15 @@ export default function Home() {
|
|
|
129
216
|
</main>
|
|
130
217
|
);
|
|
131
218
|
}
|
|
219
|
+
|
|
220
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
221
|
+
// Page Component (wraps with AuthProvider)
|
|
222
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
223
|
+
|
|
224
|
+
export default function Home() {
|
|
225
|
+
return (
|
|
226
|
+
<AuthProvider>
|
|
227
|
+
<MainContent />
|
|
228
|
+
</AuthProvider>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useAuth } from "./AuthProvider";
|
|
5
|
+
|
|
6
|
+
export function AdminSetup() {
|
|
7
|
+
const { setupAdmin, error, clearError } = useAuth();
|
|
8
|
+
const [password, setPassword] = useState("");
|
|
9
|
+
const [confirmPassword, setConfirmPassword] = useState("");
|
|
10
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
11
|
+
const [localError, setLocalError] = useState<string | null>(null);
|
|
12
|
+
|
|
13
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
14
|
+
e.preventDefault();
|
|
15
|
+
setLocalError(null);
|
|
16
|
+
clearError();
|
|
17
|
+
|
|
18
|
+
if (password.length < 4) {
|
|
19
|
+
setLocalError("Password must be at least 4 characters");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (password !== confirmPassword) {
|
|
24
|
+
setLocalError("Passwords do not match");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
setIsLoading(true);
|
|
29
|
+
await setupAdmin(password);
|
|
30
|
+
setIsLoading(false);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const displayError = localError || error;
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="min-h-screen flex items-center justify-center p-4">
|
|
37
|
+
<div className="w-full max-w-md">
|
|
38
|
+
{/* Logo and Title */}
|
|
39
|
+
<div className="text-center mb-8">
|
|
40
|
+
<div className="w-20 h-20 mx-auto mb-6 rounded-2xl bg-gradient-to-br from-cortex-500 to-cortex-700 flex items-center justify-center shadow-lg shadow-cortex-500/20">
|
|
41
|
+
<span className="text-4xl">🧠</span>
|
|
42
|
+
</div>
|
|
43
|
+
<h1 className="text-3xl font-bold mb-2">Welcome to Cortex</h1>
|
|
44
|
+
<p className="text-gray-400">Set up your admin password to get started</p>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
{/* Setup Form */}
|
|
48
|
+
<div className="glass rounded-2xl p-8">
|
|
49
|
+
<form onSubmit={handleSubmit} className="space-y-6">
|
|
50
|
+
<div>
|
|
51
|
+
<label
|
|
52
|
+
htmlFor="password"
|
|
53
|
+
className="block text-sm font-medium text-gray-300 mb-2"
|
|
54
|
+
>
|
|
55
|
+
Admin Password
|
|
56
|
+
</label>
|
|
57
|
+
<input
|
|
58
|
+
id="password"
|
|
59
|
+
type="password"
|
|
60
|
+
value={password}
|
|
61
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
62
|
+
placeholder="Enter a secure password"
|
|
63
|
+
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl focus:outline-none focus:border-cortex-500 transition-colors"
|
|
64
|
+
disabled={isLoading}
|
|
65
|
+
autoFocus
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div>
|
|
70
|
+
<label
|
|
71
|
+
htmlFor="confirmPassword"
|
|
72
|
+
className="block text-sm font-medium text-gray-300 mb-2"
|
|
73
|
+
>
|
|
74
|
+
Confirm Password
|
|
75
|
+
</label>
|
|
76
|
+
<input
|
|
77
|
+
id="confirmPassword"
|
|
78
|
+
type="password"
|
|
79
|
+
value={confirmPassword}
|
|
80
|
+
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
81
|
+
placeholder="Confirm your password"
|
|
82
|
+
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl focus:outline-none focus:border-cortex-500 transition-colors"
|
|
83
|
+
disabled={isLoading}
|
|
84
|
+
/>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
{displayError && (
|
|
88
|
+
<div className="p-3 bg-red-500/10 border border-red-500/20 rounded-xl text-red-400 text-sm">
|
|
89
|
+
{displayError}
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
|
|
93
|
+
<button
|
|
94
|
+
type="submit"
|
|
95
|
+
disabled={isLoading || !password || !confirmPassword}
|
|
96
|
+
className="w-full py-3 bg-cortex-600 hover:bg-cortex-700 disabled:opacity-50 disabled:cursor-not-allowed rounded-xl font-medium transition-colors flex items-center justify-center gap-2"
|
|
97
|
+
>
|
|
98
|
+
{isLoading ? (
|
|
99
|
+
<>
|
|
100
|
+
<svg
|
|
101
|
+
className="animate-spin h-5 w-5"
|
|
102
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
103
|
+
fill="none"
|
|
104
|
+
viewBox="0 0 24 24"
|
|
105
|
+
>
|
|
106
|
+
<circle
|
|
107
|
+
className="opacity-25"
|
|
108
|
+
cx="12"
|
|
109
|
+
cy="12"
|
|
110
|
+
r="10"
|
|
111
|
+
stroke="currentColor"
|
|
112
|
+
strokeWidth="4"
|
|
113
|
+
/>
|
|
114
|
+
<path
|
|
115
|
+
className="opacity-75"
|
|
116
|
+
fill="currentColor"
|
|
117
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
118
|
+
/>
|
|
119
|
+
</svg>
|
|
120
|
+
Setting up...
|
|
121
|
+
</>
|
|
122
|
+
) : (
|
|
123
|
+
"Set Admin Password"
|
|
124
|
+
)}
|
|
125
|
+
</button>
|
|
126
|
+
</form>
|
|
127
|
+
|
|
128
|
+
<div className="mt-6 pt-6 border-t border-white/10">
|
|
129
|
+
<p className="text-xs text-gray-500 text-center">
|
|
130
|
+
This password will be used to manage the quickstart demo.
|
|
131
|
+
<br />
|
|
132
|
+
You can create additional user accounts after setup.
|
|
133
|
+
</p>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
useContext,
|
|
6
|
+
useState,
|
|
7
|
+
useEffect,
|
|
8
|
+
useCallback,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
} from "react";
|
|
11
|
+
|
|
12
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
13
|
+
// Types
|
|
14
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
15
|
+
|
|
16
|
+
export interface User {
|
|
17
|
+
id: string;
|
|
18
|
+
displayName: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface AuthState {
|
|
22
|
+
isLoading: boolean;
|
|
23
|
+
isAdminSetup: boolean | null;
|
|
24
|
+
isAuthenticated: boolean;
|
|
25
|
+
user: User | null;
|
|
26
|
+
error: string | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface AuthContextValue extends AuthState {
|
|
30
|
+
setupAdmin: (password: string) => Promise<boolean>;
|
|
31
|
+
login: (username: string, password: string) => Promise<boolean>;
|
|
32
|
+
register: (
|
|
33
|
+
username: string,
|
|
34
|
+
password: string,
|
|
35
|
+
displayName?: string
|
|
36
|
+
) => Promise<boolean>;
|
|
37
|
+
logout: () => void;
|
|
38
|
+
clearError: () => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
42
|
+
// Context
|
|
43
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
44
|
+
|
|
45
|
+
const AuthContext = createContext<AuthContextValue | null>(null);
|
|
46
|
+
|
|
47
|
+
const STORAGE_KEY = "cortex-quickstart-auth";
|
|
48
|
+
|
|
49
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
50
|
+
// Provider
|
|
51
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
52
|
+
|
|
53
|
+
export function AuthProvider({ children }: { children: ReactNode }) {
|
|
54
|
+
const [state, setState] = useState<AuthState>({
|
|
55
|
+
isLoading: true,
|
|
56
|
+
isAdminSetup: null,
|
|
57
|
+
isAuthenticated: false,
|
|
58
|
+
user: null,
|
|
59
|
+
error: null,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Check admin setup status and restore session on mount
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
const init = async () => {
|
|
65
|
+
try {
|
|
66
|
+
// Check if admin is set up
|
|
67
|
+
const checkResponse = await fetch("/api/auth/check");
|
|
68
|
+
const checkData = await checkResponse.json();
|
|
69
|
+
|
|
70
|
+
// Try to restore session from localStorage
|
|
71
|
+
let user: User | null = null;
|
|
72
|
+
let sessionToken: string | null = null;
|
|
73
|
+
|
|
74
|
+
if (typeof window !== "undefined") {
|
|
75
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
76
|
+
if (stored) {
|
|
77
|
+
try {
|
|
78
|
+
const parsed = JSON.parse(stored);
|
|
79
|
+
user = parsed.user;
|
|
80
|
+
sessionToken = parsed.sessionToken;
|
|
81
|
+
} catch {
|
|
82
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
setState({
|
|
88
|
+
isLoading: false,
|
|
89
|
+
isAdminSetup: checkData.isSetup,
|
|
90
|
+
isAuthenticated: !!user && !!sessionToken,
|
|
91
|
+
user,
|
|
92
|
+
error: null,
|
|
93
|
+
});
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error("Auth init error:", error);
|
|
96
|
+
setState((prev) => ({
|
|
97
|
+
...prev,
|
|
98
|
+
isLoading: false,
|
|
99
|
+
error: "Failed to initialize authentication",
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
init();
|
|
105
|
+
}, []);
|
|
106
|
+
|
|
107
|
+
// Setup admin password
|
|
108
|
+
const setupAdmin = useCallback(async (password: string): Promise<boolean> => {
|
|
109
|
+
setState((prev) => ({ ...prev, error: null }));
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const response = await fetch("/api/auth/setup", {
|
|
113
|
+
method: "POST",
|
|
114
|
+
headers: { "Content-Type": "application/json" },
|
|
115
|
+
body: JSON.stringify({ password }),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const data = await response.json();
|
|
119
|
+
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
setState((prev) => ({ ...prev, error: data.error }));
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
setState((prev) => ({
|
|
126
|
+
...prev,
|
|
127
|
+
isAdminSetup: true,
|
|
128
|
+
}));
|
|
129
|
+
|
|
130
|
+
return true;
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.error("Setup error:", error);
|
|
133
|
+
setState((prev) => ({
|
|
134
|
+
...prev,
|
|
135
|
+
error: "Failed to setup admin password",
|
|
136
|
+
}));
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}, []);
|
|
140
|
+
|
|
141
|
+
// Login
|
|
142
|
+
const login = useCallback(
|
|
143
|
+
async (username: string, password: string): Promise<boolean> => {
|
|
144
|
+
setState((prev) => ({ ...prev, error: null }));
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const response = await fetch("/api/auth/login", {
|
|
148
|
+
method: "POST",
|
|
149
|
+
headers: { "Content-Type": "application/json" },
|
|
150
|
+
body: JSON.stringify({ username, password }),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const data = await response.json();
|
|
154
|
+
|
|
155
|
+
if (!response.ok) {
|
|
156
|
+
setState((prev) => ({ ...prev, error: data.error }));
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Store session
|
|
161
|
+
if (typeof window !== "undefined") {
|
|
162
|
+
localStorage.setItem(
|
|
163
|
+
STORAGE_KEY,
|
|
164
|
+
JSON.stringify({
|
|
165
|
+
user: data.user,
|
|
166
|
+
sessionToken: data.sessionToken,
|
|
167
|
+
})
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
setState((prev) => ({
|
|
172
|
+
...prev,
|
|
173
|
+
isAuthenticated: true,
|
|
174
|
+
user: data.user,
|
|
175
|
+
}));
|
|
176
|
+
|
|
177
|
+
return true;
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error("Login error:", error);
|
|
180
|
+
setState((prev) => ({
|
|
181
|
+
...prev,
|
|
182
|
+
error: "Failed to login",
|
|
183
|
+
}));
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
[]
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
// Register
|
|
191
|
+
const register = useCallback(
|
|
192
|
+
async (
|
|
193
|
+
username: string,
|
|
194
|
+
password: string,
|
|
195
|
+
displayName?: string
|
|
196
|
+
): Promise<boolean> => {
|
|
197
|
+
setState((prev) => ({ ...prev, error: null }));
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const response = await fetch("/api/auth/register", {
|
|
201
|
+
method: "POST",
|
|
202
|
+
headers: { "Content-Type": "application/json" },
|
|
203
|
+
body: JSON.stringify({ username, password, displayName }),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const data = await response.json();
|
|
207
|
+
|
|
208
|
+
if (!response.ok) {
|
|
209
|
+
setState((prev) => ({ ...prev, error: data.error }));
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Store session
|
|
214
|
+
if (typeof window !== "undefined") {
|
|
215
|
+
localStorage.setItem(
|
|
216
|
+
STORAGE_KEY,
|
|
217
|
+
JSON.stringify({
|
|
218
|
+
user: data.user,
|
|
219
|
+
sessionToken: data.sessionToken,
|
|
220
|
+
})
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
setState((prev) => ({
|
|
225
|
+
...prev,
|
|
226
|
+
isAuthenticated: true,
|
|
227
|
+
user: data.user,
|
|
228
|
+
}));
|
|
229
|
+
|
|
230
|
+
return true;
|
|
231
|
+
} catch (error) {
|
|
232
|
+
console.error("Register error:", error);
|
|
233
|
+
setState((prev) => ({
|
|
234
|
+
...prev,
|
|
235
|
+
error: "Failed to register",
|
|
236
|
+
}));
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
[]
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
// Logout
|
|
244
|
+
const logout = useCallback(() => {
|
|
245
|
+
if (typeof window !== "undefined") {
|
|
246
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
setState((prev) => ({
|
|
250
|
+
...prev,
|
|
251
|
+
isAuthenticated: false,
|
|
252
|
+
user: null,
|
|
253
|
+
}));
|
|
254
|
+
}, []);
|
|
255
|
+
|
|
256
|
+
// Clear error
|
|
257
|
+
const clearError = useCallback(() => {
|
|
258
|
+
setState((prev) => ({ ...prev, error: null }));
|
|
259
|
+
}, []);
|
|
260
|
+
|
|
261
|
+
const value: AuthContextValue = {
|
|
262
|
+
...state,
|
|
263
|
+
setupAdmin,
|
|
264
|
+
login,
|
|
265
|
+
register,
|
|
266
|
+
logout,
|
|
267
|
+
clearError,
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
274
|
+
// Hook
|
|
275
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
276
|
+
|
|
277
|
+
export function useAuth(): AuthContextValue {
|
|
278
|
+
const context = useContext(AuthContext);
|
|
279
|
+
if (!context) {
|
|
280
|
+
throw new Error("useAuth must be used within an AuthProvider");
|
|
281
|
+
}
|
|
282
|
+
return context;
|
|
283
|
+
}
|