@instroc/auth 1.0.0

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,107 @@
1
+ interface AuthUser {
2
+ id: string;
3
+ email: string;
4
+ email_verified: boolean;
5
+ display_name: string | null;
6
+ avatar_url: string | null;
7
+ metadata: Record<string, unknown>;
8
+ created_at: string;
9
+ }
10
+ interface AuthSession {
11
+ access_token: string;
12
+ refresh_token: string;
13
+ expires_at: string;
14
+ }
15
+ interface AuthState {
16
+ user: AuthUser | null;
17
+ session: AuthSession | null;
18
+ loading: boolean;
19
+ error: string | null;
20
+ }
21
+ interface LoginCredentials {
22
+ email: string;
23
+ password: string;
24
+ }
25
+ interface SignupCredentials {
26
+ email: string;
27
+ password: string;
28
+ displayName?: string;
29
+ metadata?: Record<string, unknown>;
30
+ }
31
+ type OAuthProvider = "google" | "github" | "microsoft" | "facebook";
32
+ interface AuthConfig {
33
+ emailAuthEnabled: boolean;
34
+ googleAuthEnabled: boolean;
35
+ githubAuthEnabled: boolean;
36
+ microsoftAuthEnabled: boolean;
37
+ facebookAuthEnabled: boolean;
38
+ allowSignup: boolean;
39
+ requireEmailVerification: boolean;
40
+ }
41
+ interface VisibilityConfig {
42
+ projectName: string;
43
+ visibility: "public" | "private";
44
+ requireLogin: boolean;
45
+ logoUrl: string | null;
46
+ welcomeMessage: string | null;
47
+ }
48
+ interface AuthContextValue extends AuthState {
49
+ login: (credentials: LoginCredentials) => Promise<void>;
50
+ signup: (credentials: SignupCredentials) => Promise<void>;
51
+ logout: () => Promise<void>;
52
+ signInWithOAuth: (provider: OAuthProvider) => void;
53
+ verifyOTP: (email: string, code: string) => Promise<void>;
54
+ resendOTP: (email: string) => Promise<void>;
55
+ updateProfile: (data: UpdateProfileData) => Promise<void>;
56
+ refreshSession: () => Promise<void>;
57
+ forgotPassword: (email: string) => Promise<void>;
58
+ resetPassword: (token: string, newPassword: string) => Promise<void>;
59
+ setProjectId: (projectId: string) => void;
60
+ projectId: string | null;
61
+ authConfig: AuthConfig | null;
62
+ visibilityConfig: VisibilityConfig | null;
63
+ }
64
+ interface UpdateProfileData {
65
+ displayName?: string;
66
+ avatarUrl?: string;
67
+ metadata?: Record<string, unknown>;
68
+ }
69
+ interface AuthProviderProps {
70
+ children: React.ReactNode;
71
+ projectId?: string;
72
+ baseUrl?: string;
73
+ persistSession?: boolean;
74
+ onAuthStateChange?: (user: AuthUser | null) => void;
75
+ }
76
+ interface AuthResponse {
77
+ user: AuthUser;
78
+ session: AuthSession;
79
+ }
80
+ interface RefreshResponse {
81
+ access_token: string;
82
+ expires_at: string;
83
+ }
84
+
85
+ declare function AuthProvider({ children, projectId: initialProjectId, baseUrl, persistSession, onAuthStateChange, }: AuthProviderProps): JSX.Element;
86
+ declare function useAuthContext(): AuthContextValue;
87
+
88
+ declare function useAuth(): AuthContextValue;
89
+ declare function useUser(): {
90
+ user: AuthUser | null;
91
+ loading: boolean;
92
+ };
93
+ declare function useSession(): {
94
+ session: AuthSession | null;
95
+ loading: boolean;
96
+ };
97
+ declare function useAuthRequired(): {
98
+ user: null;
99
+ session: null;
100
+ loading: boolean;
101
+ } | {
102
+ user: AuthUser;
103
+ session: AuthSession | null;
104
+ loading: boolean;
105
+ };
106
+
107
+ export { type AuthConfig, type AuthContextValue, AuthProvider, type AuthProviderProps, type AuthResponse, type AuthSession, type AuthState, type AuthUser, type LoginCredentials, type OAuthProvider, type RefreshResponse, type SignupCredentials, type UpdateProfileData, type VisibilityConfig, useAuth, useAuthContext, useAuthRequired, useSession, useUser };
package/dist/index.js ADDED
@@ -0,0 +1,536 @@
1
+ // src/provider.tsx
2
+ import {
3
+ createContext,
4
+ useContext,
5
+ useState,
6
+ useCallback,
7
+ useEffect,
8
+ useRef
9
+ } from "react";
10
+ import { jsx } from "react/jsx-runtime";
11
+ var AuthContext = createContext(null);
12
+ var STORAGE_KEY = "instroc_auth_session";
13
+ function AuthProvider({
14
+ children,
15
+ projectId: initialProjectId,
16
+ baseUrl = "/api/baas",
17
+ persistSession = true,
18
+ onAuthStateChange
19
+ }) {
20
+ const [projectId, setProjectId] = useState(
21
+ initialProjectId || null
22
+ );
23
+ const [user, setUser] = useState(null);
24
+ const [session, setSession] = useState(null);
25
+ const [loading, setLoading] = useState(true);
26
+ const [error, setError] = useState(null);
27
+ const [authConfig, setAuthConfig] = useState(null);
28
+ const [visibilityConfig, setVisibilityConfig] = useState(null);
29
+ const refreshTimeoutRef = useRef(null);
30
+ const buildUrl = useCallback(
31
+ (endpoint) => {
32
+ if (!projectId) {
33
+ throw new Error("Project ID is required");
34
+ }
35
+ return `${baseUrl}/${projectId}/auth/${endpoint}`;
36
+ },
37
+ [projectId, baseUrl]
38
+ );
39
+ const saveSession = useCallback(
40
+ (sessionData) => {
41
+ if (persistSession && typeof window !== "undefined") {
42
+ if (sessionData) {
43
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(sessionData));
44
+ } else {
45
+ localStorage.removeItem(STORAGE_KEY);
46
+ }
47
+ }
48
+ },
49
+ [persistSession]
50
+ );
51
+ const loadSession = useCallback(() => {
52
+ if (persistSession && typeof window !== "undefined") {
53
+ const stored = localStorage.getItem(STORAGE_KEY);
54
+ if (stored) {
55
+ try {
56
+ return JSON.parse(stored);
57
+ } catch {
58
+ localStorage.removeItem(STORAGE_KEY);
59
+ }
60
+ }
61
+ }
62
+ return null;
63
+ }, [persistSession]);
64
+ const scheduleRefresh = useCallback(
65
+ (expiresAt) => {
66
+ if (refreshTimeoutRef.current) {
67
+ clearTimeout(refreshTimeoutRef.current);
68
+ }
69
+ const expiresTime = new Date(expiresAt).getTime();
70
+ const now = Date.now();
71
+ const refreshIn = expiresTime - now - 5 * 60 * 1e3;
72
+ if (refreshIn > 0) {
73
+ refreshTimeoutRef.current = setTimeout(async () => {
74
+ try {
75
+ await refreshSession();
76
+ } catch (err) {
77
+ console.error("Token refresh failed:", err);
78
+ setUser(null);
79
+ setSession(null);
80
+ saveSession(null);
81
+ }
82
+ }, refreshIn);
83
+ }
84
+ },
85
+ [saveSession]
86
+ );
87
+ const updateAuthState = useCallback(
88
+ (newUser, newSession) => {
89
+ setUser(newUser);
90
+ setSession(newSession);
91
+ saveSession(newSession);
92
+ if (newSession) {
93
+ scheduleRefresh(newSession.expires_at);
94
+ }
95
+ if (onAuthStateChange) {
96
+ onAuthStateChange(newUser);
97
+ }
98
+ },
99
+ [saveSession, scheduleRefresh, onAuthStateChange]
100
+ );
101
+ const fetchUser = useCallback(async () => {
102
+ const storedSession = loadSession();
103
+ if (!storedSession || !projectId) {
104
+ setLoading(false);
105
+ return;
106
+ }
107
+ try {
108
+ const response = await fetch(buildUrl("me"), {
109
+ headers: {
110
+ Authorization: `Bearer ${storedSession.access_token}`
111
+ }
112
+ });
113
+ if (response.ok) {
114
+ const data = await response.json();
115
+ updateAuthState(data.user, storedSession);
116
+ } else if (response.status === 401) {
117
+ try {
118
+ const refreshResponse = await fetch(buildUrl("refresh"), {
119
+ method: "POST",
120
+ headers: { "Content-Type": "application/json" },
121
+ body: JSON.stringify({ refreshToken: storedSession.refresh_token })
122
+ });
123
+ if (refreshResponse.ok) {
124
+ const refreshData = await refreshResponse.json();
125
+ const newSession = {
126
+ ...storedSession,
127
+ access_token: refreshData.access_token,
128
+ expires_at: refreshData.expires_at
129
+ };
130
+ const userResponse = await fetch(buildUrl("me"), {
131
+ headers: {
132
+ Authorization: `Bearer ${newSession.access_token}`
133
+ }
134
+ });
135
+ if (userResponse.ok) {
136
+ const userData = await userResponse.json();
137
+ updateAuthState(userData.user, newSession);
138
+ } else {
139
+ updateAuthState(null, null);
140
+ }
141
+ } else {
142
+ updateAuthState(null, null);
143
+ }
144
+ } catch {
145
+ updateAuthState(null, null);
146
+ }
147
+ } else {
148
+ updateAuthState(null, null);
149
+ }
150
+ } catch (err) {
151
+ console.error("Failed to fetch user:", err);
152
+ updateAuthState(null, null);
153
+ } finally {
154
+ setLoading(false);
155
+ }
156
+ }, [projectId, loadSession, buildUrl, updateAuthState]);
157
+ useEffect(() => {
158
+ if (projectId) {
159
+ fetchUser();
160
+ } else {
161
+ setLoading(false);
162
+ }
163
+ return () => {
164
+ if (refreshTimeoutRef.current) {
165
+ clearTimeout(refreshTimeoutRef.current);
166
+ }
167
+ };
168
+ }, [projectId, fetchUser]);
169
+ const login = useCallback(
170
+ async (credentials) => {
171
+ setError(null);
172
+ setLoading(true);
173
+ try {
174
+ const response = await fetch(buildUrl("login"), {
175
+ method: "POST",
176
+ headers: { "Content-Type": "application/json" },
177
+ body: JSON.stringify(credentials)
178
+ });
179
+ const data = await response.json();
180
+ if (!response.ok) {
181
+ throw new Error(data.error || "Login failed");
182
+ }
183
+ const authResponse = data;
184
+ updateAuthState(authResponse.user, authResponse.session);
185
+ } catch (err) {
186
+ const message = err instanceof Error ? err.message : "Login failed";
187
+ setError(message);
188
+ throw err;
189
+ } finally {
190
+ setLoading(false);
191
+ }
192
+ },
193
+ [buildUrl, updateAuthState]
194
+ );
195
+ const signup = useCallback(
196
+ async (credentials) => {
197
+ setError(null);
198
+ setLoading(true);
199
+ try {
200
+ const response = await fetch(buildUrl("signup"), {
201
+ method: "POST",
202
+ headers: { "Content-Type": "application/json" },
203
+ body: JSON.stringify(credentials)
204
+ });
205
+ const data = await response.json();
206
+ if (!response.ok) {
207
+ throw new Error(data.error || "Signup failed");
208
+ }
209
+ const authResponse = data;
210
+ if (authResponse.session.access_token) {
211
+ updateAuthState(authResponse.user, authResponse.session);
212
+ } else {
213
+ setUser(authResponse.user);
214
+ }
215
+ } catch (err) {
216
+ const message = err instanceof Error ? err.message : "Signup failed";
217
+ setError(message);
218
+ throw err;
219
+ } finally {
220
+ setLoading(false);
221
+ }
222
+ },
223
+ [buildUrl, updateAuthState]
224
+ );
225
+ const logout = useCallback(async () => {
226
+ try {
227
+ if (session) {
228
+ await fetch(buildUrl("logout"), {
229
+ method: "POST",
230
+ headers: {
231
+ Authorization: `Bearer ${session.access_token}`
232
+ }
233
+ });
234
+ }
235
+ } catch (err) {
236
+ console.error("Logout error:", err);
237
+ } finally {
238
+ updateAuthState(null, null);
239
+ if (refreshTimeoutRef.current) {
240
+ clearTimeout(refreshTimeoutRef.current);
241
+ }
242
+ }
243
+ }, [session, buildUrl, updateAuthState]);
244
+ const updateProfile = useCallback(
245
+ async (data) => {
246
+ if (!session) {
247
+ throw new Error("Not authenticated");
248
+ }
249
+ setError(null);
250
+ try {
251
+ const response = await fetch(buildUrl("me"), {
252
+ method: "PATCH",
253
+ headers: {
254
+ "Content-Type": "application/json",
255
+ Authorization: `Bearer ${session.access_token}`
256
+ },
257
+ body: JSON.stringify(data)
258
+ });
259
+ const result = await response.json();
260
+ if (!response.ok) {
261
+ throw new Error(result.error || "Update failed");
262
+ }
263
+ setUser(result.user);
264
+ } catch (err) {
265
+ const message = err instanceof Error ? err.message : "Update failed";
266
+ setError(message);
267
+ throw err;
268
+ }
269
+ },
270
+ [session, buildUrl]
271
+ );
272
+ const refreshSession = useCallback(async () => {
273
+ if (!session) {
274
+ throw new Error("No session to refresh");
275
+ }
276
+ const response = await fetch(buildUrl("refresh"), {
277
+ method: "POST",
278
+ headers: { "Content-Type": "application/json" },
279
+ body: JSON.stringify({ refreshToken: session.refresh_token })
280
+ });
281
+ const data = await response.json();
282
+ if (!response.ok) {
283
+ throw new Error(data.error || "Refresh failed");
284
+ }
285
+ const refreshData = data;
286
+ const newSession = {
287
+ ...session,
288
+ access_token: refreshData.access_token,
289
+ expires_at: refreshData.expires_at
290
+ };
291
+ updateAuthState(user, newSession);
292
+ }, [session, user, buildUrl, updateAuthState]);
293
+ const forgotPassword = useCallback(
294
+ async (email) => {
295
+ setError(null);
296
+ try {
297
+ const response = await fetch(buildUrl("forgot-password"), {
298
+ method: "POST",
299
+ headers: { "Content-Type": "application/json" },
300
+ body: JSON.stringify({ email })
301
+ });
302
+ const data = await response.json();
303
+ if (!response.ok) {
304
+ throw new Error(data.error || "Request failed");
305
+ }
306
+ } catch (err) {
307
+ const message = err instanceof Error ? err.message : "Request failed";
308
+ setError(message);
309
+ throw err;
310
+ }
311
+ },
312
+ [buildUrl]
313
+ );
314
+ const resetPassword = useCallback(
315
+ async (token, newPassword) => {
316
+ setError(null);
317
+ try {
318
+ const response = await fetch(buildUrl("reset-password"), {
319
+ method: "POST",
320
+ headers: { "Content-Type": "application/json" },
321
+ body: JSON.stringify({ token, password: newPassword })
322
+ });
323
+ const data = await response.json();
324
+ if (!response.ok) {
325
+ throw new Error(data.error || "Reset failed");
326
+ }
327
+ } catch (err) {
328
+ const message = err instanceof Error ? err.message : "Reset failed";
329
+ setError(message);
330
+ throw err;
331
+ }
332
+ },
333
+ [buildUrl]
334
+ );
335
+ const signInWithOAuth = useCallback(
336
+ (provider) => {
337
+ if (!projectId) {
338
+ setError("Project ID is required");
339
+ return;
340
+ }
341
+ const currentUrl = window.location.origin + window.location.pathname;
342
+ const oauthUrl = `${baseUrl}/${projectId}/auth/oauth/${provider}?redirect_uri=${encodeURIComponent(currentUrl)}`;
343
+ window.location.href = oauthUrl;
344
+ },
345
+ [projectId, baseUrl]
346
+ );
347
+ const verifyOTP = useCallback(
348
+ async (email, code) => {
349
+ setError(null);
350
+ setLoading(true);
351
+ try {
352
+ const response = await fetch(buildUrl("verify-email"), {
353
+ method: "POST",
354
+ headers: { "Content-Type": "application/json" },
355
+ body: JSON.stringify({ email, otp: code })
356
+ });
357
+ const data = await response.json();
358
+ if (!response.ok) {
359
+ throw new Error(data.error || "Verification failed");
360
+ }
361
+ if (data.session) {
362
+ updateAuthState(data.user, data.session);
363
+ }
364
+ } catch (err) {
365
+ const message = err instanceof Error ? err.message : "Verification failed";
366
+ setError(message);
367
+ throw err;
368
+ } finally {
369
+ setLoading(false);
370
+ }
371
+ },
372
+ [buildUrl, updateAuthState]
373
+ );
374
+ const resendOTP = useCallback(
375
+ async (email) => {
376
+ setError(null);
377
+ try {
378
+ const response = await fetch(buildUrl("resend-verification"), {
379
+ method: "POST",
380
+ headers: { "Content-Type": "application/json" },
381
+ body: JSON.stringify({ email })
382
+ });
383
+ const data = await response.json();
384
+ if (!response.ok) {
385
+ throw new Error(data.error || "Failed to resend code");
386
+ }
387
+ } catch (err) {
388
+ const message = err instanceof Error ? err.message : "Failed to resend code";
389
+ setError(message);
390
+ throw err;
391
+ }
392
+ },
393
+ [buildUrl]
394
+ );
395
+ const fetchAuthConfig = useCallback(async () => {
396
+ if (!projectId)
397
+ return;
398
+ try {
399
+ const response = await fetch(
400
+ `${baseUrl}/${projectId}/auth/config`
401
+ );
402
+ if (response.ok) {
403
+ const data = await response.json();
404
+ setAuthConfig(data.config || null);
405
+ setVisibilityConfig(
406
+ data.visibility || {
407
+ projectName: "App",
408
+ visibility: "public",
409
+ requireLogin: false,
410
+ logoUrl: null,
411
+ welcomeMessage: null
412
+ }
413
+ );
414
+ }
415
+ } catch (err) {
416
+ console.error("Failed to fetch auth config:", err);
417
+ }
418
+ }, [projectId, baseUrl]);
419
+ const parseOAuthCallback = useCallback(() => {
420
+ if (typeof window === "undefined")
421
+ return;
422
+ const hash = window.location.hash;
423
+ if (hash && hash.startsWith("#auth=")) {
424
+ try {
425
+ const authData = decodeURIComponent(hash.substring(6));
426
+ const tokenData = JSON.parse(authData);
427
+ if (tokenData.access_token && tokenData.refresh_token) {
428
+ const newSession = {
429
+ access_token: tokenData.access_token,
430
+ refresh_token: tokenData.refresh_token,
431
+ expires_at: tokenData.expires_at
432
+ };
433
+ saveSession(newSession);
434
+ setSession(newSession);
435
+ window.history.replaceState({}, "", window.location.pathname);
436
+ return true;
437
+ }
438
+ } catch {
439
+ }
440
+ }
441
+ const params = new URLSearchParams(window.location.search);
442
+ const verifyEmailToken = params.get("verify_email");
443
+ if (verifyEmailToken && projectId) {
444
+ fetch(buildUrl("verify-email"), {
445
+ method: "POST",
446
+ headers: { "Content-Type": "application/json" },
447
+ body: JSON.stringify({ token: verifyEmailToken })
448
+ }).then((res) => res.json()).then(() => {
449
+ window.history.replaceState({}, "", window.location.pathname);
450
+ }).catch(() => {
451
+ window.history.replaceState({}, "", window.location.pathname);
452
+ });
453
+ return true;
454
+ }
455
+ const accessToken = params.get("access_token");
456
+ const refreshToken = params.get("refresh_token");
457
+ const expiresAt = params.get("expires_at");
458
+ if (accessToken && refreshToken && expiresAt) {
459
+ const newSession = {
460
+ access_token: accessToken,
461
+ refresh_token: refreshToken,
462
+ expires_at: expiresAt
463
+ };
464
+ saveSession(newSession);
465
+ setSession(newSession);
466
+ window.history.replaceState({}, "", window.location.pathname);
467
+ return true;
468
+ }
469
+ return false;
470
+ }, [projectId, buildUrl, saveSession]);
471
+ useEffect(() => {
472
+ fetchAuthConfig();
473
+ }, [fetchAuthConfig]);
474
+ useEffect(() => {
475
+ parseOAuthCallback();
476
+ }, [parseOAuthCallback]);
477
+ const value = {
478
+ user,
479
+ session,
480
+ loading,
481
+ error,
482
+ login,
483
+ signup,
484
+ logout,
485
+ signInWithOAuth,
486
+ verifyOTP,
487
+ resendOTP,
488
+ updateProfile,
489
+ refreshSession,
490
+ forgotPassword,
491
+ resetPassword,
492
+ setProjectId,
493
+ projectId,
494
+ authConfig,
495
+ visibilityConfig
496
+ };
497
+ return /* @__PURE__ */ jsx(AuthContext.Provider, { value, children });
498
+ }
499
+ function useAuthContext() {
500
+ const context = useContext(AuthContext);
501
+ if (!context) {
502
+ throw new Error("useAuthContext must be used within an AuthProvider");
503
+ }
504
+ return context;
505
+ }
506
+
507
+ // src/use-auth.ts
508
+ function useAuth() {
509
+ return useAuthContext();
510
+ }
511
+ function useUser() {
512
+ const { user, loading } = useAuthContext();
513
+ return { user, loading };
514
+ }
515
+ function useSession() {
516
+ const { session, loading } = useAuthContext();
517
+ return { session, loading };
518
+ }
519
+ function useAuthRequired() {
520
+ const { user, loading, session } = useAuthContext();
521
+ if (loading) {
522
+ return { user: null, session: null, loading: true };
523
+ }
524
+ if (!user) {
525
+ throw new Error("Authentication required");
526
+ }
527
+ return { user, session, loading: false };
528
+ }
529
+ export {
530
+ AuthProvider,
531
+ useAuth,
532
+ useAuthContext,
533
+ useAuthRequired,
534
+ useSession,
535
+ useUser
536
+ };
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@instroc/auth",
3
+ "version": "1.0.0",
4
+ "description": "Authentication hooks for Instroc Cloud — useAuth, useUser, AuthProvider",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": ["dist"],
15
+ "scripts": {
16
+ "build": "tsup",
17
+ "dev": "tsup --watch"
18
+ },
19
+ "peerDependencies": {
20
+ "react": "^18.0.0"
21
+ },
22
+ "devDependencies": {
23
+ "react": "^18.2.0",
24
+ "@types/react": "^18.2.0",
25
+ "tsup": "^8.0.0",
26
+ "typescript": "^5.3.0"
27
+ },
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/Olaide-EO/robobuild",
32
+ "directory": "packages/auth"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ }
37
+ }