@nordsym/apiclaw 1.3.2 → 1.3.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.
@@ -0,0 +1,292 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { useSearchParams } from "next/navigation";
5
+ import { Suspense } from "react";
6
+
7
+ const CONVEX_URL = process.env.NEXT_PUBLIC_CONVEX_URL || "https://brilliant-puffin-712.eu-west-1.convex.cloud";
8
+
9
+ interface VerifyResult {
10
+ success: boolean;
11
+ error?: string;
12
+ workspace?: {
13
+ id: string;
14
+ email: string;
15
+ status: string;
16
+ tier: string;
17
+ usageCount: number;
18
+ usageLimit: number;
19
+ };
20
+ sessionToken?: string;
21
+ }
22
+
23
+ function VerifyContent() {
24
+ const searchParams = useSearchParams();
25
+ const token = searchParams.get("token");
26
+
27
+ const [status, setStatus] = useState<"loading" | "success" | "error">("loading");
28
+ const [error, setError] = useState<string>("");
29
+ const [workspace, setWorkspace] = useState<VerifyResult["workspace"] | null>(null);
30
+ const [sessionToken, setSessionToken] = useState<string>("");
31
+
32
+ // Password form state
33
+ const [showPasswordForm, setShowPasswordForm] = useState(false);
34
+ const [password, setPassword] = useState("");
35
+ const [confirmPassword, setConfirmPassword] = useState("");
36
+ const [passwordError, setPasswordError] = useState("");
37
+ const [passwordSaved, setPasswordSaved] = useState(false);
38
+
39
+ useEffect(() => {
40
+ if (!token) {
41
+ setStatus("error");
42
+ setError("No verification token provided");
43
+ return;
44
+ }
45
+
46
+ verifyToken(token);
47
+ }, [token]);
48
+
49
+ async function verifyToken(token: string) {
50
+ try {
51
+ const response = await fetch(`${CONVEX_URL}/api/mutation`, {
52
+ method: "POST",
53
+ headers: { "Content-Type": "application/json" },
54
+ body: JSON.stringify({
55
+ path: "workspaces:verifyMagicLink",
56
+ args: { token },
57
+ }),
58
+ });
59
+
60
+ if (!response.ok) {
61
+ throw new Error("Verification failed");
62
+ }
63
+
64
+ const result = await response.json();
65
+ const data: VerifyResult = result.value || result;
66
+
67
+ if (data.success) {
68
+ setStatus("success");
69
+ setWorkspace(data.workspace || null);
70
+ setSessionToken(data.sessionToken || "");
71
+ } else {
72
+ setStatus("error");
73
+ switch (data.error) {
74
+ case "invalid_token":
75
+ setError("This verification link is invalid.");
76
+ break;
77
+ case "already_used":
78
+ setError("This verification link has already been used.");
79
+ break;
80
+ case "expired":
81
+ setError("This verification link has expired. Please request a new one.");
82
+ break;
83
+ default:
84
+ setError("Verification failed. Please try again.");
85
+ }
86
+ }
87
+ } catch (err) {
88
+ setStatus("error");
89
+ setError("Something went wrong. Please try again.");
90
+ console.error("Verification error:", err);
91
+ }
92
+ }
93
+
94
+ async function handleSetPassword(e: React.FormEvent) {
95
+ e.preventDefault();
96
+ setPasswordError("");
97
+
98
+ if (password.length < 8) {
99
+ setPasswordError("Password must be at least 8 characters");
100
+ return;
101
+ }
102
+
103
+ if (password !== confirmPassword) {
104
+ setPasswordError("Passwords don't match");
105
+ return;
106
+ }
107
+
108
+ try {
109
+ const response = await fetch(`${CONVEX_URL}/api/mutation`, {
110
+ method: "POST",
111
+ headers: { "Content-Type": "application/json" },
112
+ body: JSON.stringify({
113
+ path: "workspaces:setPassword",
114
+ args: { sessionToken, password },
115
+ }),
116
+ });
117
+
118
+ const result = await response.json();
119
+ const data = result.value || result;
120
+
121
+ if (data.success) {
122
+ setPasswordSaved(true);
123
+ setShowPasswordForm(false);
124
+ } else {
125
+ setPasswordError("Failed to set password. Please try again.");
126
+ }
127
+ } catch (err) {
128
+ setPasswordError("Something went wrong. Please try again.");
129
+ }
130
+ }
131
+
132
+ return (
133
+ <div className="min-h-screen bg-[var(--background)] flex items-center justify-center p-4">
134
+ <div className="w-full max-w-md">
135
+ {/* Logo */}
136
+ <div className="text-center mb-8">
137
+ <span className="text-6xl">🦞</span>
138
+ <h1 className="text-2xl font-bold mt-4">APIClaw</h1>
139
+ </div>
140
+
141
+ {/* Loading state */}
142
+ {status === "loading" && (
143
+ <div className="bg-[var(--surface)] rounded-xl p-8 text-center border border-[var(--border)]">
144
+ <div className="animate-spin w-8 h-8 border-4 border-[var(--accent)] border-t-transparent rounded-full mx-auto mb-4" />
145
+ <p className="text-[var(--text-secondary)]">Verifying your email...</p>
146
+ </div>
147
+ )}
148
+
149
+ {/* Success state */}
150
+ {status === "success" && (
151
+ <div className="bg-[var(--surface)] rounded-xl p-8 border border-[var(--border)]">
152
+ <div className="text-center mb-6">
153
+ <div className="w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center mx-auto mb-4">
154
+ <svg className="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
155
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
156
+ </svg>
157
+ </div>
158
+ <h2 className="text-xl font-bold mb-2">Email Verified!</h2>
159
+ <p className="text-[var(--text-secondary)]">
160
+ Your workspace is now active. Your AI agent can start using APIClaw.
161
+ </p>
162
+ </div>
163
+
164
+ {workspace && (
165
+ <div className="bg-[var(--background)] rounded-lg p-4 mb-6">
166
+ <div className="space-y-2 text-sm">
167
+ <div className="flex justify-between">
168
+ <span className="text-[var(--text-muted)]">Email</span>
169
+ <span className="font-medium">{workspace.email}</span>
170
+ </div>
171
+ <div className="flex justify-between">
172
+ <span className="text-[var(--text-muted)]">Plan</span>
173
+ <span className="font-medium capitalize">{workspace.tier}</span>
174
+ </div>
175
+ <div className="flex justify-between">
176
+ <span className="text-[var(--text-muted)]">API Calls</span>
177
+ <span className="font-medium">{workspace.usageCount} / {workspace.usageLimit}</span>
178
+ </div>
179
+ </div>
180
+ </div>
181
+ )}
182
+
183
+ {/* Password form */}
184
+ {!passwordSaved && !showPasswordForm && (
185
+ <button
186
+ onClick={() => setShowPasswordForm(true)}
187
+ className="w-full py-3 px-4 rounded-lg border border-[var(--border)] text-[var(--text-secondary)] hover:bg-[var(--background)] transition-colors text-sm mb-4"
188
+ >
189
+ Set a password (optional)
190
+ </button>
191
+ )}
192
+
193
+ {showPasswordForm && (
194
+ <form onSubmit={handleSetPassword} className="mb-4">
195
+ <div className="space-y-3">
196
+ <input
197
+ type="password"
198
+ placeholder="Password (min. 8 characters)"
199
+ value={password}
200
+ onChange={(e) => setPassword(e.target.value)}
201
+ className="w-full px-4 py-3 rounded-lg bg-[var(--background)] border border-[var(--border)] focus:border-[var(--accent)] focus:ring-1 focus:ring-[var(--accent)] outline-none transition-colors"
202
+ />
203
+ <input
204
+ type="password"
205
+ placeholder="Confirm password"
206
+ value={confirmPassword}
207
+ onChange={(e) => setConfirmPassword(e.target.value)}
208
+ className="w-full px-4 py-3 rounded-lg bg-[var(--background)] border border-[var(--border)] focus:border-[var(--accent)] focus:ring-1 focus:ring-[var(--accent)] outline-none transition-colors"
209
+ />
210
+ </div>
211
+ {passwordError && (
212
+ <p className="text-red-500 text-sm mt-2">{passwordError}</p>
213
+ )}
214
+ <div className="flex gap-2 mt-3">
215
+ <button
216
+ type="button"
217
+ onClick={() => setShowPasswordForm(false)}
218
+ className="flex-1 py-2 px-4 rounded-lg border border-[var(--border)] text-[var(--text-secondary)] hover:bg-[var(--background)] transition-colors text-sm"
219
+ >
220
+ Cancel
221
+ </button>
222
+ <button
223
+ type="submit"
224
+ className="flex-1 py-2 px-4 rounded-lg bg-[var(--accent)] text-white font-medium hover:opacity-90 transition-opacity text-sm"
225
+ >
226
+ Save Password
227
+ </button>
228
+ </div>
229
+ </form>
230
+ )}
231
+
232
+ {passwordSaved && (
233
+ <div className="bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-400 rounded-lg p-3 text-sm text-center mb-4">
234
+ ✓ Password saved
235
+ </div>
236
+ )}
237
+
238
+ <div className="text-center pt-4 border-t border-[var(--border)]">
239
+ <p className="text-[var(--text-muted)] text-sm">
240
+ ✓ You can close this tab now
241
+ </p>
242
+ </div>
243
+ </div>
244
+ )}
245
+
246
+ {/* Error state */}
247
+ {status === "error" && (
248
+ <div className="bg-[var(--surface)] rounded-xl p-8 border border-[var(--border)]">
249
+ <div className="text-center">
250
+ <div className="w-16 h-16 bg-red-100 dark:bg-red-900/30 rounded-full flex items-center justify-center mx-auto mb-4">
251
+ <svg className="w-8 h-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
252
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
253
+ </svg>
254
+ </div>
255
+ <h2 className="text-xl font-bold mb-2">Verification Failed</h2>
256
+ <p className="text-[var(--text-secondary)] mb-6">{error}</p>
257
+ <a
258
+ href="/"
259
+ className="inline-block py-3 px-6 rounded-lg bg-[var(--accent)] text-white font-medium hover:opacity-90 transition-opacity"
260
+ >
261
+ Go to Homepage
262
+ </a>
263
+ </div>
264
+ </div>
265
+ )}
266
+
267
+ {/* Footer */}
268
+ <p className="text-center text-[var(--text-muted)] text-xs mt-6">
269
+ Having trouble? Contact{" "}
270
+ <a href="mailto:support@apiclaw.com" className="text-[var(--accent)] hover:underline">
271
+ support@apiclaw.com
272
+ </a>
273
+ </p>
274
+ </div>
275
+ </div>
276
+ );
277
+ }
278
+
279
+ export default function VerifyPage() {
280
+ return (
281
+ <Suspense fallback={
282
+ <div className="min-h-screen bg-[var(--background)] flex items-center justify-center">
283
+ <div className="text-center">
284
+ <span className="text-6xl">🦞</span>
285
+ <p className="mt-4 text-[var(--text-secondary)]">Loading...</p>
286
+ </div>
287
+ </div>
288
+ }>
289
+ <VerifyContent />
290
+ </Suspense>
291
+ );
292
+ }
@@ -0,0 +1,22 @@
1
+ import { Suspense } from "react";
2
+
3
+ export const metadata = {
4
+ title: "Dashboard | APIClaw",
5
+ description: "Manage your AI agents and view API usage analytics",
6
+ };
7
+
8
+ export default function DashboardLayout({
9
+ children,
10
+ }: {
11
+ children: React.ReactNode;
12
+ }) {
13
+ return (
14
+ <Suspense fallback={
15
+ <div className="min-h-screen flex items-center justify-center bg-[var(--background)]">
16
+ <div className="w-8 h-8 border-4 border-accent border-t-transparent rounded-full animate-spin" />
17
+ </div>
18
+ }>
19
+ {children}
20
+ </Suspense>
21
+ );
22
+ }