clefbase 2.0.2 → 2.0.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/auth/index.d.ts +47 -103
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +171 -123
- package/dist/auth/index.js.map +1 -1
- package/dist/cli-src/cli/commands/init.js +446 -9
- package/dist/cli.js +395 -6
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +18 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/dist/auth/index.d.ts
CHANGED
|
@@ -1,163 +1,107 @@
|
|
|
1
1
|
import { HttpClient } from "../http";
|
|
2
|
-
import type { AuthUser, AuthResult } from "../types";
|
|
3
|
-
/**
|
|
4
|
-
* Options for `auth.signInWithGateway()`.
|
|
5
|
-
*/
|
|
2
|
+
import type { AuthUser, AuthResult, AuthOptions } from "../types";
|
|
6
3
|
export interface GatewaySignInOptions {
|
|
7
4
|
/**
|
|
8
|
-
*
|
|
9
|
-
* Defaults to `window.location.origin
|
|
10
|
-
* Must be registered as an allowed redirect for your project
|
|
5
|
+
* Where the gateway should redirect after sign-in.
|
|
6
|
+
* Defaults to `window.location.origin`.
|
|
7
|
+
* Must be registered as an allowed redirect for your project.
|
|
11
8
|
*/
|
|
12
9
|
redirectUrl?: string;
|
|
13
10
|
}
|
|
11
|
+
export interface OpenAuthUIOptions {
|
|
12
|
+
/**
|
|
13
|
+
* How to display the auth UI.
|
|
14
|
+
* - `"modal"` (default) — overlay on top of the current page
|
|
15
|
+
* - `"redirect"` — full-page redirect to auth.cleforyx.com
|
|
16
|
+
*/
|
|
17
|
+
mode?: "modal" | "redirect";
|
|
18
|
+
/** Called when the user successfully signs in. */
|
|
19
|
+
onSuccess?: (result: AuthResult) => void;
|
|
20
|
+
/** Called when the modal is dismissed without signing in. */
|
|
21
|
+
onDismiss?: () => void;
|
|
22
|
+
/** Where to redirect after sign-in (redirect mode only). Defaults to current page. */
|
|
23
|
+
redirectUrl?: string;
|
|
24
|
+
}
|
|
14
25
|
type AuthStateCallback = (user: AuthUser | null) => void;
|
|
15
|
-
/**
|
|
16
|
-
* Authentication service. Obtain via `getAuth(app)`.
|
|
17
|
-
*
|
|
18
|
-
* @example
|
|
19
|
-
* const auth = getAuth(app);
|
|
20
|
-
*
|
|
21
|
-
* // Email / password
|
|
22
|
-
* const { user } = await auth.signIn("alice@example.com", "pass");
|
|
23
|
-
*
|
|
24
|
-
* // Google — redirect to gateway, come back signed in
|
|
25
|
-
* await auth.signInWithGateway("google");
|
|
26
|
-
*
|
|
27
|
-
* // On every page load, handle the gateway callback:
|
|
28
|
-
* const result = await auth.handleGatewayCallback();
|
|
29
|
-
* if (result) console.log("Signed in via gateway:", result.user.email);
|
|
30
|
-
*/
|
|
31
26
|
export declare class Auth {
|
|
32
27
|
private readonly http;
|
|
33
28
|
private readonly store;
|
|
34
29
|
private listeners;
|
|
35
|
-
/** The base URL of the Cleforyx auth gateway (e.g. https://auth.cleforyx.com) */
|
|
36
30
|
private readonly gatewayUrl;
|
|
37
|
-
/** The project ID — used as the `?project=` param when redirecting to the gateway */
|
|
38
31
|
private readonly projectId;
|
|
39
|
-
|
|
32
|
+
private _modalCleanup;
|
|
33
|
+
private readonly apiEndpoint;
|
|
34
|
+
private readonly dbId?;
|
|
35
|
+
constructor(http: HttpClient, gatewayUrl: string, projectId: string, options?: AuthOptions);
|
|
36
|
+
/**
|
|
37
|
+
* Get the appropriate endpoint base path for API calls.
|
|
38
|
+
* - "public": /auth/public (for client-side flows)
|
|
39
|
+
* - "external": /auth (for server-side flows with API key)
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
private getEndpointPath;
|
|
40
43
|
private notify;
|
|
41
44
|
private handleResult;
|
|
42
45
|
/** @internal — used by Storage to attach the bearer token to upload requests */
|
|
43
46
|
getAuthHeaders(): Record<string, string>;
|
|
44
|
-
/** The currently signed-in user, or null. */
|
|
45
47
|
get currentUser(): AuthUser | null;
|
|
46
|
-
/** The raw bearer token for the active session, or null. */
|
|
47
48
|
get currentToken(): string | null;
|
|
48
|
-
/**
|
|
49
|
-
* Create a new account with email + password.
|
|
50
|
-
*
|
|
51
|
-
* @example
|
|
52
|
-
* const { user } = await auth.signUp("alice@example.com", "pass123", {
|
|
53
|
-
* displayName: "Alice",
|
|
54
|
-
* metadata: { role: "admin" },
|
|
55
|
-
* });
|
|
56
|
-
*/
|
|
57
49
|
signUp(email: string, password: string, profile?: {
|
|
58
50
|
displayName?: string;
|
|
59
51
|
photoUrl?: string;
|
|
60
52
|
metadata?: Record<string, unknown>;
|
|
61
53
|
}): Promise<AuthResult>;
|
|
62
|
-
/**
|
|
63
|
-
* Sign in with email + password.
|
|
64
|
-
*
|
|
65
|
-
* @example
|
|
66
|
-
* const { user, token } = await auth.signIn("alice@example.com", "pass");
|
|
67
|
-
*/
|
|
68
54
|
signIn(email: string, password: string): Promise<AuthResult>;
|
|
69
55
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* The gateway handles the OAuth flow and redirects back to your app with
|
|
74
|
-
* `?cfx_token=<token>&cfx_project=<projectId>` in the URL.
|
|
75
|
-
* Call `auth.handleGatewayCallback()` on page load to complete sign-in.
|
|
56
|
+
* Open the Cleforyx hosted auth UI as a modal overlay on top of your page.
|
|
57
|
+
* Handles sign-in via email/password, Google, and magic link seamlessly.
|
|
76
58
|
*
|
|
77
|
-
*
|
|
59
|
+
* The modal closes automatically on successful sign-in and fires `onSuccess`.
|
|
78
60
|
*
|
|
79
61
|
* @example
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* redirectUrl: "https://myapp.cleforyx.com/dashboard",
|
|
62
|
+
* auth.openAuthUI({
|
|
63
|
+
* onSuccess: (result) => {
|
|
64
|
+
* console.log("Signed in:", result.user.email);
|
|
65
|
+
* navigate("/dashboard");
|
|
66
|
+
* },
|
|
86
67
|
* });
|
|
87
68
|
*/
|
|
88
|
-
|
|
69
|
+
openAuthUI(options?: OpenAuthUIOptions): void;
|
|
70
|
+
/** Close the auth modal programmatically if open. */
|
|
71
|
+
closeAuthUI(): void;
|
|
89
72
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* `handleGatewayCallback()`.
|
|
73
|
+
* Redirect to the Cleforyx auth gateway for OAuth sign-in.
|
|
74
|
+
* Use `openAuthUI()` for a modal experience instead.
|
|
93
75
|
*
|
|
94
76
|
* @example
|
|
95
|
-
*
|
|
96
|
-
* showLoadingSpinner();
|
|
97
|
-
* }
|
|
77
|
+
* await auth.signInWithGateway("google");
|
|
98
78
|
*/
|
|
79
|
+
signInWithGateway(provider: "google", options?: GatewaySignInOptions): Promise<never>;
|
|
99
80
|
isGatewayCallbackPending(): boolean;
|
|
100
81
|
/**
|
|
101
|
-
* Call
|
|
102
|
-
*
|
|
103
|
-
* the server, the session is saved, and the signed-in `AuthResult` is
|
|
104
|
-
* returned. The query params are cleaned from the URL automatically.
|
|
105
|
-
*
|
|
106
|
-
* Returns `null` if there is no pending callback.
|
|
82
|
+
* Call on every page load to handle the gateway redirect callback.
|
|
83
|
+
* Detects `?cfx_token=` in the URL, validates it, and returns the signed-in user.
|
|
107
84
|
*
|
|
108
85
|
* @example
|
|
109
|
-
* // In your app's root component or entry point:
|
|
110
86
|
* useEffect(() => {
|
|
111
87
|
* auth.handleGatewayCallback().then(result => {
|
|
112
|
-
* if (result)
|
|
113
|
-
* console.log("Signed in:", result.user.email);
|
|
114
|
-
* navigate("/dashboard");
|
|
115
|
-
* }
|
|
88
|
+
* if (result) navigate("/dashboard");
|
|
116
89
|
* });
|
|
117
90
|
* }, []);
|
|
118
|
-
*
|
|
119
|
-
* // Or outside React:
|
|
120
|
-
* const result = await auth.handleGatewayCallback();
|
|
121
|
-
* if (result) showDashboard(result.user);
|
|
122
91
|
*/
|
|
123
92
|
handleGatewayCallback(): Promise<AuthResult | null>;
|
|
124
|
-
/** Sign out and clear the local session. */
|
|
125
93
|
signOut(): Promise<void>;
|
|
126
|
-
/** Re-fetch the current user's profile from the server and update the cache. */
|
|
127
94
|
refreshCurrentUser(): Promise<AuthUser | null>;
|
|
128
|
-
/**
|
|
129
|
-
* Update the signed-in user's profile.
|
|
130
|
-
*
|
|
131
|
-
* @example
|
|
132
|
-
* await auth.updateProfile({ displayName: "Bob", metadata: { theme: "dark" } });
|
|
133
|
-
*/
|
|
134
95
|
updateProfile(updates: {
|
|
135
96
|
displayName?: string;
|
|
136
97
|
photoUrl?: string;
|
|
137
98
|
metadata?: Record<string, unknown>;
|
|
138
99
|
}): Promise<AuthUser>;
|
|
139
|
-
/** Change password. All other sessions are invalidated server-side. */
|
|
140
100
|
changePassword(currentPassword: string, newPassword: string): Promise<void>;
|
|
141
|
-
/** Request a password-reset email. Safe to call even for unregistered addresses. */
|
|
142
101
|
sendPasswordResetEmail(email: string): Promise<void>;
|
|
143
|
-
/** Complete a password reset with the token from the reset email. */
|
|
144
102
|
confirmPasswordReset(resetToken: string, newPassword: string): Promise<void>;
|
|
145
|
-
/** Send an email verification code to the currently signed-in user. */
|
|
146
103
|
sendEmailVerification(): Promise<void>;
|
|
147
|
-
/** Verify email with the code the user received. */
|
|
148
104
|
verifyEmail(code: string): Promise<AuthUser>;
|
|
149
|
-
/**
|
|
150
|
-
* Subscribe to auth state changes.
|
|
151
|
-
* Fires immediately with the current user, then on every sign-in or sign-out.
|
|
152
|
-
* Returns an unsubscribe function.
|
|
153
|
-
*
|
|
154
|
-
* @example
|
|
155
|
-
* const unsubscribe = auth.onAuthStateChanged(user => {
|
|
156
|
-
* if (user) console.log("signed in:", user.email);
|
|
157
|
-
* else console.log("signed out");
|
|
158
|
-
* });
|
|
159
|
-
* unsubscribe();
|
|
160
|
-
*/
|
|
161
105
|
onAuthStateChanged(callback: AuthStateCallback): () => void;
|
|
162
106
|
}
|
|
163
107
|
export {};
|
package/dist/auth/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"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"}
|
package/dist/auth/index.js
CHANGED
|
@@ -47,31 +47,34 @@ class SessionStore {
|
|
|
47
47
|
catch { /* ignore */ }
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
-
/**
|
|
51
|
-
* Authentication service. Obtain via `getAuth(app)`.
|
|
52
|
-
*
|
|
53
|
-
* @example
|
|
54
|
-
* const auth = getAuth(app);
|
|
55
|
-
*
|
|
56
|
-
* // Email / password
|
|
57
|
-
* const { user } = await auth.signIn("alice@example.com", "pass");
|
|
58
|
-
*
|
|
59
|
-
* // Google — redirect to gateway, come back signed in
|
|
60
|
-
* await auth.signInWithGateway("google");
|
|
61
|
-
*
|
|
62
|
-
* // On every page load, handle the gateway callback:
|
|
63
|
-
* const result = await auth.handleGatewayCallback();
|
|
64
|
-
* if (result) console.log("Signed in via gateway:", result.user.email);
|
|
65
|
-
*/
|
|
66
50
|
class Auth {
|
|
67
|
-
constructor(http, gatewayUrl, projectId) {
|
|
51
|
+
constructor(http, gatewayUrl, projectId, options) {
|
|
68
52
|
this.http = http;
|
|
69
53
|
this.store = new SessionStore();
|
|
70
54
|
this.listeners = [];
|
|
55
|
+
this._modalCleanup = null;
|
|
71
56
|
this.gatewayUrl = gatewayUrl.replace(/\/+$/, "");
|
|
72
57
|
this.projectId = projectId;
|
|
58
|
+
this.apiEndpoint = options?.apiEndpoint ?? "external";
|
|
59
|
+
this.dbId = options?.dbId;
|
|
73
60
|
this.store.load();
|
|
74
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Get the appropriate endpoint base path for API calls.
|
|
64
|
+
* - "public": /auth/public (for client-side flows)
|
|
65
|
+
* - "external": /auth (for server-side flows with API key)
|
|
66
|
+
* @internal
|
|
67
|
+
*/
|
|
68
|
+
getEndpointPath(operation) {
|
|
69
|
+
if (this.apiEndpoint === "public") {
|
|
70
|
+
if (!this.dbId) {
|
|
71
|
+
throw new Error(`dbId is required for public routes. Pass it when initializing Auth with apiEndpoint: "public".`);
|
|
72
|
+
}
|
|
73
|
+
// For public routes, append dbId as query param and operation path
|
|
74
|
+
return `/public${operation}?dbId=${encodeURIComponent(this.dbId)}`;
|
|
75
|
+
}
|
|
76
|
+
return operation;
|
|
77
|
+
}
|
|
75
78
|
// ── Internals ─────────────────────────────────────────────────────────────
|
|
76
79
|
notify(user) {
|
|
77
80
|
for (const cb of this.listeners) {
|
|
@@ -96,104 +99,185 @@ class Auth {
|
|
|
96
99
|
return s ? { Authorization: `Bearer ${s.token}` } : {};
|
|
97
100
|
}
|
|
98
101
|
// ── Public API ────────────────────────────────────────────────────────────
|
|
99
|
-
/** The currently signed-in user, or null. */
|
|
100
102
|
get currentUser() { return this.store.load()?.user ?? null; }
|
|
101
|
-
/** The raw bearer token for the active session, or null. */
|
|
102
103
|
get currentToken() { return this.store.load()?.token ?? null; }
|
|
103
104
|
// ── Email / password ──────────────────────────────────────────────────────
|
|
104
|
-
/**
|
|
105
|
-
* Create a new account with email + password.
|
|
106
|
-
*
|
|
107
|
-
* @example
|
|
108
|
-
* const { user } = await auth.signUp("alice@example.com", "pass123", {
|
|
109
|
-
* displayName: "Alice",
|
|
110
|
-
* metadata: { role: "admin" },
|
|
111
|
-
* });
|
|
112
|
-
*/
|
|
113
105
|
async signUp(email, password, profile) {
|
|
114
106
|
const result = await this.http.post("/signup", { email, password, ...profile });
|
|
115
107
|
return this.handleResult(result);
|
|
116
108
|
}
|
|
117
|
-
/**
|
|
118
|
-
* Sign in with email + password.
|
|
119
|
-
*
|
|
120
|
-
* @example
|
|
121
|
-
* const { user, token } = await auth.signIn("alice@example.com", "pass");
|
|
122
|
-
*/
|
|
123
109
|
async signIn(email, password) {
|
|
124
110
|
const result = await this.http.post("/signin", { email, password });
|
|
125
111
|
return this.handleResult(result);
|
|
126
112
|
}
|
|
127
|
-
// ──
|
|
113
|
+
// ── Auth UI modal ─────────────────────────────────────────────────────────
|
|
128
114
|
/**
|
|
129
|
-
*
|
|
130
|
-
*
|
|
115
|
+
* Open the Cleforyx hosted auth UI as a modal overlay on top of your page.
|
|
116
|
+
* Handles sign-in via email/password, Google, and magic link seamlessly.
|
|
131
117
|
*
|
|
132
|
-
* The
|
|
133
|
-
* `?cfx_token=<token>&cfx_project=<projectId>` in the URL.
|
|
134
|
-
* Call `auth.handleGatewayCallback()` on page load to complete sign-in.
|
|
135
|
-
*
|
|
136
|
-
* **This method never returns** — it calls `window.location.href`.
|
|
118
|
+
* The modal closes automatically on successful sign-in and fires `onSuccess`.
|
|
137
119
|
*
|
|
138
120
|
* @example
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* redirectUrl: "https://myapp.cleforyx.com/dashboard",
|
|
121
|
+
* auth.openAuthUI({
|
|
122
|
+
* onSuccess: (result) => {
|
|
123
|
+
* console.log("Signed in:", result.user.email);
|
|
124
|
+
* navigate("/dashboard");
|
|
125
|
+
* },
|
|
145
126
|
* });
|
|
146
127
|
*/
|
|
147
|
-
|
|
128
|
+
openAuthUI(options = {}) {
|
|
148
129
|
if (typeof window === "undefined") {
|
|
149
|
-
throw new types_1.ClefbaseError("
|
|
130
|
+
throw new types_1.ClefbaseError("openAuthUI() requires a browser environment.", "UNSUPPORTED_ENVIRONMENT");
|
|
150
131
|
}
|
|
151
|
-
const
|
|
132
|
+
const { mode = "modal", onSuccess, onDismiss, redirectUrl } = options;
|
|
133
|
+
const currentUrl = window.location.href;
|
|
134
|
+
const returnUrl = redirectUrl ?? currentUrl;
|
|
152
135
|
const params = new URLSearchParams({
|
|
153
136
|
project: this.projectId,
|
|
154
|
-
redirect:
|
|
137
|
+
redirect: returnUrl,
|
|
138
|
+
embed: "popup",
|
|
155
139
|
});
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
140
|
+
const authUrl = `${this.gatewayUrl}/login?${params}`;
|
|
141
|
+
if (mode === "redirect") {
|
|
142
|
+
window.location.href = authUrl;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// ── Modal mode ──────────────────────────────────────────────────────────
|
|
146
|
+
// Inject styles once
|
|
147
|
+
if (!document.getElementById("cfx-modal-styles")) {
|
|
148
|
+
const style = document.createElement("style");
|
|
149
|
+
style.id = "cfx-modal-styles";
|
|
150
|
+
style.textContent = `
|
|
151
|
+
@keyframes cfxFadeIn { from { opacity: 0 } to { opacity: 1 } }
|
|
152
|
+
@keyframes cfxSlideUp { from { transform: translateY(20px); opacity: 0 } to { transform: none; opacity: 1 } }
|
|
153
|
+
#cfx-auth-overlay {
|
|
154
|
+
position: fixed; inset: 0; z-index: 999999;
|
|
155
|
+
background: rgba(15,23,42,0.6); backdrop-filter: blur(4px);
|
|
156
|
+
display: flex; align-items: center; justify-content: center;
|
|
157
|
+
animation: cfxFadeIn 0.18s ease;
|
|
158
|
+
padding: 16px; box-sizing: border-box;
|
|
159
|
+
}
|
|
160
|
+
#cfx-auth-card {
|
|
161
|
+
background: white; border-radius: 20px;
|
|
162
|
+
box-shadow: 0 24px 64px rgba(0,0,0,0.2);
|
|
163
|
+
overflow: hidden; width: 100%; max-width: 460px;
|
|
164
|
+
animation: cfxSlideUp 0.22s ease; position: relative;
|
|
165
|
+
}
|
|
166
|
+
#cfx-auth-frame {
|
|
167
|
+
width: 100%; height: 560px; border: none; display: block;
|
|
168
|
+
}
|
|
169
|
+
#cfx-auth-close {
|
|
170
|
+
position: absolute; top: 14px; right: 16px;
|
|
171
|
+
background: rgba(0,0,0,0.06); border: none;
|
|
172
|
+
width: 28px; height: 28px; border-radius: 50%;
|
|
173
|
+
font-size: 16px; line-height: 1; cursor: pointer;
|
|
174
|
+
display: flex; align-items: center; justify-content: center;
|
|
175
|
+
color: #64748b; z-index: 1; transition: background 0.15s;
|
|
176
|
+
}
|
|
177
|
+
#cfx-auth-close:hover { background: rgba(0,0,0,0.12); }
|
|
178
|
+
`;
|
|
179
|
+
document.head.appendChild(style);
|
|
180
|
+
}
|
|
181
|
+
// Create overlay
|
|
182
|
+
const overlay = document.createElement("div");
|
|
183
|
+
overlay.id = "cfx-auth-overlay";
|
|
184
|
+
const card = document.createElement("div");
|
|
185
|
+
card.id = "cfx-auth-card";
|
|
186
|
+
const closeBtn = document.createElement("button");
|
|
187
|
+
closeBtn.id = "cfx-auth-close";
|
|
188
|
+
closeBtn.innerHTML = "✕";
|
|
189
|
+
closeBtn.setAttribute("aria-label", "Close");
|
|
190
|
+
const iframe = document.createElement("iframe");
|
|
191
|
+
iframe.id = "cfx-auth-frame";
|
|
192
|
+
iframe.src = authUrl;
|
|
193
|
+
iframe.setAttribute("allow", "identity-credentials-get");
|
|
194
|
+
card.appendChild(closeBtn);
|
|
195
|
+
card.appendChild(iframe);
|
|
196
|
+
overlay.appendChild(card);
|
|
197
|
+
document.body.appendChild(overlay);
|
|
198
|
+
// Prevent body scroll
|
|
199
|
+
const prevOverflow = document.body.style.overflow;
|
|
200
|
+
document.body.style.overflow = "hidden";
|
|
201
|
+
const cleanup = () => {
|
|
202
|
+
overlay.remove();
|
|
203
|
+
document.body.style.overflow = prevOverflow;
|
|
204
|
+
window.removeEventListener("message", messageHandler);
|
|
205
|
+
this._modalCleanup = null;
|
|
206
|
+
};
|
|
207
|
+
const messageHandler = async (e) => {
|
|
208
|
+
if (e.data?.source !== "cleforyx-auth" || e.data?.type !== "auth_success")
|
|
209
|
+
return;
|
|
210
|
+
const { token, user: rawUser } = e.data;
|
|
211
|
+
if (!token)
|
|
212
|
+
return;
|
|
213
|
+
cleanup();
|
|
214
|
+
// Build a full AuthResult — fetch fresh user data to get expiresAt
|
|
215
|
+
// Use the public /me endpoint (no API key needed, just dbId + bearer)
|
|
216
|
+
try {
|
|
217
|
+
const serverUrl = this.http.getBaseUrl().replace(/\/auth$/, "");
|
|
218
|
+
const res = await fetch(`${serverUrl}/auth/public/me?dbId=${encodeURIComponent(this.projectId)}`, {
|
|
219
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
220
|
+
});
|
|
221
|
+
const user = res.ok ? await res.json() : rawUser;
|
|
222
|
+
const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
223
|
+
const result = { user, token, session: { token, expiresAt } };
|
|
224
|
+
this.handleResult(result);
|
|
225
|
+
onSuccess?.(result);
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
// Fallback: use the user data from postMessage directly
|
|
229
|
+
const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
230
|
+
const result = { user: rawUser, token, session: { token, expiresAt } };
|
|
231
|
+
this.handleResult(result);
|
|
232
|
+
onSuccess?.(result);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
closeBtn.onclick = () => { cleanup(); onDismiss?.(); };
|
|
236
|
+
overlay.addEventListener("click", (e) => {
|
|
237
|
+
if (e.target === overlay) {
|
|
238
|
+
cleanup();
|
|
239
|
+
onDismiss?.();
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
window.addEventListener("message", messageHandler);
|
|
243
|
+
this._modalCleanup = cleanup;
|
|
244
|
+
}
|
|
245
|
+
/** Close the auth modal programmatically if open. */
|
|
246
|
+
closeAuthUI() {
|
|
247
|
+
this._modalCleanup?.();
|
|
159
248
|
}
|
|
249
|
+
// ── Gateway redirect flow ─────────────────────────────────────────────────
|
|
160
250
|
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
* `handleGatewayCallback()`.
|
|
251
|
+
* Redirect to the Cleforyx auth gateway for OAuth sign-in.
|
|
252
|
+
* Use `openAuthUI()` for a modal experience instead.
|
|
164
253
|
*
|
|
165
254
|
* @example
|
|
166
|
-
*
|
|
167
|
-
* showLoadingSpinner();
|
|
168
|
-
* }
|
|
255
|
+
* await auth.signInWithGateway("google");
|
|
169
256
|
*/
|
|
257
|
+
async signInWithGateway(provider, options) {
|
|
258
|
+
if (typeof window === "undefined") {
|
|
259
|
+
throw new types_1.ClefbaseError("signInWithGateway() requires a browser environment.", "UNSUPPORTED_ENVIRONMENT");
|
|
260
|
+
}
|
|
261
|
+
const redirectUrl = options?.redirectUrl ?? window.location.href;
|
|
262
|
+
const params = new URLSearchParams({ project: this.projectId, redirect: redirectUrl });
|
|
263
|
+
window.location.href = `${this.gatewayUrl}/google?${params}`;
|
|
264
|
+
return new Promise(() => { });
|
|
265
|
+
}
|
|
170
266
|
isGatewayCallbackPending() {
|
|
171
267
|
if (typeof window === "undefined")
|
|
172
268
|
return false;
|
|
173
269
|
return new URLSearchParams(window.location.search).has("cfx_token");
|
|
174
270
|
}
|
|
175
271
|
/**
|
|
176
|
-
* Call
|
|
177
|
-
*
|
|
178
|
-
* the server, the session is saved, and the signed-in `AuthResult` is
|
|
179
|
-
* returned. The query params are cleaned from the URL automatically.
|
|
180
|
-
*
|
|
181
|
-
* Returns `null` if there is no pending callback.
|
|
272
|
+
* Call on every page load to handle the gateway redirect callback.
|
|
273
|
+
* Detects `?cfx_token=` in the URL, validates it, and returns the signed-in user.
|
|
182
274
|
*
|
|
183
275
|
* @example
|
|
184
|
-
* // In your app's root component or entry point:
|
|
185
276
|
* useEffect(() => {
|
|
186
277
|
* auth.handleGatewayCallback().then(result => {
|
|
187
|
-
* if (result)
|
|
188
|
-
* console.log("Signed in:", result.user.email);
|
|
189
|
-
* navigate("/dashboard");
|
|
190
|
-
* }
|
|
278
|
+
* if (result) navigate("/dashboard");
|
|
191
279
|
* });
|
|
192
280
|
* }, []);
|
|
193
|
-
*
|
|
194
|
-
* // Or outside React:
|
|
195
|
-
* const result = await auth.handleGatewayCallback();
|
|
196
|
-
* if (result) showDashboard(result.user);
|
|
197
281
|
*/
|
|
198
282
|
async handleGatewayCallback() {
|
|
199
283
|
if (typeof window === "undefined")
|
|
@@ -202,33 +286,25 @@ class Auth {
|
|
|
202
286
|
const token = params.get("cfx_token");
|
|
203
287
|
if (!token)
|
|
204
288
|
return null;
|
|
205
|
-
// Clean
|
|
206
|
-
// aren't reprocessed on refresh or shared in links
|
|
289
|
+
// Clean URL
|
|
207
290
|
params.delete("cfx_token");
|
|
208
291
|
params.delete("cfx_project");
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
// Validate the token against the server and fetch the full user object
|
|
292
|
+
window.history.replaceState({}, "", window.location.pathname + (params.toString() ? `?${params}` : ""));
|
|
293
|
+
// Use /auth/public/me — no API key needed, just dbId + bearer
|
|
294
|
+
const serverUrl = this.http.getBaseUrl().replace(/\/auth$/, "");
|
|
213
295
|
try {
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
296
|
+
const res = await fetch(`${serverUrl}/auth/public/me?dbId=${encodeURIComponent(this.projectId)}`, { headers: { Authorization: `Bearer ${token}` } });
|
|
297
|
+
if (!res.ok)
|
|
298
|
+
throw new Error(`${res.status} ${res.statusText}`);
|
|
299
|
+
const user = await res.json();
|
|
217
300
|
const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
218
|
-
|
|
219
|
-
user,
|
|
220
|
-
token,
|
|
221
|
-
session: { token, expiresAt },
|
|
222
|
-
};
|
|
223
|
-
return this.handleResult(result);
|
|
301
|
+
return this.handleResult({ user, token, session: { token, expiresAt } });
|
|
224
302
|
}
|
|
225
303
|
catch (err) {
|
|
226
|
-
// Token was invalid or expired — don't sign in
|
|
227
304
|
throw new types_1.ClefbaseError(`Gateway callback failed: ${err.message}`, "GATEWAY_CALLBACK_ERROR");
|
|
228
305
|
}
|
|
229
306
|
}
|
|
230
307
|
// ── Session management ────────────────────────────────────────────────────
|
|
231
|
-
/** Sign out and clear the local session. */
|
|
232
308
|
async signOut() {
|
|
233
309
|
const token = this.currentToken;
|
|
234
310
|
if (token) {
|
|
@@ -240,7 +316,6 @@ class Auth {
|
|
|
240
316
|
this.store.clear();
|
|
241
317
|
this.notify(null);
|
|
242
318
|
}
|
|
243
|
-
/** Re-fetch the current user's profile from the server and update the cache. */
|
|
244
319
|
async refreshCurrentUser() {
|
|
245
320
|
const token = this.currentToken;
|
|
246
321
|
if (!token)
|
|
@@ -257,72 +332,45 @@ class Auth {
|
|
|
257
332
|
return null;
|
|
258
333
|
}
|
|
259
334
|
}
|
|
260
|
-
/**
|
|
261
|
-
* Update the signed-in user's profile.
|
|
262
|
-
*
|
|
263
|
-
* @example
|
|
264
|
-
* await auth.updateProfile({ displayName: "Bob", metadata: { theme: "dark" } });
|
|
265
|
-
*/
|
|
266
335
|
async updateProfile(updates) {
|
|
267
336
|
const token = this.currentToken;
|
|
268
337
|
if (!token)
|
|
269
338
|
throw new Error("No user is currently signed in.");
|
|
270
|
-
const user = await this.http.patch("/me", updates, {
|
|
271
|
-
Authorization: `Bearer ${token}`,
|
|
272
|
-
});
|
|
339
|
+
const user = await this.http.patch("/me", updates, { Authorization: `Bearer ${token}` });
|
|
273
340
|
const session = this.store.load();
|
|
274
341
|
if (session)
|
|
275
342
|
this.store.save({ ...session, user });
|
|
276
343
|
this.notify(user);
|
|
277
344
|
return user;
|
|
278
345
|
}
|
|
279
|
-
/** Change password. All other sessions are invalidated server-side. */
|
|
280
346
|
async changePassword(currentPassword, newPassword) {
|
|
281
347
|
const token = this.currentToken;
|
|
282
348
|
if (!token)
|
|
283
349
|
throw new Error("No user is currently signed in.");
|
|
284
350
|
await this.http.post("/change-password", { currentPassword, newPassword }, { Authorization: `Bearer ${token}` });
|
|
285
351
|
}
|
|
286
|
-
/** Request a password-reset email. Safe to call even for unregistered addresses. */
|
|
287
352
|
async sendPasswordResetEmail(email) {
|
|
288
353
|
await this.http.post("/forgot-password", { email });
|
|
289
354
|
}
|
|
290
|
-
/** Complete a password reset with the token from the reset email. */
|
|
291
355
|
async confirmPasswordReset(resetToken, newPassword) {
|
|
292
356
|
await this.http.post("/reset-password", { token: resetToken, newPassword });
|
|
293
357
|
}
|
|
294
|
-
/** Send an email verification code to the currently signed-in user. */
|
|
295
358
|
async sendEmailVerification() {
|
|
296
359
|
const token = this.currentToken;
|
|
297
360
|
if (!token)
|
|
298
361
|
throw new Error("No user is currently signed in.");
|
|
299
362
|
await this.http.post("/send-verification", undefined, { Authorization: `Bearer ${token}` });
|
|
300
363
|
}
|
|
301
|
-
/** Verify email with the code the user received. */
|
|
302
364
|
async verifyEmail(code) {
|
|
303
365
|
return this.http.post("/verify-email", { code });
|
|
304
366
|
}
|
|
305
|
-
/**
|
|
306
|
-
* Subscribe to auth state changes.
|
|
307
|
-
* Fires immediately with the current user, then on every sign-in or sign-out.
|
|
308
|
-
* Returns an unsubscribe function.
|
|
309
|
-
*
|
|
310
|
-
* @example
|
|
311
|
-
* const unsubscribe = auth.onAuthStateChanged(user => {
|
|
312
|
-
* if (user) console.log("signed in:", user.email);
|
|
313
|
-
* else console.log("signed out");
|
|
314
|
-
* });
|
|
315
|
-
* unsubscribe();
|
|
316
|
-
*/
|
|
317
367
|
onAuthStateChanged(callback) {
|
|
318
368
|
this.listeners.push(callback);
|
|
319
369
|
try {
|
|
320
370
|
callback(this.currentUser);
|
|
321
371
|
}
|
|
322
372
|
catch { /* ignore */ }
|
|
323
|
-
return () => {
|
|
324
|
-
this.listeners = this.listeners.filter(cb => cb !== callback);
|
|
325
|
-
};
|
|
373
|
+
return () => { this.listeners = this.listeners.filter(cb => cb !== callback); };
|
|
326
374
|
}
|
|
327
375
|
}
|
|
328
376
|
exports.Auth = Auth;
|