flowlink-auth 2.6.9
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/AuthClient.js +2822 -0
- package/dist/ErrorBox.js +1042 -0
- package/dist/Protected.js +2628 -0
- package/dist/SignIn.js +1192 -0
- package/dist/SignUp.js +1246 -0
- package/dist/api.js +55 -0
- package/dist/createAuthMiddleware.js +5949 -0
- package/dist/index.js +574 -0
- package/dist/init.js +73 -0
- package/dist/provider.js +1234 -0
- package/dist/securityUtils.js +102 -0
- package/dist/useAuth.js +1057 -0
- package/package.json +31 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
// src/provider.js
|
|
2
|
+
import React, { createContext, useContext, useEffect, useState, useCallback, useRef } from "react";
|
|
3
|
+
|
|
4
|
+
// src/securityUtils.js
|
|
5
|
+
function isSecureContext() {
|
|
6
|
+
if (typeof window === "undefined") return true;
|
|
7
|
+
const isLocalhost = window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1" || window.location.hostname === "[::1]";
|
|
8
|
+
const isHttps = window.location.protocol === "https:";
|
|
9
|
+
return isHttps || isLocalhost;
|
|
10
|
+
}
|
|
11
|
+
function checkSecureContext() {
|
|
12
|
+
if (!isSecureContext()) {
|
|
13
|
+
console.warn(
|
|
14
|
+
"flowlink-auth: HTTPS is required for production. Your connection is not secure. Authentication may fail."
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function getSafeErrorMessage(error) {
|
|
19
|
+
if (typeof error === "string") {
|
|
20
|
+
if (error.includes("password") || error.includes("token") || error.includes("secret")) {
|
|
21
|
+
return "An error occurred. Please try again.";
|
|
22
|
+
}
|
|
23
|
+
return error;
|
|
24
|
+
}
|
|
25
|
+
if (error?.message) {
|
|
26
|
+
return getSafeErrorMessage(error.message);
|
|
27
|
+
}
|
|
28
|
+
return "An error occurred. Please try again.";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/provider.js
|
|
32
|
+
var AuthContext = createContext(null);
|
|
33
|
+
var flowlinkAuthProvider = ({ children, publishableKey, baseUrl, redirect }) => {
|
|
34
|
+
const [ready, setReady] = useState(false);
|
|
35
|
+
const [error, setError] = useState(null);
|
|
36
|
+
const [user, setUser] = useState(null);
|
|
37
|
+
const [loadingUser, setLoadingUser] = useState(true);
|
|
38
|
+
const [sessionTimeout, setSessionTimeout] = useState(null);
|
|
39
|
+
const redirectedRef = useRef(false);
|
|
40
|
+
const sessionTimerRef = useRef(null);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
checkSecureContext();
|
|
43
|
+
}, []);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!publishableKey || !publishableKey.trim()) {
|
|
46
|
+
setError("Missing publishable key");
|
|
47
|
+
setReady(false);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (!baseUrl || !baseUrl.trim()) {
|
|
51
|
+
setError("Missing baseUrl");
|
|
52
|
+
setReady(false);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (!publishableKey.startsWith("pk_")) {
|
|
56
|
+
console.warn('flowlink-auth: publishableKey should start with "pk_"');
|
|
57
|
+
}
|
|
58
|
+
setError(null);
|
|
59
|
+
setReady(true);
|
|
60
|
+
}, [publishableKey, baseUrl]);
|
|
61
|
+
const normalizedBase = useCallback(() => {
|
|
62
|
+
return baseUrl?.replace(/\/+$/, "") || "";
|
|
63
|
+
}, [baseUrl]);
|
|
64
|
+
const redirectTo = (url, { replace = true } = {}) => {
|
|
65
|
+
if (!url) return;
|
|
66
|
+
if (typeof window === "undefined") return;
|
|
67
|
+
if (url.startsWith("//") || url.startsWith("http")) {
|
|
68
|
+
console.error("flowlink-auth: Redirect URL must be relative");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
if (replace) window.location.replace(url);
|
|
73
|
+
else window.location.assign(url);
|
|
74
|
+
} catch (_) {
|
|
75
|
+
window.location.href = url;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const resetSessionTimeout = useCallback(() => {
|
|
79
|
+
if (sessionTimerRef.current) {
|
|
80
|
+
clearTimeout(sessionTimerRef.current);
|
|
81
|
+
}
|
|
82
|
+
sessionTimerRef.current = setTimeout(() => {
|
|
83
|
+
setSessionTimeout(true);
|
|
84
|
+
logout();
|
|
85
|
+
}, 24 * 60 * 60 * 1e3);
|
|
86
|
+
}, []);
|
|
87
|
+
const fetchMe = useCallback(async () => {
|
|
88
|
+
setLoadingUser(true);
|
|
89
|
+
try {
|
|
90
|
+
const base = normalizedBase();
|
|
91
|
+
if (!base) {
|
|
92
|
+
setUser(null);
|
|
93
|
+
setLoadingUser(false);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const fullUrl = new URL(`${base}/api/sdk/me`).href;
|
|
97
|
+
const res = await fetch(fullUrl, {
|
|
98
|
+
method: "GET",
|
|
99
|
+
credentials: "include",
|
|
100
|
+
headers: {
|
|
101
|
+
"Content-Type": "application/json"
|
|
102
|
+
},
|
|
103
|
+
referrerPolicy: "strict-origin-when-cross-origin"
|
|
104
|
+
});
|
|
105
|
+
if (!res.ok) {
|
|
106
|
+
setUser(null);
|
|
107
|
+
setLoadingUser(false);
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const data = await res.json();
|
|
111
|
+
const u = data?.user ?? null;
|
|
112
|
+
setUser(u);
|
|
113
|
+
setLoadingUser(false);
|
|
114
|
+
resetSessionTimeout();
|
|
115
|
+
return u;
|
|
116
|
+
} catch (err) {
|
|
117
|
+
console.error("Failed to fetch user:", getSafeErrorMessage(err));
|
|
118
|
+
setUser(null);
|
|
119
|
+
setLoadingUser(false);
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}, [normalizedBase, resetSessionTimeout]);
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
if (!ready) return;
|
|
125
|
+
fetchMe();
|
|
126
|
+
const onStorage = (e) => {
|
|
127
|
+
if (!e.key) return;
|
|
128
|
+
if (e.key === "flowlink_login" || e.key === "flowlink_logout") {
|
|
129
|
+
fetchMe();
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
if (typeof window !== "undefined") {
|
|
133
|
+
window.addEventListener("storage", onStorage);
|
|
134
|
+
}
|
|
135
|
+
return () => {
|
|
136
|
+
if (typeof window !== "undefined") {
|
|
137
|
+
window.removeEventListener("storage", onStorage);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}, [ready, fetchMe]);
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
if (!ready) return;
|
|
143
|
+
if (loadingUser) return;
|
|
144
|
+
if (!user) {
|
|
145
|
+
redirectedRef.current = false;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (user && redirect && !redirectedRef.current) {
|
|
149
|
+
redirectedRef.current = true;
|
|
150
|
+
redirectTo(redirect, { replace: true });
|
|
151
|
+
}
|
|
152
|
+
}, [ready, loadingUser, user, redirect]);
|
|
153
|
+
const completeLogin = useCallback(async (opts = {}) => {
|
|
154
|
+
const { redirectTo: redirectUrl, replace = true } = opts;
|
|
155
|
+
const u = await fetchMe();
|
|
156
|
+
try {
|
|
157
|
+
localStorage.setItem("flowlink_login", String(Date.now()));
|
|
158
|
+
} catch (_) {
|
|
159
|
+
}
|
|
160
|
+
const dest = redirectUrl ?? redirect;
|
|
161
|
+
if (u && dest) {
|
|
162
|
+
redirectTo(dest, { replace });
|
|
163
|
+
}
|
|
164
|
+
return u;
|
|
165
|
+
}, [fetchMe, redirect]);
|
|
166
|
+
const logout = useCallback(async (opts = {}) => {
|
|
167
|
+
const { callServer = true, redirectTo: redirectUrl, replace = true } = opts;
|
|
168
|
+
const base = normalizedBase();
|
|
169
|
+
if (callServer && base) {
|
|
170
|
+
try {
|
|
171
|
+
await fetch(`${base}/api/sdk/logout`, {
|
|
172
|
+
method: "POST",
|
|
173
|
+
credentials: "include",
|
|
174
|
+
headers: { "Content-Type": "application/json" }
|
|
175
|
+
});
|
|
176
|
+
} catch (_) {
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
setUser(null);
|
|
180
|
+
try {
|
|
181
|
+
localStorage.setItem("flowlink_logout", String(Date.now()));
|
|
182
|
+
} catch (_) {
|
|
183
|
+
}
|
|
184
|
+
const dest = redirectUrl ?? redirect;
|
|
185
|
+
if (dest) redirectTo(dest, { replace });
|
|
186
|
+
}, [normalizedBase, redirect]);
|
|
187
|
+
const value = {
|
|
188
|
+
publishableKey,
|
|
189
|
+
baseUrl,
|
|
190
|
+
redirectTo: (url, opts) => redirectTo(url, opts),
|
|
191
|
+
user,
|
|
192
|
+
setUser,
|
|
193
|
+
loadingUser,
|
|
194
|
+
fetchMe,
|
|
195
|
+
logout,
|
|
196
|
+
completeLogin
|
|
197
|
+
};
|
|
198
|
+
return /* @__PURE__ */ React.createElement(AuthContext.Provider, { value }, error ? /* @__PURE__ */ React.createElement("div", { style: { padding: "20px", background: "#220000", color: "white" } }, /* @__PURE__ */ React.createElement("h2", null, "flowlink Auth Error"), /* @__PURE__ */ React.createElement("p", null, error)) : !ready ? null : children);
|
|
199
|
+
};
|
|
200
|
+
var useAuth = () => {
|
|
201
|
+
const ctx = useContext(AuthContext);
|
|
202
|
+
if (!ctx) throw new Error("useAuth must be used within flowlinkAuthProvider");
|
|
203
|
+
return ctx;
|
|
204
|
+
};
|
|
205
|
+
var provider_default = flowlinkAuthProvider;
|
|
206
|
+
|
|
207
|
+
// src/SignIn.jsx
|
|
208
|
+
import React2, { useState as useState2 } from "react";
|
|
209
|
+
function SignIn({ onSuccess } = {}) {
|
|
210
|
+
const {
|
|
211
|
+
publishableKey,
|
|
212
|
+
baseUrl,
|
|
213
|
+
redirect,
|
|
214
|
+
redirectTo,
|
|
215
|
+
user,
|
|
216
|
+
loadingUser,
|
|
217
|
+
completeLogin,
|
|
218
|
+
fetchMe,
|
|
219
|
+
setUser
|
|
220
|
+
} = useAuth();
|
|
221
|
+
const [email, setEmail] = useState2("");
|
|
222
|
+
const [password, setPassword] = useState2("");
|
|
223
|
+
const [loading, setLoading] = useState2(false);
|
|
224
|
+
const [error, setError] = useState2(null);
|
|
225
|
+
const [message, setMessage] = useState2(null);
|
|
226
|
+
if (loadingUser) return null;
|
|
227
|
+
if (user && redirect) {
|
|
228
|
+
if (typeof redirectTo === "function") redirectTo(redirect);
|
|
229
|
+
else if (typeof window !== "undefined") window.location.assign(redirect);
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
async function submit(e) {
|
|
233
|
+
e.preventDefault();
|
|
234
|
+
setError(null);
|
|
235
|
+
setMessage(null);
|
|
236
|
+
if (!email || !password) {
|
|
237
|
+
setError("Email and password are required");
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
setLoading(true);
|
|
241
|
+
try {
|
|
242
|
+
const endpoint = `${(baseUrl || "").replace(/\/+$/, "")}/api/sdk/login`;
|
|
243
|
+
const res = await fetch(endpoint, {
|
|
244
|
+
method: "POST",
|
|
245
|
+
credentials: "include",
|
|
246
|
+
headers: {
|
|
247
|
+
"Content-Type": "application/json",
|
|
248
|
+
"x-publishable-key": publishableKey || ""
|
|
249
|
+
},
|
|
250
|
+
body: JSON.stringify({ email, password })
|
|
251
|
+
});
|
|
252
|
+
const ct = res.headers.get("content-type") || "";
|
|
253
|
+
let data = {};
|
|
254
|
+
if (ct.includes("application/json")) data = await res.json();
|
|
255
|
+
else {
|
|
256
|
+
const text = await res.text();
|
|
257
|
+
throw new Error(`Unexpected response (status ${res.status}): ${text.slice(0, 200)}`);
|
|
258
|
+
}
|
|
259
|
+
if (!res.ok) throw new Error(data.error || data.message || `Login failed (status ${res.status})`);
|
|
260
|
+
const serverUser = data.user ?? null;
|
|
261
|
+
if (serverUser && typeof setUser === "function") {
|
|
262
|
+
try {
|
|
263
|
+
setUser(serverUser);
|
|
264
|
+
} catch (_) {
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (typeof completeLogin === "function") {
|
|
268
|
+
try {
|
|
269
|
+
await completeLogin();
|
|
270
|
+
} catch (e2) {
|
|
271
|
+
if (typeof fetchMe === "function") await fetchMe();
|
|
272
|
+
}
|
|
273
|
+
} else if (typeof fetchMe === "function") {
|
|
274
|
+
await fetchMe();
|
|
275
|
+
}
|
|
276
|
+
if (onSuccess) {
|
|
277
|
+
try {
|
|
278
|
+
onSuccess(data);
|
|
279
|
+
} catch (_) {
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
setMessage("Signed in. Redirecting...");
|
|
283
|
+
if (redirect) {
|
|
284
|
+
setTimeout(() => {
|
|
285
|
+
if (typeof redirectTo === "function") redirectTo(redirect);
|
|
286
|
+
else if (typeof window !== "undefined") window.location.assign(redirect);
|
|
287
|
+
}, 250);
|
|
288
|
+
}
|
|
289
|
+
} catch (err) {
|
|
290
|
+
setError(err.message || "Network error");
|
|
291
|
+
} finally {
|
|
292
|
+
setLoading(false);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
async function startOAuthFlow(provider) {
|
|
296
|
+
setError(null);
|
|
297
|
+
setLoading(true);
|
|
298
|
+
try {
|
|
299
|
+
const rid = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
300
|
+
const callbackUrl = encodeURIComponent(`${window.location.origin}/signin`);
|
|
301
|
+
const sdkBase = typeof process !== "undefined" && process.env && process.env.NEXT_PUBLIC_SDK_BASE_URL || baseUrl || "http://localhost:3001";
|
|
302
|
+
const startUrl = `${sdkBase.replace(/\/+$/, "")}/sdk/auth/start?rid=${rid}&source=${encodeURIComponent(provider)}&callbackUrl=${callbackUrl}`;
|
|
303
|
+
if (!publishableKey) {
|
|
304
|
+
throw new Error("Missing publishable key (client side). Set NEXT_PUBLIC_SDK_PUBLISHABLE_KEY or provide publishableKey in provider.");
|
|
305
|
+
}
|
|
306
|
+
const res = await fetch(startUrl, {
|
|
307
|
+
method: "GET",
|
|
308
|
+
headers: { "x-publishable-key": publishableKey },
|
|
309
|
+
mode: "cors"
|
|
310
|
+
});
|
|
311
|
+
const data = await res.json().catch(() => null);
|
|
312
|
+
if (!res.ok) throw new Error(data?.error || `OAuth start failed (${res.status})`);
|
|
313
|
+
if (!data?.oauthUrl) throw new Error("SDK start did not return oauthUrl");
|
|
314
|
+
window.location.href = data.oauthUrl;
|
|
315
|
+
} catch (err) {
|
|
316
|
+
console.error("OAuth start error:", err);
|
|
317
|
+
setError(err?.message || "OAuth start failed");
|
|
318
|
+
setLoading(false);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const handleGoogle = (e) => {
|
|
322
|
+
if (e && typeof e.preventDefault === "function") e.preventDefault();
|
|
323
|
+
startOAuthFlow("google");
|
|
324
|
+
};
|
|
325
|
+
const handleGithub = (e) => {
|
|
326
|
+
if (e && typeof e.preventDefault === "function") e.preventDefault();
|
|
327
|
+
startOAuthFlow("github");
|
|
328
|
+
};
|
|
329
|
+
return /* @__PURE__ */ React2.createElement("div", { style: overlay }, /* @__PURE__ */ React2.createElement("div", { style: modal }, /* @__PURE__ */ React2.createElement("h2", { style: title }, "Sign in"), /* @__PURE__ */ React2.createElement("p", { style: subtitle }, "Welcome back \u2014 enter your credentials."), /* @__PURE__ */ React2.createElement("form", { onSubmit: submit, style: { width: "100%" } }, /* @__PURE__ */ React2.createElement("label", { style: label }, "Email"), /* @__PURE__ */ React2.createElement(
|
|
330
|
+
"input",
|
|
331
|
+
{
|
|
332
|
+
style: input,
|
|
333
|
+
value: email,
|
|
334
|
+
onChange: (e) => setEmail(e.target.value),
|
|
335
|
+
type: "email",
|
|
336
|
+
required: true
|
|
337
|
+
}
|
|
338
|
+
), /* @__PURE__ */ React2.createElement("label", { style: label }, "Password"), /* @__PURE__ */ React2.createElement(
|
|
339
|
+
"input",
|
|
340
|
+
{
|
|
341
|
+
style: input,
|
|
342
|
+
type: "password",
|
|
343
|
+
value: password,
|
|
344
|
+
onChange: (e) => setPassword(e.target.value),
|
|
345
|
+
required: true
|
|
346
|
+
}
|
|
347
|
+
), /* @__PURE__ */ React2.createElement("div", { style: { marginTop: 12 } }, /* @__PURE__ */ React2.createElement("button", { style: button, type: "submit", disabled: loading }, loading ? "Signing in\u2026" : "Sign in")), /* @__PURE__ */ React2.createElement("div", { style: { display: "flex", gap: 8, marginTop: 16 } }, /* @__PURE__ */ React2.createElement("button", { type: "button", onClick: handleGoogle, style: oauthButtonGoogle, disabled: loading }, "Continue with Google"), /* @__PURE__ */ React2.createElement("button", { type: "button", onClick: handleGithub, style: oauthButtonGithub, disabled: loading }, "Continue with GitHub")), error && /* @__PURE__ */ React2.createElement("div", { style: errorBox }, error), message && /* @__PURE__ */ React2.createElement("div", { style: successBox }, message))));
|
|
348
|
+
}
|
|
349
|
+
var overlay = { position: "fixed", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", background: "rgba(0,0,0,0.45)", zIndex: 9999, padding: 20 };
|
|
350
|
+
var modal = { width: "100%", maxWidth: 420, background: "#0f1724", color: "#fff", borderRadius: 12, padding: 22, boxShadow: "0 10px 30px rgba(2,6,23,0.6)", border: "1px solid rgba(255,255,255,0.04)" };
|
|
351
|
+
var title = { margin: 0, fontSize: 20, fontWeight: 600 };
|
|
352
|
+
var subtitle = { marginTop: 6, marginBottom: 14, color: "#cbd5e1", fontSize: 13 };
|
|
353
|
+
var label = { display: "block", color: "#cbd5e1", fontSize: 13, marginTop: 8 };
|
|
354
|
+
var input = { width: "100%", padding: "10px 12px", marginTop: 6, borderRadius: 8, border: "1px solid rgba(255,255,255,0.06)", background: "#0b1220", color: "#fff", boxSizing: "border-box" };
|
|
355
|
+
var button = { width: "100%", padding: "10px 12px", borderRadius: 8, background: "linear-gradient(90deg,#06b6d4,#2563eb)", color: "#0b1220", border: "none", fontWeight: 700, cursor: "pointer" };
|
|
356
|
+
var oauthButtonGoogle = { flex: 1, padding: "10px 12px", borderRadius: 8, background: "#db4437", color: "#fff", border: "none", cursor: "pointer" };
|
|
357
|
+
var oauthButtonGithub = { flex: 1, padding: "10px 12px", borderRadius: 8, background: "#24292f", color: "#fff", border: "none", cursor: "pointer" };
|
|
358
|
+
var errorBox = { marginTop: 10, color: "#ffb4b4", fontSize: 13 };
|
|
359
|
+
var successBox = { marginTop: 10, color: "#bef264", fontSize: 13 };
|
|
360
|
+
|
|
361
|
+
// src/SignUp.jsx
|
|
362
|
+
import React3, { useState as useState3 } from "react";
|
|
363
|
+
function SignUp() {
|
|
364
|
+
const {
|
|
365
|
+
publishableKey,
|
|
366
|
+
baseUrl,
|
|
367
|
+
redirect,
|
|
368
|
+
redirectTo,
|
|
369
|
+
user,
|
|
370
|
+
loadingUser,
|
|
371
|
+
fetchMe,
|
|
372
|
+
setUser
|
|
373
|
+
} = useAuth();
|
|
374
|
+
const [name, setName] = useState3("");
|
|
375
|
+
const [email, setEmail] = useState3("");
|
|
376
|
+
const [password, setPassword] = useState3("");
|
|
377
|
+
const [loading, setLoading] = useState3(false);
|
|
378
|
+
const [error, setError] = useState3(null);
|
|
379
|
+
const [message, setMessage] = useState3(null);
|
|
380
|
+
if (loadingUser) return null;
|
|
381
|
+
if (user && redirect) {
|
|
382
|
+
if (typeof redirectTo === "function") redirectTo(redirect);
|
|
383
|
+
else if (typeof window !== "undefined") window.location.assign(redirect);
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
async function submit(e) {
|
|
387
|
+
e.preventDefault();
|
|
388
|
+
setError(null);
|
|
389
|
+
setMessage(null);
|
|
390
|
+
setLoading(true);
|
|
391
|
+
const url = `${(baseUrl || "").replace(/\/+$/, "")}/api/sdk/signup`;
|
|
392
|
+
try {
|
|
393
|
+
const res = await fetch(url, {
|
|
394
|
+
method: "POST",
|
|
395
|
+
credentials: "include",
|
|
396
|
+
headers: {
|
|
397
|
+
"Content-Type": "application/json",
|
|
398
|
+
"x-publishable-key": publishableKey || ""
|
|
399
|
+
},
|
|
400
|
+
body: JSON.stringify({ name, email, password })
|
|
401
|
+
});
|
|
402
|
+
const data = await res.json().catch(() => ({}));
|
|
403
|
+
if (!res.ok) throw new Error(data.error || "Signup failed");
|
|
404
|
+
if (data.user && typeof setUser === "function") setUser(data.user);
|
|
405
|
+
if (typeof fetchMe === "function") await fetchMe();
|
|
406
|
+
setMessage("Account created. Redirecting\u2026");
|
|
407
|
+
if (redirect) {
|
|
408
|
+
setTimeout(() => {
|
|
409
|
+
if (typeof redirectTo === "function") redirectTo(redirect);
|
|
410
|
+
else if (typeof window !== "undefined") window.location.assign(redirect);
|
|
411
|
+
}, 300);
|
|
412
|
+
}
|
|
413
|
+
} catch (err) {
|
|
414
|
+
setError(err?.message ?? "Network error");
|
|
415
|
+
console.error("Signup error:", err);
|
|
416
|
+
} finally {
|
|
417
|
+
setLoading(false);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
async function startOAuthFlow(provider) {
|
|
421
|
+
setError(null);
|
|
422
|
+
setLoading(true);
|
|
423
|
+
try {
|
|
424
|
+
const rid = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
425
|
+
const callbackUrl = encodeURIComponent(`${window.location.origin}/signup`);
|
|
426
|
+
const sdkBase = typeof process !== "undefined" && process.env && process.env.NEXT_PUBLIC_SDK_BASE_URL || baseUrl || window.location.origin.replace(/\/+$/, "");
|
|
427
|
+
const startUrl = `${sdkBase}/sdk/auth/start?rid=${rid}&source=${encodeURIComponent(provider)}&callbackUrl=${callbackUrl}`;
|
|
428
|
+
if (!publishableKey) {
|
|
429
|
+
throw new Error("Missing publishable key (client side). Set NEXT_PUBLIC_SDK_PUBLISHABLE_KEY or provide publishableKey in provider.");
|
|
430
|
+
}
|
|
431
|
+
const res = await fetch(startUrl, {
|
|
432
|
+
method: "GET",
|
|
433
|
+
headers: {
|
|
434
|
+
"x-publishable-key": publishableKey
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
const data = await res.json().catch(() => null);
|
|
438
|
+
if (!res.ok) {
|
|
439
|
+
throw new Error(data?.error || `OAuth start failed (${res.status})`);
|
|
440
|
+
}
|
|
441
|
+
if (!data?.oauthUrl) {
|
|
442
|
+
throw new Error("SDK start did not return oauthUrl");
|
|
443
|
+
}
|
|
444
|
+
window.location.href = data.oauthUrl;
|
|
445
|
+
} catch (err) {
|
|
446
|
+
console.error("OAuth start error:", err);
|
|
447
|
+
setError(err?.message || "OAuth start failed");
|
|
448
|
+
setLoading(false);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
const handleGoogle = (e) => {
|
|
452
|
+
if (e && typeof e.preventDefault === "function") e.preventDefault();
|
|
453
|
+
startOAuthFlow("google");
|
|
454
|
+
};
|
|
455
|
+
const handleGithub = (e) => {
|
|
456
|
+
if (e && typeof e.preventDefault === "function") e.preventDefault();
|
|
457
|
+
startOAuthFlow("github");
|
|
458
|
+
};
|
|
459
|
+
return /* @__PURE__ */ React3.createElement("div", { style: overlay2 }, /* @__PURE__ */ React3.createElement("div", { style: modal2 }, /* @__PURE__ */ React3.createElement("h2", { style: title2 }, "Create account"), /* @__PURE__ */ React3.createElement("form", { onSubmit: submit }, /* @__PURE__ */ React3.createElement("label", { style: label2 }, "Full name"), /* @__PURE__ */ React3.createElement(
|
|
460
|
+
"input",
|
|
461
|
+
{
|
|
462
|
+
name: "name",
|
|
463
|
+
style: input2,
|
|
464
|
+
value: name,
|
|
465
|
+
onChange: (e) => setName(e.target.value),
|
|
466
|
+
autoComplete: "name"
|
|
467
|
+
}
|
|
468
|
+
), /* @__PURE__ */ React3.createElement("label", { style: label2 }, "Email"), /* @__PURE__ */ React3.createElement(
|
|
469
|
+
"input",
|
|
470
|
+
{
|
|
471
|
+
name: "email",
|
|
472
|
+
type: "email",
|
|
473
|
+
style: input2,
|
|
474
|
+
value: email,
|
|
475
|
+
onChange: (e) => setEmail(e.target.value),
|
|
476
|
+
required: true,
|
|
477
|
+
autoComplete: "email"
|
|
478
|
+
}
|
|
479
|
+
), /* @__PURE__ */ React3.createElement("label", { style: label2 }, "Password"), /* @__PURE__ */ React3.createElement(
|
|
480
|
+
"input",
|
|
481
|
+
{
|
|
482
|
+
name: "password",
|
|
483
|
+
type: "password",
|
|
484
|
+
style: input2,
|
|
485
|
+
value: password,
|
|
486
|
+
onChange: (e) => setPassword(e.target.value),
|
|
487
|
+
required: true,
|
|
488
|
+
autoComplete: "new-password"
|
|
489
|
+
}
|
|
490
|
+
), /* @__PURE__ */ React3.createElement("div", { style: { marginTop: 12 } }, /* @__PURE__ */ React3.createElement("button", { style: button2, type: "submit", disabled: loading }, loading ? "Creating..." : "Create account")), /* @__PURE__ */ React3.createElement("div", { style: { display: "flex", gap: 8, marginTop: 16 } }, /* @__PURE__ */ React3.createElement(
|
|
491
|
+
"button",
|
|
492
|
+
{
|
|
493
|
+
type: "button",
|
|
494
|
+
onClick: handleGoogle,
|
|
495
|
+
style: oauthButtonGoogle2
|
|
496
|
+
},
|
|
497
|
+
"Continue with Google"
|
|
498
|
+
), /* @__PURE__ */ React3.createElement(
|
|
499
|
+
"button",
|
|
500
|
+
{
|
|
501
|
+
type: "button",
|
|
502
|
+
onClick: handleGithub,
|
|
503
|
+
style: oauthButtonGithub2
|
|
504
|
+
},
|
|
505
|
+
"Continue with GitHub"
|
|
506
|
+
)), error && /* @__PURE__ */ React3.createElement("div", { style: errorBox2 }, error), message && /* @__PURE__ */ React3.createElement("div", { style: successBox2 }, message))));
|
|
507
|
+
}
|
|
508
|
+
var overlay2 = {
|
|
509
|
+
position: "fixed",
|
|
510
|
+
inset: 0,
|
|
511
|
+
background: "rgba(0,0,0,0.5)",
|
|
512
|
+
display: "flex",
|
|
513
|
+
justifyContent: "center",
|
|
514
|
+
alignItems: "center",
|
|
515
|
+
padding: 20,
|
|
516
|
+
zIndex: 9999
|
|
517
|
+
};
|
|
518
|
+
var modal2 = {
|
|
519
|
+
width: "100%",
|
|
520
|
+
maxWidth: 420,
|
|
521
|
+
background: "#0f1724",
|
|
522
|
+
borderRadius: 12,
|
|
523
|
+
padding: 24,
|
|
524
|
+
color: "#fff"
|
|
525
|
+
};
|
|
526
|
+
var title2 = { margin: 0, fontSize: 20, fontWeight: 600 };
|
|
527
|
+
var label2 = { marginTop: 10, display: "block", fontSize: 13 };
|
|
528
|
+
var input2 = {
|
|
529
|
+
width: "100%",
|
|
530
|
+
padding: "10px 12px",
|
|
531
|
+
borderRadius: 8,
|
|
532
|
+
background: "#0b1220",
|
|
533
|
+
color: "#fff",
|
|
534
|
+
border: "1px solid #1e293b",
|
|
535
|
+
marginTop: 6
|
|
536
|
+
};
|
|
537
|
+
var button2 = {
|
|
538
|
+
marginTop: 15,
|
|
539
|
+
width: "100%",
|
|
540
|
+
padding: "10px 12px",
|
|
541
|
+
borderRadius: 8,
|
|
542
|
+
background: "#2563eb",
|
|
543
|
+
border: "none",
|
|
544
|
+
color: "#fff",
|
|
545
|
+
fontWeight: 600,
|
|
546
|
+
cursor: "pointer"
|
|
547
|
+
};
|
|
548
|
+
var oauthButtonGoogle2 = {
|
|
549
|
+
flex: 1,
|
|
550
|
+
padding: "10px 12px",
|
|
551
|
+
borderRadius: 8,
|
|
552
|
+
background: "#db4437",
|
|
553
|
+
color: "#fff",
|
|
554
|
+
border: "none",
|
|
555
|
+
cursor: "pointer"
|
|
556
|
+
};
|
|
557
|
+
var oauthButtonGithub2 = {
|
|
558
|
+
flex: 1,
|
|
559
|
+
padding: "10px 12px",
|
|
560
|
+
borderRadius: 8,
|
|
561
|
+
background: "#24292f",
|
|
562
|
+
color: "#fff",
|
|
563
|
+
border: "none",
|
|
564
|
+
cursor: "pointer"
|
|
565
|
+
};
|
|
566
|
+
var errorBox2 = { marginTop: 10, color: "#ffb4b4", fontSize: 13 };
|
|
567
|
+
var successBox2 = { marginTop: 10, color: "#bef264", fontSize: 13 };
|
|
568
|
+
export {
|
|
569
|
+
provider_default as flowlinkAuthProvider,
|
|
570
|
+
SignIn,
|
|
571
|
+
SignUp,
|
|
572
|
+
useAuth
|
|
573
|
+
};
|
|
574
|
+
|
package/dist/init.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// src/init.js
|
|
2
|
+
var CONFIG = null;
|
|
3
|
+
function initflowlinkAuth({ publishableKey, baseUrl, opts } = {}) {
|
|
4
|
+
if (!publishableKey) throw new Error("flowlink-auth: publishableKey is required in initflowlinkAuth()");
|
|
5
|
+
if (!baseUrl) throw new Error("flowlink-auth: baseUrl is required in initflowlinkAuth()");
|
|
6
|
+
if (!publishableKey.startsWith("pk_")) {
|
|
7
|
+
console.warn('flowlink-auth: publishableKey should start with "pk_"');
|
|
8
|
+
}
|
|
9
|
+
if (typeof window !== "undefined" && window.location.protocol === "http:" && !baseUrl.includes("localhost")) {
|
|
10
|
+
console.error("flowlink-auth: HTTPS is required for production. Current protocol:", window.location.protocol);
|
|
11
|
+
}
|
|
12
|
+
const urlObj = new URL(baseUrl).href;
|
|
13
|
+
if (!urlObj) {
|
|
14
|
+
throw new Error("flowlink-auth: baseUrl must be a valid URL");
|
|
15
|
+
}
|
|
16
|
+
CONFIG = {
|
|
17
|
+
publishableKey,
|
|
18
|
+
baseUrl: baseUrl.replace(/\/+$/, ""),
|
|
19
|
+
requireServerSecret: opts?.requireServerSecret || false,
|
|
20
|
+
secretKey: opts?.serverSecret || null,
|
|
21
|
+
csrfToken: generateCSRFToken()
|
|
22
|
+
// Add CSRF protection
|
|
23
|
+
};
|
|
24
|
+
if (CONFIG.requireServerSecret && !CONFIG.secretKey) {
|
|
25
|
+
console.warn("flowlink-auth: requireServerSecret=true but serverSecret not provided. This is insecure for production.");
|
|
26
|
+
}
|
|
27
|
+
if (CONFIG.secretKey) {
|
|
28
|
+
console.error("flowlink-auth: secretKey should NEVER be provided to client SDK. Use server-side authentication only.");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function generateCSRFToken() {
|
|
32
|
+
if (typeof window === "undefined") return null;
|
|
33
|
+
const array = new Uint8Array(32);
|
|
34
|
+
window.crypto.getRandomValues(array);
|
|
35
|
+
return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
36
|
+
}
|
|
37
|
+
function setConfigFromProvider({ publishableKey, secretKey = null, baseUrl, opts = {} } = {}) {
|
|
38
|
+
if (!publishableKey || !baseUrl) {
|
|
39
|
+
CONFIG = null;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (typeof window !== "undefined" && window.location.protocol === "http:" && !baseUrl.includes("localhost")) {
|
|
43
|
+
console.warn("flowlink-auth: HTTPS recommended for production");
|
|
44
|
+
}
|
|
45
|
+
CONFIG = {
|
|
46
|
+
publishableKey,
|
|
47
|
+
baseUrl: baseUrl.replace(/\/+$/, ""),
|
|
48
|
+
requireServerSecret: opts?.requireServerSecret || false,
|
|
49
|
+
secretKey: secretKey || null,
|
|
50
|
+
csrfToken: generateCSRFToken()
|
|
51
|
+
};
|
|
52
|
+
if (secretKey) {
|
|
53
|
+
console.error("flowlink-auth: NEVER pass secretKey to client-side code");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function getConfigSafe() {
|
|
57
|
+
return CONFIG;
|
|
58
|
+
}
|
|
59
|
+
function getConfig() {
|
|
60
|
+
if (!CONFIG) throw new Error("flowlink-auth: SDK not initialized. Call initflowlinkAuth(...) or wrap components in flowlinkAuthProvider with keys.");
|
|
61
|
+
return CONFIG;
|
|
62
|
+
}
|
|
63
|
+
function getCSRFToken() {
|
|
64
|
+
return CONFIG?.csrfToken || null;
|
|
65
|
+
}
|
|
66
|
+
export {
|
|
67
|
+
getCSRFToken,
|
|
68
|
+
getConfig,
|
|
69
|
+
getConfigSafe,
|
|
70
|
+
initflowlinkAuth,
|
|
71
|
+
setConfigFromProvider
|
|
72
|
+
};
|
|
73
|
+
|