clefbase 2.0.5 → 2.0.6
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 +21 -36
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +25 -136
- package/dist/auth/index.js.map +1 -1
- package/dist/cli-src/cli/commands/init.js +3 -618
- package/dist/cli.js +4 -571
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -85,11 +85,6 @@ async function runInit(cwd = process.cwd()) {
|
|
|
85
85
|
hosting: services.includes("hosting"),
|
|
86
86
|
functions: services.includes("functions"),
|
|
87
87
|
};
|
|
88
|
-
// ── Step 4: Auth setup ────────────────────────────────────────────────────
|
|
89
|
-
let authSetup;
|
|
90
|
-
if (cfg.services.auth) {
|
|
91
|
-
authSetup = await setupAuth(cwd);
|
|
92
|
-
}
|
|
93
88
|
// ── Step 5: Hosting setup ─────────────────────────────────────────────────
|
|
94
89
|
if (cfg.services.hosting) {
|
|
95
90
|
await setupHosting(cfg, cwd);
|
|
@@ -105,11 +100,6 @@ async function runInit(cwd = process.cwd()) {
|
|
|
105
100
|
(0, config_1.writeEnvExample)(cfg, cwd);
|
|
106
101
|
// ── Step 8: Scaffold src/lib ──────────────────────────────────────────────
|
|
107
102
|
const libResult = scaffoldLib(cfg, cwd);
|
|
108
|
-
// ── Step 9: Scaffold auth files ──────────────────────────────────────────
|
|
109
|
-
let authResult;
|
|
110
|
-
if (cfg.services.auth && authSetup) {
|
|
111
|
-
authResult = await scaffoldAuth(cwd, authSetup);
|
|
112
|
-
}
|
|
113
103
|
// ── Done ──────────────────────────────────────────────────────────────────
|
|
114
104
|
console.log();
|
|
115
105
|
console.log(chalk_1.default.green.bold(" ✓ Project initialised!"));
|
|
@@ -121,172 +111,12 @@ async function runInit(cwd = process.cwd()) {
|
|
|
121
111
|
console.log(chalk_1.default.dim(` Lib config: ${libResult.configCopy}`));
|
|
122
112
|
console.log(chalk_1.default.dim(` Lib entry: ${libResult.libFile}`));
|
|
123
113
|
}
|
|
124
|
-
if (authResult?.hasAuth) {
|
|
125
|
-
if (authResult.context)
|
|
126
|
-
console.log(chalk_1.default.dim(` Auth context: ${authResult.context}`));
|
|
127
|
-
if (authResult.modal)
|
|
128
|
-
console.log(chalk_1.default.dim(` Auth modal: ${authResult.modal}`));
|
|
129
|
-
if (authResult.protected)
|
|
130
|
-
console.log(chalk_1.default.dim(` Protected: ${authResult.protected}`));
|
|
131
|
-
}
|
|
132
114
|
if (cfg.services.functions) {
|
|
133
115
|
console.log(chalk_1.default.dim(` Functions: functions/ (${functionsRuntime ?? "node"})`));
|
|
134
116
|
console.log(chalk_1.default.dim(` run: node functions/deploy.mjs`));
|
|
135
117
|
}
|
|
136
118
|
console.log();
|
|
137
|
-
printUsageHint(cfg
|
|
138
|
-
}
|
|
139
|
-
// ─── Auth scaffolding ────────────────────────────────────────────────────────
|
|
140
|
-
/**
|
|
141
|
-
* Interactively scaffold auth files (AuthContext, AuthModal, etc.).
|
|
142
|
-
*/
|
|
143
|
-
async function setupAuth(cwd) {
|
|
144
|
-
console.log();
|
|
145
|
-
console.log(chalk_1.default.bold(" Auth Setup"));
|
|
146
|
-
console.log();
|
|
147
|
-
const { scaffoldContext } = await inquirer_1.default.prompt([{
|
|
148
|
-
type: "confirm",
|
|
149
|
-
name: "scaffoldContext",
|
|
150
|
-
message: "Scaffold React AuthContext?",
|
|
151
|
-
default: true,
|
|
152
|
-
}]);
|
|
153
|
-
const { scaffoldModal } = await inquirer_1.default.prompt([{
|
|
154
|
-
type: "confirm",
|
|
155
|
-
name: "scaffoldModal",
|
|
156
|
-
message: "Scaffold AuthModal component?",
|
|
157
|
-
default: true,
|
|
158
|
-
}]);
|
|
159
|
-
const { updateApp } = await inquirer_1.default.prompt([{
|
|
160
|
-
type: "confirm",
|
|
161
|
-
name: "updateApp",
|
|
162
|
-
message: "Update App.tsx to use AuthProvider?",
|
|
163
|
-
default: false,
|
|
164
|
-
}]);
|
|
165
|
-
return { scaffoldContext, scaffoldModal, updateApp };
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Get the path to a template file within the clefbase CLI package.
|
|
169
|
-
*/
|
|
170
|
-
function getTemplatePath(filename) {
|
|
171
|
-
// In compiled dist, __dirname will be dist-src/cli/commands
|
|
172
|
-
// We want to resolve to src/cli/templates/ in source
|
|
173
|
-
// Or in dist, it would be at ../templates/
|
|
174
|
-
let dir = __dirname;
|
|
175
|
-
// Check if we're in dist (compiled)
|
|
176
|
-
if (dir.includes("/dist-src/")) {
|
|
177
|
-
return path_1.default.join(dir, "../templates", filename);
|
|
178
|
-
}
|
|
179
|
-
// Otherwise we're in source
|
|
180
|
-
return path_1.default.join(dir, "../templates", filename);
|
|
181
|
-
}
|
|
182
|
-
/**
|
|
183
|
-
* Read a template file with fallback to embedded template.
|
|
184
|
-
*/
|
|
185
|
-
function readTemplate(filename, fallback) {
|
|
186
|
-
try {
|
|
187
|
-
const templatePath = getTemplatePath(filename);
|
|
188
|
-
return fs_1.default.readFileSync(templatePath, "utf-8");
|
|
189
|
-
}
|
|
190
|
-
catch {
|
|
191
|
-
return fallback;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
/**
|
|
195
|
-
* Scaffold auth-related files (AuthContext, AuthModal, ProtectedRoute).
|
|
196
|
-
*/
|
|
197
|
-
async function scaffoldAuth(cwd, setup) {
|
|
198
|
-
const result = { hasAuth: false };
|
|
199
|
-
// Create context directory
|
|
200
|
-
const contextDir = path_1.default.join(cwd, "src", "context");
|
|
201
|
-
fs_1.default.mkdirSync(contextDir, { recursive: true });
|
|
202
|
-
if (setup.scaffoldContext) {
|
|
203
|
-
const contextFile = path_1.default.join(contextDir, "AuthContext.tsx");
|
|
204
|
-
try {
|
|
205
|
-
const content = readTemplate("AuthContext.tsx.template", AUTH_CONTEXT_FALLBACK);
|
|
206
|
-
fs_1.default.writeFileSync(contextFile, content);
|
|
207
|
-
result.context = path_1.default.relative(cwd, contextFile);
|
|
208
|
-
result.hasAuth = true;
|
|
209
|
-
}
|
|
210
|
-
catch (err) {
|
|
211
|
-
console.warn(chalk_1.default.yellow(` Warning: Could not scaffold AuthContext: ${err.message}`));
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
if (setup.scaffoldModal) {
|
|
215
|
-
const componentDir = path_1.default.join(cwd, "src", "components");
|
|
216
|
-
fs_1.default.mkdirSync(componentDir, { recursive: true });
|
|
217
|
-
const modalFile = path_1.default.join(componentDir, "AuthModal.tsx");
|
|
218
|
-
try {
|
|
219
|
-
const content = readTemplate("AuthModal.tsx.template", AUTH_MODAL_FALLBACK);
|
|
220
|
-
fs_1.default.writeFileSync(modalFile, content);
|
|
221
|
-
result.modal = path_1.default.relative(cwd, modalFile);
|
|
222
|
-
result.hasAuth = true;
|
|
223
|
-
}
|
|
224
|
-
catch (err) {
|
|
225
|
-
console.warn(chalk_1.default.yellow(` Warning: Could not scaffold AuthModal: ${err.message}`));
|
|
226
|
-
}
|
|
227
|
-
// Also scaffold ProtectedRoute
|
|
228
|
-
const protectedFile = path_1.default.join(componentDir, "ProtectedRoute.tsx");
|
|
229
|
-
try {
|
|
230
|
-
const content = readTemplate("ProtectedRoute.tsx.template", PROTECTED_ROUTE_FALLBACK);
|
|
231
|
-
fs_1.default.writeFileSync(protectedFile, content);
|
|
232
|
-
result.protected = path_1.default.relative(cwd, protectedFile);
|
|
233
|
-
}
|
|
234
|
-
catch (err) {
|
|
235
|
-
console.warn(chalk_1.default.yellow(` Warning: Could not scaffold ProtectedRoute: ${err.message}`));
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
// Update App.tsx if it exists and user wants it
|
|
239
|
-
if (setup.updateApp) {
|
|
240
|
-
const appPath = path_1.default.join(cwd, "src", "App.tsx");
|
|
241
|
-
if (fs_1.default.existsSync(appPath)) {
|
|
242
|
-
try {
|
|
243
|
-
updateAppWithAuth(appPath);
|
|
244
|
-
}
|
|
245
|
-
catch (err) {
|
|
246
|
-
console.warn(chalk_1.default.yellow(` Warning: Could not update App.tsx: ${err.message}`));
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
return result;
|
|
251
|
-
}
|
|
252
|
-
/**
|
|
253
|
-
* Update an existing App.tsx to wrap with AuthProvider.
|
|
254
|
-
*/
|
|
255
|
-
function updateAppWithAuth(appPath) {
|
|
256
|
-
let content = fs_1.default.readFileSync(appPath, "utf-8");
|
|
257
|
-
// Check if already has AuthProvider
|
|
258
|
-
if (content.includes("AuthProvider")) {
|
|
259
|
-
return; // Already updated
|
|
260
|
-
}
|
|
261
|
-
// Add import if not present
|
|
262
|
-
if (!content.includes("import { AuthProvider, useAuth }")) {
|
|
263
|
-
const importLine = "import { AuthProvider, useAuth } from '@/context/AuthContext';\n";
|
|
264
|
-
content = importLine + content;
|
|
265
|
-
}
|
|
266
|
-
// Check if this is a functional component that exports a component
|
|
267
|
-
// Look for "export default function" or "export default"
|
|
268
|
-
const exportMatch = content.match(/export\s+default\s+(function\s+\w+\s*\(|const\s+\w+\s*=|\(\)|=>)/);
|
|
269
|
-
if (exportMatch) {
|
|
270
|
-
// Find the return statement and wrap the JSX with AuthProvider
|
|
271
|
-
// This is a simple approach - find the outermost return/JSX in export
|
|
272
|
-
const returnMatch = content.match(/export\s+default\s+(?:function\s+\w+\s*\([^)]*\)\s*\{|\(.*?\)\s*=>\s*)[\s\S]*?return\s+/);
|
|
273
|
-
if (returnMatch) {
|
|
274
|
-
// Insert AuthProvider wrapper
|
|
275
|
-
// This is complex, so we'll do a simpler approach:
|
|
276
|
-
// Just add the import and let users wrap manually
|
|
277
|
-
// Or check if it's a simple functional component
|
|
278
|
-
if (content.includes("return (") || content.includes("return <")) {
|
|
279
|
-
// Try to wrap the main rendered content
|
|
280
|
-
// Find the JSX being returned and wrap it
|
|
281
|
-
const beforeReturn = content.substring(0, content.lastIndexOf("return"));
|
|
282
|
-
const afterReturn = content.substring(content.lastIndexOf("return"));
|
|
283
|
-
const wrappedReturn = afterReturn
|
|
284
|
-
.replace(/return\s+(<[^>]+>[\s\S]*)/m, 'return (\n <AuthProvider>\n $1\n </AuthProvider>\n )');
|
|
285
|
-
content = beforeReturn + wrappedReturn;
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
fs_1.default.writeFileSync(appPath, content);
|
|
119
|
+
printUsageHint(cfg);
|
|
290
120
|
}
|
|
291
121
|
// ─── Functions scaffolding ────────────────────────────────────────────────────
|
|
292
122
|
/**
|
|
@@ -943,7 +773,7 @@ function buildLibTs(cfg) {
|
|
|
943
773
|
` * const { user } = await auth.signIn("alice@example.com", "pass");`,
|
|
944
774
|
` *`,
|
|
945
775
|
` * // Google sign-in — add this to your root component / entry point:`,
|
|
946
|
-
` * await auth.
|
|
776
|
+
` * const result = await auth.handleAuthCallback(); // handles ?cfx_token= on return`,
|
|
947
777
|
` *`,
|
|
948
778
|
` * // On sign-in button click:`,
|
|
949
779
|
` * await auth.signInWithGateway("google");`,
|
|
@@ -987,20 +817,6 @@ function buildLibTs(cfg) {
|
|
|
987
817
|
lines.push(`/** Clefbase Auth — sign up, sign in, manage sessions. */`);
|
|
988
818
|
lines.push(`export const auth: Auth = getAuth(app);`);
|
|
989
819
|
lines.push("");
|
|
990
|
-
lines.push(`/**`);
|
|
991
|
-
lines.push(` * Call this once on every page load (before rendering your UI).`);
|
|
992
|
-
lines.push(` * Detects the ?cfx_token= param the gateway appends after OAuth,`);
|
|
993
|
-
lines.push(` * saves the session, and cleans the URL.`);
|
|
994
|
-
lines.push(` *`);
|
|
995
|
-
lines.push(` * @example`);
|
|
996
|
-
lines.push(` * // React / Next.js — in your root layout or _app.tsx:`);
|
|
997
|
-
lines.push(` * useEffect(() => { auth.handleGatewayCallback(); }, []);`);
|
|
998
|
-
lines.push(` *`);
|
|
999
|
-
lines.push(` * // Vanilla JS — at the top of your entry point:`);
|
|
1000
|
-
lines.push(` * await auth.handleGatewayCallback();`);
|
|
1001
|
-
lines.push(` */`);
|
|
1002
|
-
lines.push(`export { auth };`);
|
|
1003
|
-
lines.push("");
|
|
1004
820
|
}
|
|
1005
821
|
if (storage) {
|
|
1006
822
|
lines.push(`/** Clefbase Storage — upload and manage files. */`);
|
|
@@ -1143,16 +959,12 @@ function printUsageHint(cfg, authSetup) {
|
|
|
1143
959
|
console.log(chalk_1.default.cyan(` import { AuthProvider, useAuth } from "@/context/AuthContext";`));
|
|
1144
960
|
console.log(chalk_1.default.cyan(` const { user, signIn, signOut } = useAuth();`));
|
|
1145
961
|
}
|
|
1146
|
-
if (authSetup?.scaffoldModal) {
|
|
1147
|
-
console.log(chalk_1.default.cyan(` import AuthModal from "@/components/AuthModal";`));
|
|
1148
|
-
}
|
|
1149
962
|
if (authSetup?.scaffoldContext) {
|
|
1150
963
|
console.log(chalk_1.default.cyan(` import { EmailVerificationCard } from "clefbase/react";`));
|
|
1151
964
|
}
|
|
1152
965
|
if (!authSetup?.scaffoldContext) {
|
|
1153
966
|
console.log(chalk_1.default.cyan(` const { user } = await auth.signIn("email", "pass");`));
|
|
1154
|
-
console.log(chalk_1.default.cyan(` await auth.
|
|
1155
|
-
console.log(chalk_1.default.cyan(` await auth.handleGatewayCallback(); // call on every page load`));
|
|
967
|
+
console.log(chalk_1.default.cyan(` const result = await auth.handleAuthCallback(); // call on every page load`));
|
|
1156
968
|
}
|
|
1157
969
|
}
|
|
1158
970
|
if (cfg.services.storage) {
|
|
@@ -1180,430 +992,3 @@ function printUsageHint(cfg, authSetup) {
|
|
|
1180
992
|
}
|
|
1181
993
|
console.log();
|
|
1182
994
|
}
|
|
1183
|
-
// ─── Template Fallbacks ──────────────────────────────────────────────────
|
|
1184
|
-
const AUTH_CONTEXT_FALLBACK = `'use client';
|
|
1185
|
-
|
|
1186
|
-
import React, { createContext, useContext, useEffect, useState } from 'react';
|
|
1187
|
-
import type { AuthUser } from 'clefbase';
|
|
1188
|
-
import { getAuth, getDatabase } from 'clefbase';
|
|
1189
|
-
|
|
1190
|
-
/**
|
|
1191
|
-
* Base user data interface stored in the 'users' collection.
|
|
1192
|
-
* Extend this in your app for custom fields.
|
|
1193
|
-
*/
|
|
1194
|
-
export interface UserData {
|
|
1195
|
-
uid: string;
|
|
1196
|
-
email: string;
|
|
1197
|
-
displayName?: string;
|
|
1198
|
-
photoUrl?: string;
|
|
1199
|
-
createdAt?: string;
|
|
1200
|
-
[key: string]: unknown;
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
/**
|
|
1204
|
-
* Auth context for managing authentication state and user data in your React app.
|
|
1205
|
-
* Wraps the clefbase Auth SDK with React Context integration.
|
|
1206
|
-
*
|
|
1207
|
-
* Features:
|
|
1208
|
-
* - User state management with auth listener
|
|
1209
|
-
* - User profile data synced from database's 'users' collection
|
|
1210
|
-
* - Token persistence (handled by SDK)
|
|
1211
|
-
* - Auth modal state
|
|
1212
|
-
* - Error handling and loading states
|
|
1213
|
-
* - Auto-create user doc on first signup
|
|
1214
|
-
* - Auto-sync user data on signin
|
|
1215
|
-
*/
|
|
1216
|
-
|
|
1217
|
-
interface AuthContextType<T extends UserData = UserData> {
|
|
1218
|
-
/** Currently authenticated user, or null if not signed in */
|
|
1219
|
-
user: AuthUser | null;
|
|
1220
|
-
/** User profile data from the 'users' collection, or null if not signed in */
|
|
1221
|
-
userData: T | null;
|
|
1222
|
-
/** Authentication token */
|
|
1223
|
-
token: string | null;
|
|
1224
|
-
/** True while auth state is being restored or operations are pending */
|
|
1225
|
-
loading: boolean;
|
|
1226
|
-
/** Error message from last failed operation */
|
|
1227
|
-
error: string | null;
|
|
1228
|
-
/** True if auth modal is open */
|
|
1229
|
-
isAuthModalOpen: boolean;
|
|
1230
|
-
|
|
1231
|
-
// ── Auth modal management ────────────────────────────────────────────────
|
|
1232
|
-
|
|
1233
|
-
/**
|
|
1234
|
-
* Open the auth modal for sign in / sign up.
|
|
1235
|
-
* The modal at auth.cleforyx.com handles the actual authentication.
|
|
1236
|
-
* After success, auth state updates automatically.
|
|
1237
|
-
*/
|
|
1238
|
-
openAuthModal: () => void;
|
|
1239
|
-
|
|
1240
|
-
/**
|
|
1241
|
-
* Close the auth modal programmatically.
|
|
1242
|
-
*/
|
|
1243
|
-
closeAuthModal: () => void;
|
|
1244
|
-
|
|
1245
|
-
/**
|
|
1246
|
-
* Manually sync user data from the database.
|
|
1247
|
-
*/
|
|
1248
|
-
syncUserData: () => Promise<void>;
|
|
1249
|
-
|
|
1250
|
-
/**
|
|
1251
|
-
* Update user data in the database.
|
|
1252
|
-
*/
|
|
1253
|
-
updateUserData: (updates: Partial<Omit<T, 'uid'>>) => Promise<void>;
|
|
1254
|
-
|
|
1255
|
-
/**
|
|
1256
|
-
* Verify email with a verification code.
|
|
1257
|
-
*/
|
|
1258
|
-
verifyEmail: (code: string) => Promise<void>;
|
|
1259
|
-
|
|
1260
|
-
/**
|
|
1261
|
-
* Request a password reset email.
|
|
1262
|
-
*/
|
|
1263
|
-
sendPasswordResetEmail: (email: string) => Promise<void>;
|
|
1264
|
-
|
|
1265
|
-
/**
|
|
1266
|
-
* Sign out the current user and clear data.
|
|
1267
|
-
*/
|
|
1268
|
-
signOut: () => Promise<void>;
|
|
1269
|
-
|
|
1270
|
-
/**
|
|
1271
|
-
* Refresh user data from auth server.
|
|
1272
|
-
*/
|
|
1273
|
-
refreshUser: () => Promise<void>;
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
1277
|
-
|
|
1278
|
-
/**
|
|
1279
|
-
* Hook to use auth context in your components.
|
|
1280
|
-
*/
|
|
1281
|
-
export function useAuth<T extends UserData = UserData>(): AuthContextType<T> {
|
|
1282
|
-
const context = useContext(AuthContext);
|
|
1283
|
-
if (!context) {
|
|
1284
|
-
throw new Error('useAuth() must be used within <AuthProvider>');
|
|
1285
|
-
}
|
|
1286
|
-
return context as AuthContextType<T>;
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
/**
|
|
1290
|
-
* Provider component that wraps your app with authentication.
|
|
1291
|
-
*/
|
|
1292
|
-
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
1293
|
-
// State
|
|
1294
|
-
const [user, setUser] = useState<AuthUser | null>(null);
|
|
1295
|
-
const [userData, setUserData] = useState<UserData | null>(null);
|
|
1296
|
-
const [token, setToken] = useState<string | null>(null);
|
|
1297
|
-
const [loading, setLoading] = useState(true);
|
|
1298
|
-
const [error, setError] = useState<string | null>(null);
|
|
1299
|
-
const [isAuthModalOpen, setIsAuthModalOpen] = useState(false);
|
|
1300
|
-
|
|
1301
|
-
// Get auth and database services from clefbase
|
|
1302
|
-
const auth = getAuth();
|
|
1303
|
-
const db = getDatabase();
|
|
1304
|
-
|
|
1305
|
-
// ── Helper: sync user data from database ────────────────────────────────
|
|
1306
|
-
|
|
1307
|
-
const syncUserDataInternal = async (uid: string): Promise<void> => {
|
|
1308
|
-
try {
|
|
1309
|
-
const doc = await db.collection('users').doc(uid).get();
|
|
1310
|
-
if (doc) {
|
|
1311
|
-
setUserData(doc as UserData);
|
|
1312
|
-
} else {
|
|
1313
|
-
// First login - create user doc
|
|
1314
|
-
const authUser = auth.currentUser;
|
|
1315
|
-
if (authUser) {
|
|
1316
|
-
const newUserData: UserData = {
|
|
1317
|
-
uid: authUser.uid,
|
|
1318
|
-
email: authUser.email,
|
|
1319
|
-
displayName: authUser.displayName,
|
|
1320
|
-
photoUrl: authUser.photoUrl,
|
|
1321
|
-
createdAt: new Date().toISOString(),
|
|
1322
|
-
};
|
|
1323
|
-
await db.collection('users').doc(uid).set(newUserData);
|
|
1324
|
-
setUserData(newUserData);
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
} catch (err) {
|
|
1328
|
-
console.error('Error syncing user data:', err);
|
|
1329
|
-
}
|
|
1330
|
-
};
|
|
1331
|
-
|
|
1332
|
-
// ── Initialize on mount ────────────────────────────────────────────────
|
|
1333
|
-
|
|
1334
|
-
useEffect(() => {
|
|
1335
|
-
const initAuth = async () => {
|
|
1336
|
-
try {
|
|
1337
|
-
setLoading(true);
|
|
1338
|
-
|
|
1339
|
-
const currentUser = auth.currentUser;
|
|
1340
|
-
const currentToken = auth.currentToken;
|
|
1341
|
-
|
|
1342
|
-
setUser(currentUser);
|
|
1343
|
-
setToken(currentToken);
|
|
1344
|
-
|
|
1345
|
-
if (currentUser) {
|
|
1346
|
-
await syncUserDataInternal(currentUser.uid);
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
const unsubscribe = auth.onAuthStateChanged(async (newUser) => {
|
|
1350
|
-
setUser(newUser);
|
|
1351
|
-
if (newUser) {
|
|
1352
|
-
setToken(auth.currentToken);
|
|
1353
|
-
await syncUserDataInternal(newUser.uid);
|
|
1354
|
-
} else {
|
|
1355
|
-
setToken(null);
|
|
1356
|
-
setUserData(null);
|
|
1357
|
-
}
|
|
1358
|
-
});
|
|
1359
|
-
|
|
1360
|
-
setLoading(false);
|
|
1361
|
-
|
|
1362
|
-
return () => unsubscribe();
|
|
1363
|
-
} catch (err) {
|
|
1364
|
-
console.error('Auth initialization error:', err);
|
|
1365
|
-
setLoading(false);
|
|
1366
|
-
}
|
|
1367
|
-
};
|
|
1368
|
-
|
|
1369
|
-
initAuth();
|
|
1370
|
-
}, [auth, db]);
|
|
1371
|
-
|
|
1372
|
-
// ── Error handling helper ──────────────────────────────────────────────
|
|
1373
|
-
|
|
1374
|
-
const handleError = (err: unknown): string => {
|
|
1375
|
-
if (err instanceof Error) {
|
|
1376
|
-
return err.message;
|
|
1377
|
-
}
|
|
1378
|
-
if (typeof err === 'string') {
|
|
1379
|
-
return err;
|
|
1380
|
-
}
|
|
1381
|
-
return 'An unknown error occurred';
|
|
1382
|
-
};
|
|
1383
|
-
|
|
1384
|
-
// ── User data methods ──────────────────────────────────────────────────
|
|
1385
|
-
|
|
1386
|
-
const syncUserData = async (): Promise<void> => {
|
|
1387
|
-
if (!user) {
|
|
1388
|
-
setUserData(null);
|
|
1389
|
-
return;
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
try {
|
|
1393
|
-
await syncUserDataInternal(user.uid);
|
|
1394
|
-
} catch (err) {
|
|
1395
|
-
const message = handleError(err);
|
|
1396
|
-
setError(message);
|
|
1397
|
-
throw err;
|
|
1398
|
-
}
|
|
1399
|
-
};
|
|
1400
|
-
|
|
1401
|
-
const updateUserData = async (
|
|
1402
|
-
updates: Partial<Omit<UserData, 'uid'>>
|
|
1403
|
-
): Promise<void> => {
|
|
1404
|
-
if (!user) {
|
|
1405
|
-
throw new Error('User not authenticated');
|
|
1406
|
-
}
|
|
1407
|
-
|
|
1408
|
-
try {
|
|
1409
|
-
setError(null);
|
|
1410
|
-
setLoading(true);
|
|
1411
|
-
|
|
1412
|
-
const authUpdates: Record<string, unknown> = {};
|
|
1413
|
-
if ('displayName' in updates) {
|
|
1414
|
-
authUpdates.displayName = updates.displayName;
|
|
1415
|
-
}
|
|
1416
|
-
if ('photoUrl' in updates) {
|
|
1417
|
-
authUpdates.photoUrl = updates.photoUrl;
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
if (Object.keys(authUpdates).length > 0) {
|
|
1421
|
-
await auth.updateProfile(authUpdates);
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1424
|
-
await db.collection('users').doc(user.uid).update(updates);
|
|
1425
|
-
|
|
1426
|
-
setUserData((prev) => (prev ? { ...prev, ...updates } : null));
|
|
1427
|
-
} catch (err) {
|
|
1428
|
-
const message = handleError(err);
|
|
1429
|
-
setError(message);
|
|
1430
|
-
throw err;
|
|
1431
|
-
} finally {
|
|
1432
|
-
setLoading(false);
|
|
1433
|
-
}
|
|
1434
|
-
};
|
|
1435
|
-
|
|
1436
|
-
// ── Auth methods ───────────────────────────────────────────────────────
|
|
1437
|
-
|
|
1438
|
-
const signOut = async () => {
|
|
1439
|
-
try {
|
|
1440
|
-
setError(null);
|
|
1441
|
-
setLoading(true);
|
|
1442
|
-
await auth.signOut();
|
|
1443
|
-
setUser(null);
|
|
1444
|
-
setToken(null);
|
|
1445
|
-
setUserData(null);
|
|
1446
|
-
} catch (err) {
|
|
1447
|
-
const message = handleError(err);
|
|
1448
|
-
setError(message);
|
|
1449
|
-
throw err;
|
|
1450
|
-
} finally {
|
|
1451
|
-
setLoading(false);
|
|
1452
|
-
}
|
|
1453
|
-
};
|
|
1454
|
-
|
|
1455
|
-
const verifyEmail = async (code: string) => {
|
|
1456
|
-
try {
|
|
1457
|
-
setError(null);
|
|
1458
|
-
setLoading(true);
|
|
1459
|
-
const result = await auth.verifyEmail(code);
|
|
1460
|
-
setUser(result);
|
|
1461
|
-
} catch (err) {
|
|
1462
|
-
const message = handleError(err);
|
|
1463
|
-
setError(message);
|
|
1464
|
-
throw err;
|
|
1465
|
-
} finally {
|
|
1466
|
-
setLoading(false);
|
|
1467
|
-
}
|
|
1468
|
-
};
|
|
1469
|
-
|
|
1470
|
-
const sendPasswordResetEmail = async (email: string) => {
|
|
1471
|
-
try {
|
|
1472
|
-
setError(null);
|
|
1473
|
-
setLoading(true);
|
|
1474
|
-
await auth.sendPasswordResetEmail(email);
|
|
1475
|
-
} catch (err) {
|
|
1476
|
-
const message = handleError(err);
|
|
1477
|
-
setError(message);
|
|
1478
|
-
throw err;
|
|
1479
|
-
} finally {
|
|
1480
|
-
setLoading(false);
|
|
1481
|
-
}
|
|
1482
|
-
};
|
|
1483
|
-
|
|
1484
|
-
const refreshUser = async () => {
|
|
1485
|
-
try {
|
|
1486
|
-
setError(null);
|
|
1487
|
-
const updated = await auth.refreshCurrentUser();
|
|
1488
|
-
if (updated) setUser(updated);
|
|
1489
|
-
} catch (err) {
|
|
1490
|
-
const message = handleError(err);
|
|
1491
|
-
setError(message);
|
|
1492
|
-
throw err;
|
|
1493
|
-
}
|
|
1494
|
-
};
|
|
1495
|
-
|
|
1496
|
-
// ── Modal management ───────────────────────────────────────────────────
|
|
1497
|
-
|
|
1498
|
-
const openAuthModal = () => {
|
|
1499
|
-
setIsAuthModalOpen(true);
|
|
1500
|
-
};
|
|
1501
|
-
|
|
1502
|
-
const closeAuthModal = () => {
|
|
1503
|
-
setIsAuthModalOpen(false);
|
|
1504
|
-
};
|
|
1505
|
-
|
|
1506
|
-
// ── Provider value ────────────────────────────────────────────────────
|
|
1507
|
-
|
|
1508
|
-
const value: AuthContextType = {
|
|
1509
|
-
user,
|
|
1510
|
-
userData,
|
|
1511
|
-
token,
|
|
1512
|
-
loading,
|
|
1513
|
-
error,
|
|
1514
|
-
isAuthModalOpen,
|
|
1515
|
-
syncUserData,
|
|
1516
|
-
updateUserData,
|
|
1517
|
-
verifyEmail,
|
|
1518
|
-
sendPasswordResetEmail,
|
|
1519
|
-
signOut,
|
|
1520
|
-
refreshUser,
|
|
1521
|
-
openAuthModal,
|
|
1522
|
-
closeAuthModal,
|
|
1523
|
-
};
|
|
1524
|
-
|
|
1525
|
-
return (
|
|
1526
|
-
<AuthContext.Provider value={value}>
|
|
1527
|
-
{!loading && children}
|
|
1528
|
-
</AuthContext.Provider>
|
|
1529
|
-
);
|
|
1530
|
-
}
|
|
1531
|
-
|
|
1532
|
-
/**
|
|
1533
|
-
* CUSTOMIZATION GUIDE
|
|
1534
|
-
*
|
|
1535
|
-
* Extend UserData with custom fields, sync to database, and use useAuth<MyType>().
|
|
1536
|
-
*/
|
|
1537
|
-
`;
|
|
1538
|
-
const AUTH_MODAL_FALLBACK = `'use client';
|
|
1539
|
-
import React, { useEffect, CSSProperties } from 'react';
|
|
1540
|
-
import { useAuth } from '@/context/AuthContext';
|
|
1541
|
-
|
|
1542
|
-
export default function AuthModal({ isOpen, onClose, overlayStyle, cardStyle, showCloseButton = true, backdropDismiss = true }: any) {
|
|
1543
|
-
const { closeAuthModal } = useAuth();
|
|
1544
|
-
const projectId = typeof window !== 'undefined' && (window as any).__CLEFORYX_PROJECT_ID;
|
|
1545
|
-
|
|
1546
|
-
useEffect(() => {
|
|
1547
|
-
if (!isOpen) return;
|
|
1548
|
-
const handleMessage = (e: MessageEvent) => {
|
|
1549
|
-
if (e.data?.source === 'cleforyx-auth' && e.data?.type === 'auth_success') {
|
|
1550
|
-
closeAuthModal();
|
|
1551
|
-
onClose();
|
|
1552
|
-
}
|
|
1553
|
-
};
|
|
1554
|
-
window.addEventListener('message', handleMessage);
|
|
1555
|
-
return () => window.removeEventListener('message', handleMessage);
|
|
1556
|
-
}, [isOpen, closeAuthModal, onClose]);
|
|
1557
|
-
|
|
1558
|
-
if (!isOpen) return null;
|
|
1559
|
-
|
|
1560
|
-
const currentUrl = typeof window !== 'undefined' ? window.location.origin : '';
|
|
1561
|
-
const iframeUrl = \`https://auth.cleforyx.com/login?project=\${projectId}&redirect=\${encodeURIComponent(currentUrl)}&embed=popup\`;
|
|
1562
|
-
|
|
1563
|
-
const defaultOverlayStyle: CSSProperties = {
|
|
1564
|
-
position: 'fixed', inset: 0, backgroundColor: 'rgba(15, 23, 42, 0.6)',
|
|
1565
|
-
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
1566
|
-
zIndex: 9999, backdropFilter: 'blur(4px)', padding: '16px', boxSizing: 'border-box',
|
|
1567
|
-
};
|
|
1568
|
-
|
|
1569
|
-
const defaultCardStyle: CSSProperties = {
|
|
1570
|
-
backgroundColor: 'white', borderRadius: '20px',
|
|
1571
|
-
boxShadow: '0 24px 64px rgba(0, 0, 0, 0.2)',
|
|
1572
|
-
overflow: 'hidden', width: '100%', maxWidth: '460px', position: 'relative',
|
|
1573
|
-
};
|
|
1574
|
-
|
|
1575
|
-
const handleBackdropClick = (e: React.MouseEvent) => {
|
|
1576
|
-
if (backdropDismiss && e.target === e.currentTarget) onClose();
|
|
1577
|
-
};
|
|
1578
|
-
|
|
1579
|
-
return (
|
|
1580
|
-
<div style={{ ...defaultOverlayStyle, ...overlayStyle }} onClick={handleBackdropClick}>
|
|
1581
|
-
<div style={{ ...defaultCardStyle, ...cardStyle }}>
|
|
1582
|
-
{showCloseButton && (
|
|
1583
|
-
<button style={{ position: 'absolute', top: '14px', right: '16px', background: 'rgba(0,0,0,0.06)', border: 'none', width: '28px', height: '28px', borderRadius: '50%', cursor: 'pointer', zIndex: 1 }} onClick={onClose} aria-label="Close">✕</button>
|
|
1584
|
-
)}
|
|
1585
|
-
<iframe src={iframeUrl} style={{ width: '100%', height: '560px', border: 'none', display: 'block' }} title="Sign in" allow="identity-credentials-get" />
|
|
1586
|
-
</div>
|
|
1587
|
-
</div>
|
|
1588
|
-
);
|
|
1589
|
-
}`;
|
|
1590
|
-
const PROTECTED_ROUTE_FALLBACK = `'use client';
|
|
1591
|
-
import React from 'react';
|
|
1592
|
-
import { useAuth } from '@/context/AuthContext';
|
|
1593
|
-
|
|
1594
|
-
export function ProtectedRoute({ children, LoadingComponent, onUnauthorized }: any) {
|
|
1595
|
-
const { user, loading, openAuthModal } = useAuth();
|
|
1596
|
-
|
|
1597
|
-
if (loading) {
|
|
1598
|
-
if (LoadingComponent) return <LoadingComponent />;
|
|
1599
|
-
return <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100vh' }}>Loading...</div>;
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
|
-
if (!user) {
|
|
1603
|
-
onUnauthorized?.();
|
|
1604
|
-
openAuthModal();
|
|
1605
|
-
return null;
|
|
1606
|
-
}
|
|
1607
|
-
|
|
1608
|
-
return <>{children}</>;
|
|
1609
|
-
}`;
|