@mspkapps/auth-client 0.1.22 → 0.1.23
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/README.md +242 -1972
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,2106 +1,376 @@
|
|
|
1
|
-
# Auth
|
|
2
|
-
|
|
3
|
-
A beginner-friendly guide to implementing authentication using the `@mspkapps/auth-client` package with **React Web** and **React Native**.
|
|
4
|
-
|
|
5
|
-
## Table of Contents
|
|
6
|
-
- [Overview](#overview)
|
|
7
|
-
- [Installation](#installation)
|
|
8
|
-
- [React Web](#react-web-setup)
|
|
9
|
-
- [React Native](#react-native-setup)
|
|
10
|
-
- [Setup](#setup)
|
|
11
|
-
- [React Web](#react-web-1)
|
|
12
|
-
- [React Native](#react-native-1)
|
|
13
|
-
- [Available Functions](#available-functions)
|
|
14
|
-
- [Implementation Examples](#implementation-examples)
|
|
15
|
-
- [React Web Examples](#react-web-examples)
|
|
16
|
-
- [React Native Examples](#react-native-examples)
|
|
17
|
-
- [Common Patterns](#common-patterns)
|
|
18
|
-
- [Troubleshooting](#troubleshooting)
|
|
1
|
+
# MSPK™ Auth Client (`@mspkapps/auth-client`)
|
|
19
2
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
## Overview
|
|
23
|
-
|
|
24
|
-
This demo project showcases how to integrate the `@mspkapps/auth-client` authentication package in a React application. It includes:
|
|
3
|
+
Simple backend SDK for the MSPK™ Auth Platform.
|
|
4
|
+
Use it from your server code to handle login, register, Google auth, password flows, and profile operations with a few lines.
|
|
25
5
|
|
|
26
|
-
- ✅ Email/
|
|
27
|
-
- ✅ Google
|
|
28
|
-
- ✅
|
|
29
|
-
- ✅
|
|
30
|
-
- ✅
|
|
31
|
-
- ✅
|
|
32
|
-
- ✅
|
|
6
|
+
- ✅ Email/password login & register
|
|
7
|
+
- ✅ Google OAuth login
|
|
8
|
+
- ✅ Password reset & change flows
|
|
9
|
+
- ✅ Email verification / resend flows
|
|
10
|
+
- ✅ Account delete
|
|
11
|
+
- ✅ Profile read & update
|
|
12
|
+
- ✅ Simple singleton API: `authclient.init(...)` then `authclient.login(...)`
|
|
33
13
|
|
|
34
14
|
---
|
|
35
15
|
|
|
36
16
|
## Installation
|
|
37
17
|
|
|
38
|
-
### React Web Setup
|
|
39
|
-
|
|
40
|
-
#### 1. Create a New React Project (with Vite)
|
|
41
|
-
|
|
42
|
-
```bash
|
|
43
|
-
npm create vite@latest my-auth-app -- --template react
|
|
44
|
-
cd my-auth-app
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
#### 2. Install Required Dependencies
|
|
48
|
-
|
|
49
18
|
```bash
|
|
50
|
-
npm install @mspkapps/auth-client
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
### React Native Setup
|
|
54
|
-
|
|
55
|
-
#### 1. Create a New React Native Project
|
|
56
|
-
|
|
57
|
-
```bash
|
|
58
|
-
# Using Expo (recommended for beginners)
|
|
59
|
-
npx create-expo-app my-auth-app
|
|
60
|
-
cd my-auth-app
|
|
61
|
-
|
|
62
|
-
# Or using React Native CLI
|
|
63
|
-
npx react-native init my-auth-app
|
|
64
|
-
cd my-auth-app
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
#### 2. Install Required Dependencies
|
|
68
|
-
|
|
69
|
-
```bash
|
|
70
|
-
# For Expo
|
|
71
|
-
npx expo install @mspkapps/auth-client @react-native-google-signin/google-signin react-native-async-storage
|
|
72
|
-
|
|
73
|
-
# For React Native CLI
|
|
74
|
-
npm install @mspkapps/auth-client @react-native-google-signin/google-signin react-native-async-storage
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
#### 3. Configure Google Sign-In (Expo)
|
|
78
|
-
|
|
79
|
-
Add to your `app.json`:
|
|
80
|
-
|
|
81
|
-
```json
|
|
82
|
-
{
|
|
83
|
-
"expo": {
|
|
84
|
-
"plugins": [
|
|
85
|
-
[
|
|
86
|
-
"@react-native-google-signin/google-signin",
|
|
87
|
-
{
|
|
88
|
-
"androidClientId": "YOUR_ANDROID_CLIENT_ID.apps.googleusercontent.com",
|
|
89
|
-
"iosClientId": "YOUR_IOS_CLIENT_ID.apps.googleusercontent.com"
|
|
90
|
-
}
|
|
91
|
-
]
|
|
92
|
-
]
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
Then run:
|
|
98
|
-
```bash
|
|
99
|
-
npx expo prebuild
|
|
19
|
+
npm install @mspkapps/auth-client
|
|
20
|
+
# or
|
|
21
|
+
yarn add @mspkapps/auth-client
|
|
100
22
|
```
|
|
101
23
|
|
|
102
24
|
---
|
|
103
25
|
|
|
104
|
-
##
|
|
105
|
-
|
|
106
|
-
### React Web
|
|
107
|
-
|
|
108
|
-
#### Step 1: Create Authentication Context
|
|
109
|
-
|
|
110
|
-
Create `src/AuthContext.jsx` to manage authentication state across your app:
|
|
111
|
-
|
|
112
|
-
```jsx
|
|
113
|
-
import React, { createContext, useContext, useCallback, useState, useMemo } from 'react';
|
|
114
|
-
import { AuthClient } from '@mspkapps/auth-client';
|
|
115
|
-
|
|
116
|
-
// Replace these with your actual API keys
|
|
117
|
-
const PUBLIC_KEY = 'your_public_api_key_here';
|
|
118
|
-
const API_SECRET = 'your_api_secret_here';
|
|
119
|
-
|
|
120
|
-
// Initialize the auth client
|
|
121
|
-
// Note: baseUrl is already configured in the package, no need to override it
|
|
122
|
-
const authClient = AuthClient.create(PUBLIC_KEY, API_SECRET, {
|
|
123
|
-
keyInPath: true, // Use API key in URL path
|
|
124
|
-
storage: null, // Keep token in memory only (more secure)
|
|
125
|
-
fetch: (input, init = {}) => {
|
|
126
|
-
// Add required headers to all requests
|
|
127
|
-
const headers = new Headers(init.headers || {});
|
|
128
|
-
if (!headers.has('X-API-Key')) headers.set('X-API-Key', PUBLIC_KEY);
|
|
129
|
-
if (!headers.has('X-API-Secret')) headers.set('X-API-Secret', API_SECRET);
|
|
130
|
-
|
|
131
|
-
// Convert token format from "UserToken" to "Bearer" (if needed by your server)
|
|
132
|
-
if (headers.has('Authorization')) {
|
|
133
|
-
const authHeader = headers.get('Authorization');
|
|
134
|
-
if (authHeader.startsWith('UserToken ')) {
|
|
135
|
-
headers.set('Authorization', 'Bearer ' + authHeader.substring(10));
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return window.fetch(input, { ...init, headers });
|
|
140
|
-
},
|
|
141
|
-
});
|
|
26
|
+
## Backend Usage (Recommended)
|
|
142
27
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
export function AuthProvider({ children }) {
|
|
146
|
-
const [user, setUser] = useState(null);
|
|
147
|
-
const [token, setToken] = useState(null);
|
|
148
|
-
const [loading, setLoading] = useState(false);
|
|
149
|
-
const [error, setError] = useState(null);
|
|
150
|
-
|
|
151
|
-
// Email/Password Login
|
|
152
|
-
const login = useCallback(async ({ email, password }) => {
|
|
153
|
-
setError(null);
|
|
154
|
-
setLoading(true);
|
|
155
|
-
try {
|
|
156
|
-
const res = await authClient.login({ email, password });
|
|
157
|
-
const token = res?.data?.access_token;
|
|
158
|
-
if (token) {
|
|
159
|
-
authClient.setToken(token);
|
|
160
|
-
}
|
|
161
|
-
setUser(res?.data?.user || null);
|
|
162
|
-
setToken(authClient.token);
|
|
163
|
-
return res?.data?.user || null;
|
|
164
|
-
} catch (err) {
|
|
165
|
-
setError(err);
|
|
166
|
-
throw err;
|
|
167
|
-
} finally {
|
|
168
|
-
setLoading(false);
|
|
169
|
-
}
|
|
170
|
-
}, []);
|
|
171
|
-
|
|
172
|
-
// Google Sign-In
|
|
173
|
-
const googleLogin = useCallback(async (idToken) => {
|
|
174
|
-
setError(null);
|
|
175
|
-
setLoading(true);
|
|
176
|
-
try {
|
|
177
|
-
const res = await authClient.googleAuth({ id_token: idToken });
|
|
178
|
-
const token = res?.data?.user_token || res?.data?.access_token;
|
|
179
|
-
if (token) {
|
|
180
|
-
authClient.setToken(token);
|
|
181
|
-
}
|
|
182
|
-
setUser(res?.data?.user || null);
|
|
183
|
-
setToken(authClient.token || token);
|
|
184
|
-
return { user: res?.data?.user, isNewUser: res?.data?.is_new_user };
|
|
185
|
-
} catch (err) {
|
|
186
|
-
setError(err);
|
|
187
|
-
throw err;
|
|
188
|
-
} finally {
|
|
189
|
-
setLoading(false);
|
|
190
|
-
}
|
|
191
|
-
}, []);
|
|
192
|
-
|
|
193
|
-
// Logout
|
|
194
|
-
const logout = useCallback(() => {
|
|
195
|
-
setUser(null);
|
|
196
|
-
setToken(null);
|
|
197
|
-
authClient.token = null;
|
|
198
|
-
}, []);
|
|
199
|
-
|
|
200
|
-
// Refresh User Profile
|
|
201
|
-
const refreshProfile = useCallback(async () => {
|
|
202
|
-
if (!token) return;
|
|
203
|
-
setLoading(true);
|
|
204
|
-
setError(null);
|
|
205
|
-
try {
|
|
206
|
-
const res = await authClient.getProfile();
|
|
207
|
-
setUser(res.data.user);
|
|
208
|
-
return res.data.user;
|
|
209
|
-
} catch (err) {
|
|
210
|
-
setError(err);
|
|
211
|
-
throw err;
|
|
212
|
-
} finally {
|
|
213
|
-
setLoading(false);
|
|
214
|
-
}
|
|
215
|
-
}, [token]);
|
|
216
|
-
|
|
217
|
-
const value = useMemo(
|
|
218
|
-
() => ({ authClient, user, token, login, googleLogin, logout, refreshProfile, loading, error }),
|
|
219
|
-
[user, token, login, googleLogin, logout, refreshProfile, loading, error]
|
|
220
|
-
);
|
|
221
|
-
|
|
222
|
-
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
|
223
|
-
}
|
|
28
|
+
Most apps should only use this package on the **backend** (Node/Express, NestJS, etc.).
|
|
224
29
|
|
|
225
|
-
|
|
226
|
-
export function useAuth() {
|
|
227
|
-
const ctx = useContext(AuthContext);
|
|
228
|
-
if (!ctx) throw new Error('useAuth must be used within AuthProvider');
|
|
229
|
-
return ctx;
|
|
230
|
-
}
|
|
231
|
-
```
|
|
30
|
+
### 1. Initialize once at startup
|
|
232
31
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
Create `src/AuthContext.js` with AsyncStorage support for React Native:
|
|
238
|
-
|
|
239
|
-
```jsx
|
|
240
|
-
import React, { createContext, useContext, useCallback, useState, useMemo } from 'react';
|
|
241
|
-
import { AuthClient } from '@mspkapps/auth-client';
|
|
242
|
-
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
243
|
-
|
|
244
|
-
// Replace these with your actual API keys
|
|
245
|
-
const PUBLIC_KEY = 'your_public_api_key_here';
|
|
246
|
-
const API_SECRET = 'your_api_secret_here';
|
|
247
|
-
|
|
248
|
-
// Initialize the auth client with AsyncStorage for React Native
|
|
249
|
-
const authClient = AuthClient.create(PUBLIC_KEY, API_SECRET, {
|
|
250
|
-
keyInPath: true,
|
|
251
|
-
storage: AsyncStorage, // Use AsyncStorage instead of localStorage
|
|
252
|
-
fetch: (input, init = {}) => {
|
|
253
|
-
const headers = new Headers(init.headers || {});
|
|
254
|
-
if (!headers.has('X-API-Key')) headers.set('X-API-Key', PUBLIC_KEY);
|
|
255
|
-
if (!headers.has('X-API-Secret')) headers.set('X-API-Secret', API_SECRET);
|
|
256
|
-
|
|
257
|
-
if (headers.has('Authorization')) {
|
|
258
|
-
const authHeader = headers.get('Authorization');
|
|
259
|
-
if (authHeader.startsWith('UserToken ')) {
|
|
260
|
-
headers.set('Authorization', 'Bearer ' + authHeader.substring(10));
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Use global fetch available in React Native
|
|
265
|
-
return fetch(input, { ...init, headers });
|
|
266
|
-
},
|
|
267
|
-
});
|
|
32
|
+
```js
|
|
33
|
+
// authClient.js
|
|
34
|
+
import authclient from '@mspkapps/auth-client';
|
|
268
35
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const [error, setError] = useState(null);
|
|
276
|
-
const [isReady, setIsReady] = useState(false);
|
|
277
|
-
|
|
278
|
-
// Load token from storage on mount
|
|
279
|
-
React.useEffect(() => {
|
|
280
|
-
const bootstrapAsync = async () => {
|
|
281
|
-
try {
|
|
282
|
-
const savedToken = await AsyncStorage.getItem('auth_user_token');
|
|
283
|
-
if (savedToken) {
|
|
284
|
-
authClient.setToken(savedToken);
|
|
285
|
-
setToken(savedToken);
|
|
286
|
-
|
|
287
|
-
// Fetch user profile
|
|
288
|
-
try {
|
|
289
|
-
const res = await authClient.getProfile();
|
|
290
|
-
setUser(res.data.user);
|
|
291
|
-
} catch (err) {
|
|
292
|
-
// Token might be invalid
|
|
293
|
-
await AsyncStorage.removeItem('auth_user_token');
|
|
294
|
-
authClient.setToken(null);
|
|
295
|
-
setToken(null);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
} catch (err) {
|
|
299
|
-
console.error('Failed to restore token', err);
|
|
300
|
-
} finally {
|
|
301
|
-
setIsReady(true);
|
|
302
|
-
}
|
|
303
|
-
};
|
|
304
|
-
|
|
305
|
-
bootstrapAsync();
|
|
306
|
-
}, []);
|
|
307
|
-
|
|
308
|
-
const login = useCallback(async ({ email, password }) => {
|
|
309
|
-
setError(null);
|
|
310
|
-
setLoading(true);
|
|
311
|
-
try {
|
|
312
|
-
const res = await authClient.login({ email, password });
|
|
313
|
-
const token = res?.data?.access_token;
|
|
314
|
-
if (token) {
|
|
315
|
-
authClient.setToken(token);
|
|
316
|
-
}
|
|
317
|
-
setUser(res?.data?.user || null);
|
|
318
|
-
setToken(authClient.token);
|
|
319
|
-
return res?.data?.user || null;
|
|
320
|
-
} catch (err) {
|
|
321
|
-
setError(err);
|
|
322
|
-
throw err;
|
|
323
|
-
} finally {
|
|
324
|
-
setLoading(false);
|
|
325
|
-
}
|
|
326
|
-
}, []);
|
|
327
|
-
|
|
328
|
-
const googleLogin = useCallback(async (idToken) => {
|
|
329
|
-
setError(null);
|
|
330
|
-
setLoading(true);
|
|
331
|
-
try {
|
|
332
|
-
const res = await authClient.googleAuth({ id_token: idToken });
|
|
333
|
-
const token = res?.data?.user_token || res?.data?.access_token;
|
|
334
|
-
if (token) {
|
|
335
|
-
authClient.setToken(token);
|
|
336
|
-
}
|
|
337
|
-
setUser(res?.data?.user || null);
|
|
338
|
-
setToken(authClient.token || token);
|
|
339
|
-
return { user: res?.data?.user, isNewUser: res?.data?.is_new_user };
|
|
340
|
-
} catch (err) {
|
|
341
|
-
setError(err);
|
|
342
|
-
throw err;
|
|
343
|
-
} finally {
|
|
344
|
-
setLoading(false);
|
|
345
|
-
}
|
|
346
|
-
}, []);
|
|
347
|
-
|
|
348
|
-
const logout = useCallback(async () => {
|
|
349
|
-
setUser(null);
|
|
350
|
-
setToken(null);
|
|
351
|
-
authClient.token = null;
|
|
352
|
-
await AsyncStorage.removeItem('auth_user_token');
|
|
353
|
-
}, []);
|
|
354
|
-
|
|
355
|
-
const refreshProfile = useCallback(async () => {
|
|
356
|
-
if (!token) return;
|
|
357
|
-
setLoading(true);
|
|
358
|
-
setError(null);
|
|
359
|
-
try {
|
|
360
|
-
const res = await authClient.getProfile();
|
|
361
|
-
setUser(res.data.user);
|
|
362
|
-
return res.data.user;
|
|
363
|
-
} catch (err) {
|
|
364
|
-
setError(err);
|
|
365
|
-
throw err;
|
|
366
|
-
} finally {
|
|
367
|
-
setLoading(false);
|
|
368
|
-
}
|
|
369
|
-
}, [token]);
|
|
370
|
-
|
|
371
|
-
const value = useMemo(
|
|
372
|
-
() => ({ authClient, user, token, login, googleLogin, logout, refreshProfile, loading, error, isReady }),
|
|
373
|
-
[user, token, login, googleLogin, logout, refreshProfile, loading, error, isReady]
|
|
374
|
-
);
|
|
375
|
-
|
|
376
|
-
return (
|
|
377
|
-
<AuthContext.Provider value={value}>
|
|
378
|
-
{isReady ? children : null}
|
|
379
|
-
</AuthContext.Provider>
|
|
380
|
-
);
|
|
381
|
-
}
|
|
36
|
+
authclient.init({
|
|
37
|
+
apiKey: process.env.MSPK_AUTH_API_KEY,
|
|
38
|
+
apiSecret: process.env.MSPK_AUTH_API_SECRET,
|
|
39
|
+
googleClientId: process.env.GOOGLE_CLIENT_ID, // optional, for Google OAuth
|
|
40
|
+
// baseUrl: 'https://cpanel.backend.mspkapps.in/api/v1', // optional override
|
|
41
|
+
});
|
|
382
42
|
|
|
383
|
-
export
|
|
384
|
-
const ctx = useContext(AuthContext);
|
|
385
|
-
if (!ctx) throw new Error('useAuth must be used within AuthProvider');
|
|
386
|
-
return ctx;
|
|
387
|
-
}
|
|
43
|
+
export default authclient;
|
|
388
44
|
```
|
|
389
45
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
Create `src/Navigation.js` for handling navigation between Login and Home:
|
|
46
|
+
### 2. Use in your routes/handlers
|
|
393
47
|
|
|
394
|
-
```
|
|
395
|
-
|
|
396
|
-
import
|
|
397
|
-
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
|
398
|
-
import { useAuth } from './AuthContext';
|
|
399
|
-
import LoginScreen from './screens/LoginScreen';
|
|
400
|
-
import RegisterScreen from './screens/RegisterScreen';
|
|
401
|
-
import HomeScreen from './screens/HomeScreen';
|
|
402
|
-
import SplashScreen from './screens/SplashScreen';
|
|
48
|
+
```js
|
|
49
|
+
// exampleRoute.js
|
|
50
|
+
import authclient from './authClient.js';
|
|
403
51
|
|
|
404
|
-
|
|
52
|
+
// Login (email + password)
|
|
53
|
+
export async function loginHandler(req, res) {
|
|
54
|
+
const { email, password } = req.body;
|
|
405
55
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
56
|
+
try {
|
|
57
|
+
const result = await authclient.login({ email, password });
|
|
58
|
+
// result.data.user, result.data.user_token, etc.
|
|
59
|
+
res.json(result);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
res.status(err.status || 400).json({
|
|
62
|
+
success: false,
|
|
63
|
+
message: err.message || 'Login failed',
|
|
64
|
+
code: err.code || 'LOGIN_FAILED',
|
|
65
|
+
});
|
|
411
66
|
}
|
|
412
|
-
|
|
413
|
-
return (
|
|
414
|
-
<NavigationContainer>
|
|
415
|
-
<Stack.Navigator
|
|
416
|
-
screenOptions={{ headerShown: false }}
|
|
417
|
-
>
|
|
418
|
-
{user ? (
|
|
419
|
-
<Stack.Screen name="Home" component={HomeScreen} />
|
|
420
|
-
) : (
|
|
421
|
-
<Stack.Group screenOptions={{ animationEnabled: false }}>
|
|
422
|
-
<Stack.Screen name="Auth" component={AuthNavigator} />
|
|
423
|
-
</Stack.Group>
|
|
424
|
-
)}
|
|
425
|
-
</Stack.Navigator>
|
|
426
|
-
</NavigationContainer>
|
|
427
|
-
);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
function AuthNavigator() {
|
|
431
|
-
const [showRegister, setShowRegister] = React.useState(false);
|
|
432
|
-
|
|
433
|
-
return (
|
|
434
|
-
<Stack.Navigator
|
|
435
|
-
screenOptions={{
|
|
436
|
-
headerShown: true,
|
|
437
|
-
animationEnabled: false,
|
|
438
|
-
}}
|
|
439
|
-
>
|
|
440
|
-
{showRegister ? (
|
|
441
|
-
<Stack.Screen
|
|
442
|
-
name="Register"
|
|
443
|
-
component={RegisterScreen}
|
|
444
|
-
options={{
|
|
445
|
-
title: 'Sign Up',
|
|
446
|
-
headerLeft: () => (
|
|
447
|
-
<TouchableOpacity onPress={() => setShowRegister(false)}>
|
|
448
|
-
<Text>Back</Text>
|
|
449
|
-
</TouchableOpacity>
|
|
450
|
-
),
|
|
451
|
-
}}
|
|
452
|
-
/>
|
|
453
|
-
) : (
|
|
454
|
-
<Stack.Screen
|
|
455
|
-
name="Login"
|
|
456
|
-
component={LoginScreen}
|
|
457
|
-
options={{
|
|
458
|
-
title: 'Login',
|
|
459
|
-
headerRight: () => (
|
|
460
|
-
<TouchableOpacity onPress={() => setShowRegister(true)}>
|
|
461
|
-
<Text>Sign Up</Text>
|
|
462
|
-
</TouchableOpacity>
|
|
463
|
-
),
|
|
464
|
-
}}
|
|
465
|
-
/>
|
|
466
|
-
)}
|
|
467
|
-
</Stack.Navigator>
|
|
468
|
-
);
|
|
469
67
|
}
|
|
470
68
|
```
|
|
471
69
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
```jsx
|
|
477
|
-
import React from 'react';
|
|
478
|
-
import { AuthProvider } from './src/AuthContext';
|
|
479
|
-
import { RootNavigator } from './src/Navigation';
|
|
70
|
+
```js
|
|
71
|
+
// Register
|
|
72
|
+
export async function registerHandler(req, res) {
|
|
73
|
+
const { email, username, password, name, ...extra } = req.body;
|
|
480
74
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
import { AuthProvider } from './AuthContext.jsx';
|
|
497
|
-
import { GoogleOAuthProvider } from '@react-oauth/google';
|
|
498
|
-
import './index.css';
|
|
499
|
-
|
|
500
|
-
// Replace with your Google Client ID from Google Cloud Console
|
|
501
|
-
const GOOGLE_CLIENT_ID = 'your_google_client_id_here';
|
|
502
|
-
|
|
503
|
-
ReactDOM.createRoot(document.getElementById('root')).render(
|
|
504
|
-
<React.StrictMode>
|
|
505
|
-
<GoogleOAuthProvider clientId={GOOGLE_CLIENT_ID}>
|
|
506
|
-
<AuthProvider>
|
|
507
|
-
<App />
|
|
508
|
-
</AuthProvider>
|
|
509
|
-
</GoogleOAuthProvider>
|
|
510
|
-
</React.StrictMode>
|
|
511
|
-
);
|
|
512
|
-
```
|
|
513
|
-
|
|
514
|
-
### Step 3: Create Main App Component
|
|
515
|
-
|
|
516
|
-
Create `src/App.jsx` to handle routing between Login and Home:
|
|
517
|
-
|
|
518
|
-
```jsx
|
|
519
|
-
import React, { useState } from 'react';
|
|
520
|
-
import { useAuth } from './AuthContext.jsx';
|
|
521
|
-
import Login from './components/Login.jsx';
|
|
522
|
-
import Register from './components/Register.jsx';
|
|
523
|
-
import Home from './components/Home.jsx';
|
|
524
|
-
import './App.css';
|
|
525
|
-
|
|
526
|
-
export default function App() {
|
|
527
|
-
const { user } = useAuth();
|
|
528
|
-
const [showRegister, setShowRegister] = useState(false);
|
|
529
|
-
|
|
530
|
-
// If user is logged in, show Home page
|
|
531
|
-
if (user) {
|
|
532
|
-
return <Home />;
|
|
75
|
+
try {
|
|
76
|
+
const result = await authclient.register({
|
|
77
|
+
email,
|
|
78
|
+
username,
|
|
79
|
+
password,
|
|
80
|
+
name,
|
|
81
|
+
...extra, // custom fields, if configured in MSPK
|
|
82
|
+
});
|
|
83
|
+
res.json(result);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
res.status(err.status || 400).json({
|
|
86
|
+
success: false,
|
|
87
|
+
message: err.message || 'Registration failed',
|
|
88
|
+
code: err.code || 'REGISTER_FAILED',
|
|
89
|
+
});
|
|
533
90
|
}
|
|
534
|
-
|
|
535
|
-
// Otherwise, show Login or Register page
|
|
536
|
-
return (
|
|
537
|
-
<div className="app-container">
|
|
538
|
-
<div className="auth-toggle">
|
|
539
|
-
<button
|
|
540
|
-
onClick={() => setShowRegister(false)}
|
|
541
|
-
className={!showRegister ? 'active' : ''}
|
|
542
|
-
>
|
|
543
|
-
Login
|
|
544
|
-
</button>
|
|
545
|
-
<button
|
|
546
|
-
onClick={() => setShowRegister(true)}
|
|
547
|
-
className={showRegister ? 'active' : ''}
|
|
548
|
-
>
|
|
549
|
-
Sign Up
|
|
550
|
-
</button>
|
|
551
|
-
</div>
|
|
552
|
-
{showRegister ? <Register /> : <Login />}
|
|
553
|
-
</div>
|
|
554
|
-
);
|
|
555
91
|
}
|
|
556
92
|
```
|
|
557
93
|
|
|
558
94
|
---
|
|
559
95
|
|
|
560
|
-
##
|
|
561
|
-
|
|
562
|
-
### Core Authentication Methods
|
|
563
|
-
|
|
564
|
-
All functions are accessed through the `authClient` object or the `useAuth()` hook.
|
|
565
|
-
|
|
566
|
-
#### 1. **register** - Create a new user account
|
|
567
|
-
|
|
568
|
-
```javascript
|
|
569
|
-
await authClient.register({
|
|
570
|
-
email: 'user@example.com',
|
|
571
|
-
username: 'johndoe', // Optional
|
|
572
|
-
password: 'securePass123',
|
|
573
|
-
name: 'John Doe' // Optional
|
|
574
|
-
});
|
|
575
|
-
```
|
|
576
|
-
|
|
577
|
-
**Parameters:**
|
|
578
|
-
- `email` (required): User's email address
|
|
579
|
-
- `username` (optional): Unique username
|
|
580
|
-
- `password` (required): User's password
|
|
581
|
-
- `name` (optional): User's display name
|
|
582
|
-
|
|
583
|
-
**Response:** Returns user data and automatically sets authentication token.
|
|
96
|
+
## API Reference – What Data Each Call Needs
|
|
584
97
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
#### 2. **login** - Login with email/username and password
|
|
98
|
+
All methods below assume you already called:
|
|
588
99
|
|
|
589
|
-
```
|
|
590
|
-
|
|
591
|
-
email: 'user@example.com',
|
|
592
|
-
password: 'securePass123'
|
|
593
|
-
});
|
|
100
|
+
```js
|
|
101
|
+
import authclient from '@mspkapps/auth-client';
|
|
594
102
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
103
|
+
authclient.init({
|
|
104
|
+
apiKey: 'YOUR_API_KEY',
|
|
105
|
+
apiSecret: 'YOUR_API_SECRET',
|
|
106
|
+
googleClientId: 'YOUR_GOOGLE_CLIENT_ID', // optional
|
|
599
107
|
});
|
|
600
108
|
```
|
|
601
109
|
|
|
602
|
-
|
|
603
|
-
- `email` OR `username` (required): User identifier
|
|
604
|
-
- `password` (required): User's password
|
|
110
|
+
### Auth & Users
|
|
605
111
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
---
|
|
112
|
+
#### `authclient.login({ email, password })` or `authclient.login({ username, password })`
|
|
609
113
|
|
|
610
|
-
|
|
114
|
+
- Required:
|
|
115
|
+
- `email` **or** `username` (string)
|
|
116
|
+
- `password` (string)
|
|
117
|
+
- Returns:
|
|
118
|
+
- User data and user token; token is stored internally via `setToken`.
|
|
611
119
|
|
|
612
|
-
|
|
613
|
-
await authClient.googleAuth({
|
|
614
|
-
id_token: 'google_id_token_here'
|
|
615
|
-
});
|
|
616
|
-
|
|
617
|
-
// Alternative: use access_token
|
|
618
|
-
await authClient.googleAuth({
|
|
619
|
-
access_token: 'google_access_token_here'
|
|
620
|
-
});
|
|
621
|
-
```
|
|
120
|
+
#### `authclient.register({ email, username?, password, name?, ...extraFields })`
|
|
622
121
|
|
|
623
|
-
|
|
624
|
-
- `
|
|
625
|
-
- `
|
|
122
|
+
- Required:
|
|
123
|
+
- `email` (string)
|
|
124
|
+
- `password` (string)
|
|
125
|
+
- Optional:
|
|
126
|
+
- `username` (string)
|
|
127
|
+
- `name` (string)
|
|
128
|
+
- Any extra profile fields you enabled in MSPK (e.g. `company`, `country`).
|
|
129
|
+
- Returns:
|
|
130
|
+
- User data and user token; token is stored internally.
|
|
626
131
|
|
|
627
|
-
|
|
132
|
+
#### `authclient.googleAuth({ id_token })`
|
|
133
|
+
#### `authclient.googleAuth({ access_token })`
|
|
628
134
|
|
|
629
|
-
|
|
135
|
+
- Required:
|
|
136
|
+
- Either:
|
|
137
|
+
- `id_token` (string), or
|
|
138
|
+
- `access_token` (string)
|
|
139
|
+
- The `googleClientId` is taken from `authclient.init(...)` and sent automatically.
|
|
140
|
+
- Returns:
|
|
141
|
+
- User data, token, and possibly a flag like `is_new_user`.
|
|
630
142
|
|
|
631
143
|
---
|
|
632
144
|
|
|
633
|
-
|
|
145
|
+
### Password Flows
|
|
634
146
|
|
|
635
|
-
|
|
636
|
-
authClient.logout();
|
|
637
|
-
```
|
|
147
|
+
#### `authclient.client.requestPasswordReset({ email })`
|
|
638
148
|
|
|
639
|
-
|
|
149
|
+
- Required:
|
|
150
|
+
- `email` (string)
|
|
151
|
+
- Use when: user clicks “Forgot password?”
|
|
640
152
|
|
|
641
|
-
|
|
153
|
+
#### `authclient.client.requestChangePasswordLink({ email })`
|
|
642
154
|
|
|
643
|
-
|
|
155
|
+
- Required:
|
|
156
|
+
- `email` (string)
|
|
157
|
+
- Use when: logged-in user requests a “change password” email from settings/profile.
|
|
644
158
|
|
|
645
|
-
####
|
|
159
|
+
#### `authclient.client.resendVerificationEmail({ email, purpose? })`
|
|
646
160
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
161
|
+
- Required:
|
|
162
|
+
- `email` (string)
|
|
163
|
+
- Optional:
|
|
164
|
+
- `purpose` (string). Valid values:
|
|
165
|
+
- `New Account`
|
|
166
|
+
- `Password change`
|
|
167
|
+
- `Profile Edit`
|
|
168
|
+
- `Forget Password`
|
|
169
|
+
- `Delete Account`
|
|
170
|
+
- `Set Password - Google User`
|
|
171
|
+
- If `purpose` is missing/invalid, the backend will treat it as `New Account`.
|
|
651
172
|
|
|
652
|
-
|
|
173
|
+
#### `authclient.client.sendGoogleUserSetPasswordEmail({ email })`
|
|
653
174
|
|
|
654
|
-
|
|
175
|
+
- Required:
|
|
176
|
+
- `email` (string) – user who signed up via Google and wants a password.
|
|
177
|
+
- Use when: a Google-only user wants to set a traditional password.
|
|
655
178
|
|
|
656
179
|
---
|
|
657
180
|
|
|
658
|
-
###
|
|
659
|
-
|
|
660
|
-
#### 6. **requestPasswordReset** - Send password reset email
|
|
661
|
-
|
|
662
|
-
```javascript
|
|
663
|
-
await authClient.requestPasswordReset({
|
|
664
|
-
email: 'user@example.com'
|
|
665
|
-
});
|
|
666
|
-
```
|
|
667
|
-
|
|
668
|
-
**Parameters:**
|
|
669
|
-
- `email` (required): Email address to send reset link
|
|
181
|
+
### Account Management
|
|
670
182
|
|
|
671
|
-
|
|
183
|
+
#### `authclient.client.deleteAccount({ email, password })`
|
|
672
184
|
|
|
673
|
-
|
|
185
|
+
- Required:
|
|
186
|
+
- `email` (string)
|
|
187
|
+
- `password` (string) – current password (depending on your server rules).
|
|
188
|
+
- Use when: user confirms account deletion.
|
|
674
189
|
|
|
675
190
|
---
|
|
676
191
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
```javascript
|
|
680
|
-
await authClient.requestChangePasswordLink({
|
|
681
|
-
email: 'user@example.com'
|
|
682
|
-
});
|
|
683
|
-
```
|
|
192
|
+
### Profile
|
|
684
193
|
|
|
685
|
-
|
|
686
|
-
- `email` (required): User's email address
|
|
194
|
+
#### `authclient.getProfile()`
|
|
687
195
|
|
|
688
|
-
|
|
196
|
+
- No parameters.
|
|
197
|
+
- Uses the internally stored user token.
|
|
198
|
+
- Returns current user profile.
|
|
689
199
|
|
|
690
|
-
|
|
200
|
+
#### `authclient.client.getEditableProfileFields()`
|
|
691
201
|
|
|
692
|
-
|
|
202
|
+
- No parameters.
|
|
203
|
+
- Returns metadata about which fields this user is allowed to edit based on your MSPK configuration.
|
|
693
204
|
|
|
694
|
-
####
|
|
205
|
+
#### `authclient.updateProfile(updates)`
|
|
695
206
|
|
|
696
|
-
```
|
|
697
|
-
await
|
|
698
|
-
|
|
207
|
+
```js
|
|
208
|
+
const res = await authclient.updateProfile({
|
|
209
|
+
name: 'New Name', // optional
|
|
210
|
+
username: 'new_username', // optional
|
|
211
|
+
email: 'new@example.com', // optional; may trigger verification
|
|
212
|
+
extra: { // optional; your custom fields
|
|
213
|
+
company: 'New Co',
|
|
214
|
+
country: 'US',
|
|
215
|
+
},
|
|
699
216
|
});
|
|
700
217
|
```
|
|
701
218
|
|
|
702
|
-
**
|
|
703
|
-
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
**Use Case:** When a user who registered via Google Sign-In wants to set a password for traditional login.
|
|
219
|
+
- All keys are **optional**; send only what you want to change.
|
|
220
|
+
- Allowed fields depend on:
|
|
221
|
+
- Core field permissions (name/username/email).
|
|
222
|
+
- Extra fields you defined for the app.
|
|
708
223
|
|
|
709
224
|
---
|
|
710
225
|
|
|
711
|
-
###
|
|
226
|
+
### Generic Authenticated Calls
|
|
712
227
|
|
|
713
|
-
####
|
|
228
|
+
#### `authclient.authed(path, { method = 'GET', body, headers } = {})`
|
|
714
229
|
|
|
715
|
-
```
|
|
716
|
-
await
|
|
717
|
-
|
|
718
|
-
|
|
230
|
+
```js
|
|
231
|
+
const res = await authclient.authed('user/profile', {
|
|
232
|
+
method: 'PATCH',
|
|
233
|
+
body: { name: 'Another Name' },
|
|
234
|
+
headers: {
|
|
235
|
+
'X-Custom-Header': '123',
|
|
236
|
+
},
|
|
719
237
|
});
|
|
720
238
|
```
|
|
721
239
|
|
|
722
|
-
|
|
723
|
-
- `
|
|
724
|
-
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
240
|
+
- Required:
|
|
241
|
+
- `path` (string) – relative path (e.g. `'user/profile'`), not full URL.
|
|
242
|
+
- Optional:
|
|
243
|
+
- `method` (string, default `'GET'`)
|
|
244
|
+
- `body` (object) – will be `JSON.stringify`’d
|
|
245
|
+
- `headers` (object) – extra headers merged into auth headers.
|
|
246
|
+
- Uses the same API key/secret/Google client and user token as other methods.
|
|
729
247
|
|
|
730
248
|
---
|
|
731
249
|
|
|
732
|
-
###
|
|
733
|
-
|
|
734
|
-
#### 10. **deleteAccount** - Permanently delete user account
|
|
250
|
+
### Token Helpers
|
|
735
251
|
|
|
736
|
-
|
|
737
|
-
await authClient.deleteAccount({
|
|
738
|
-
email: 'user@example.com',
|
|
739
|
-
password: 'userPassword123' // Optional, depending on server requirements
|
|
740
|
-
});
|
|
741
|
-
```
|
|
252
|
+
#### `authclient.setToken(token)`
|
|
742
253
|
|
|
743
|
-
|
|
744
|
-
- `
|
|
745
|
-
-
|
|
254
|
+
- Required:
|
|
255
|
+
- `token` (string or `null`).
|
|
256
|
+
- Usually not needed, because:
|
|
257
|
+
- `login`, `register`, and `googleAuth` will set the token automatically.
|
|
746
258
|
|
|
747
|
-
|
|
259
|
+
#### `authclient.logout()`
|
|
748
260
|
|
|
749
|
-
|
|
261
|
+
- No parameters.
|
|
262
|
+
- Clears the stored token in memory (and storage, if configured).
|
|
750
263
|
|
|
751
264
|
---
|
|
752
265
|
|
|
753
|
-
|
|
266
|
+
## Example Express Setup
|
|
754
267
|
|
|
755
|
-
|
|
268
|
+
```js
|
|
269
|
+
// authClient.js
|
|
270
|
+
import authclient from '@mspkapps/auth-client';
|
|
756
271
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
headers: { 'Custom-Header': 'value' }
|
|
272
|
+
authclient.init({
|
|
273
|
+
apiKey: process.env.MSPK_AUTH_API_KEY,
|
|
274
|
+
apiSecret: process.env.MSPK_AUTH_API_SECRET,
|
|
275
|
+
googleClientId: process.env.GOOGLE_CLIENT_ID,
|
|
762
276
|
});
|
|
763
|
-
```
|
|
764
277
|
|
|
765
|
-
|
|
766
|
-
- `path` (required): API endpoint path (without base URL)
|
|
767
|
-
- `options` (optional): Request options
|
|
768
|
-
- `method`: HTTP method (GET, POST, PUT, DELETE, etc.)
|
|
769
|
-
- `body`: Request body object
|
|
770
|
-
- `headers`: Additional headers
|
|
771
|
-
|
|
772
|
-
**Response:** Response data from custom endpoint.
|
|
773
|
-
|
|
774
|
-
**Use Case:** For custom endpoints not covered by the built-in methods.
|
|
775
|
-
|
|
776
|
-
---
|
|
777
|
-
|
|
778
|
-
## Implementation Examples
|
|
779
|
-
|
|
780
|
-
### React Web Examples
|
|
781
|
-
|
|
782
|
-
### Complete Login Component
|
|
783
|
-
|
|
784
|
-
Create `src/components/Login.jsx`:
|
|
785
|
-
|
|
786
|
-
```jsx
|
|
787
|
-
import React, { useState } from 'react';
|
|
788
|
-
import { useAuth } from '../AuthContext.jsx';
|
|
789
|
-
import { GoogleLogin } from '@react-oauth/google';
|
|
790
|
-
|
|
791
|
-
export default function Login() {
|
|
792
|
-
const { login, googleLogin, authClient } = useAuth();
|
|
793
|
-
|
|
794
|
-
// Form state
|
|
795
|
-
const [email, setEmail] = useState('');
|
|
796
|
-
const [password, setPassword] = useState('');
|
|
797
|
-
const [busy, setBusy] = useState(false);
|
|
798
|
-
const [error, setError] = useState(null);
|
|
799
|
-
|
|
800
|
-
// Password reset state
|
|
801
|
-
const [showReset, setShowReset] = useState(false);
|
|
802
|
-
const [resetEmail, setResetEmail] = useState('');
|
|
803
|
-
const [resetFeedback, setResetFeedback] = useState(null);
|
|
804
|
-
|
|
805
|
-
// Google user set password state
|
|
806
|
-
const [showGoogleSetPassword, setShowGoogleSetPassword] = useState(false);
|
|
807
|
-
const [googleSetPasswordEmail, setGoogleSetPasswordEmail] = useState('');
|
|
808
|
-
const [googleSetPasswordFeedback, setGoogleSetPasswordFeedback] = useState(null);
|
|
809
|
-
|
|
810
|
-
// Email verification state
|
|
811
|
-
const [resendFeedback, setResendFeedback] = useState(null);
|
|
812
|
-
|
|
813
|
-
// Handle standard email/password login
|
|
814
|
-
async function handleSubmit(e) {
|
|
815
|
-
e.preventDefault();
|
|
816
|
-
setError(null);
|
|
817
|
-
setResendFeedback(null);
|
|
818
|
-
setBusy(true);
|
|
819
|
-
|
|
820
|
-
try {
|
|
821
|
-
await login({ email, password });
|
|
822
|
-
// Success - user will be redirected by App.jsx
|
|
823
|
-
} catch (err) {
|
|
824
|
-
setError(err.message || 'Login failed');
|
|
825
|
-
|
|
826
|
-
// Check if account needs verification
|
|
827
|
-
if (
|
|
828
|
-
err.code === 'EMAIL_NOT_VERIFIED' ||
|
|
829
|
-
err.message?.toLowerCase().includes('not been verified')
|
|
830
|
-
) {
|
|
831
|
-
setResendFeedback('Your account has not been verified. Click below to resend verification.');
|
|
832
|
-
}
|
|
833
|
-
} finally {
|
|
834
|
-
setBusy(false);
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
// Handle Google Sign-In
|
|
839
|
-
async function handleGoogleSuccess(credentialResponse) {
|
|
840
|
-
setBusy(true);
|
|
841
|
-
try {
|
|
842
|
-
const result = await googleLogin(credentialResponse.credential);
|
|
843
|
-
if (result.isNewUser) {
|
|
844
|
-
console.log('Welcome, new user!');
|
|
845
|
-
}
|
|
846
|
-
} catch (err) {
|
|
847
|
-
setError(err.message || 'Google login failed');
|
|
848
|
-
} finally {
|
|
849
|
-
setBusy(false);
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
// Handle password reset request
|
|
854
|
-
async function handlePasswordReset(e) {
|
|
855
|
-
e.preventDefault();
|
|
856
|
-
setBusy(true);
|
|
857
|
-
setResetFeedback(null);
|
|
858
|
-
|
|
859
|
-
try {
|
|
860
|
-
await authClient.requestPasswordReset({ email: resetEmail });
|
|
861
|
-
setResetFeedback('Password reset email sent. Check your inbox.');
|
|
862
|
-
setResetEmail('');
|
|
863
|
-
} catch (err) {
|
|
864
|
-
setResetFeedback(err.message || 'Failed to send reset email');
|
|
865
|
-
} finally {
|
|
866
|
-
setBusy(false);
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
// Handle resend verification email
|
|
871
|
-
async function handleResendVerification() {
|
|
872
|
-
setBusy(true);
|
|
873
|
-
setResendFeedback(null);
|
|
874
|
-
|
|
875
|
-
try {
|
|
876
|
-
await authClient.resendVerificationEmail({
|
|
877
|
-
email,
|
|
878
|
-
purpose: 'New Account'
|
|
879
|
-
});
|
|
880
|
-
setResendFeedback('Verification email sent. Please check your inbox.');
|
|
881
|
-
} catch (err) {
|
|
882
|
-
setResendFeedback(err.message || 'Failed to resend verification');
|
|
883
|
-
} finally {
|
|
884
|
-
setBusy(false);
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
// Handle Google user set password email
|
|
889
|
-
async function handleSendGoogleSetPasswordEmail(e) {
|
|
890
|
-
e.preventDefault();
|
|
891
|
-
setBusy(true);
|
|
892
|
-
setGoogleSetPasswordFeedback(null);
|
|
893
|
-
|
|
894
|
-
try {
|
|
895
|
-
await authClient.sendGoogleUserSetPasswordEmail({
|
|
896
|
-
email: googleSetPasswordEmail
|
|
897
|
-
});
|
|
898
|
-
setGoogleSetPasswordFeedback('Email sent. Check your inbox to set a password.');
|
|
899
|
-
setGoogleSetPasswordEmail('');
|
|
900
|
-
} catch (err) {
|
|
901
|
-
setGoogleSetPasswordFeedback(err.message || 'Failed to send email');
|
|
902
|
-
} finally {
|
|
903
|
-
setBusy(false);
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
return (
|
|
908
|
-
<div className="login-container">
|
|
909
|
-
<h2>Login</h2>
|
|
910
|
-
|
|
911
|
-
{/* Email/Password Login Form */}
|
|
912
|
-
<form onSubmit={handleSubmit}>
|
|
913
|
-
<label>
|
|
914
|
-
Email
|
|
915
|
-
<input
|
|
916
|
-
type="email"
|
|
917
|
-
value={email}
|
|
918
|
-
onChange={(e) => setEmail(e.target.value)}
|
|
919
|
-
required
|
|
920
|
-
/>
|
|
921
|
-
</label>
|
|
922
|
-
|
|
923
|
-
<label>
|
|
924
|
-
Password
|
|
925
|
-
<input
|
|
926
|
-
type="password"
|
|
927
|
-
value={password}
|
|
928
|
-
onChange={(e) => setPassword(e.target.value)}
|
|
929
|
-
required
|
|
930
|
-
/>
|
|
931
|
-
</label>
|
|
932
|
-
|
|
933
|
-
<button type="submit" disabled={busy}>
|
|
934
|
-
{busy ? 'Please wait…' : 'Login'}
|
|
935
|
-
</button>
|
|
936
|
-
</form>
|
|
937
|
-
|
|
938
|
-
{error && <p style={{ color: 'red' }}>{error}</p>}
|
|
939
|
-
|
|
940
|
-
{/* Google Sign-In Button */}
|
|
941
|
-
<div style={{ marginTop: 16 }}>
|
|
942
|
-
<span>or</span>
|
|
943
|
-
<GoogleLogin
|
|
944
|
-
onSuccess={handleGoogleSuccess}
|
|
945
|
-
onError={() => setError('Google login failed')}
|
|
946
|
-
/>
|
|
947
|
-
</div>
|
|
948
|
-
|
|
949
|
-
{/* Resend Verification Email */}
|
|
950
|
-
{resendFeedback && (
|
|
951
|
-
<div>
|
|
952
|
-
<p>{resendFeedback}</p>
|
|
953
|
-
{resendFeedback.includes('has not been verified') && (
|
|
954
|
-
<button onClick={handleResendVerification} disabled={busy}>
|
|
955
|
-
Resend Verification Email
|
|
956
|
-
</button>
|
|
957
|
-
)}
|
|
958
|
-
</div>
|
|
959
|
-
)}
|
|
960
|
-
|
|
961
|
-
{/* Forgot Password */}
|
|
962
|
-
<div style={{ marginTop: 16 }}>
|
|
963
|
-
<button onClick={() => setShowReset(!showReset)}>
|
|
964
|
-
{showReset ? 'Hide' : 'Forgot password?'}
|
|
965
|
-
</button>
|
|
966
|
-
|
|
967
|
-
{showReset && (
|
|
968
|
-
<form onSubmit={handlePasswordReset}>
|
|
969
|
-
<input
|
|
970
|
-
type="email"
|
|
971
|
-
placeholder="Enter your email"
|
|
972
|
-
value={resetEmail}
|
|
973
|
-
onChange={(e) => setResetEmail(e.target.value)}
|
|
974
|
-
required
|
|
975
|
-
/>
|
|
976
|
-
<button type="submit" disabled={busy}>
|
|
977
|
-
Send Reset Link
|
|
978
|
-
</button>
|
|
979
|
-
{resetFeedback && <p>{resetFeedback}</p>}
|
|
980
|
-
</form>
|
|
981
|
-
)}
|
|
982
|
-
</div>
|
|
983
|
-
|
|
984
|
-
{/* Google User Set Password */}
|
|
985
|
-
<div style={{ marginTop: 12 }}>
|
|
986
|
-
<button onClick={() => setShowGoogleSetPassword(!showGoogleSetPassword)}>
|
|
987
|
-
{showGoogleSetPassword ? 'Hide' : 'No password? Are you a Google user?'}
|
|
988
|
-
</button>
|
|
989
|
-
|
|
990
|
-
{showGoogleSetPassword && (
|
|
991
|
-
<form onSubmit={handleSendGoogleSetPasswordEmail}>
|
|
992
|
-
<input
|
|
993
|
-
type="email"
|
|
994
|
-
placeholder="Enter your Google account email"
|
|
995
|
-
value={googleSetPasswordEmail}
|
|
996
|
-
onChange={(e) => setGoogleSetPasswordEmail(e.target.value)}
|
|
997
|
-
required
|
|
998
|
-
/>
|
|
999
|
-
<button type="submit" disabled={busy}>
|
|
1000
|
-
Send Set-Password Email
|
|
1001
|
-
</button>
|
|
1002
|
-
{googleSetPasswordFeedback && <p>{googleSetPasswordFeedback}</p>}
|
|
1003
|
-
</form>
|
|
1004
|
-
)}
|
|
1005
|
-
</div>
|
|
1006
|
-
</div>
|
|
1007
|
-
);
|
|
1008
|
-
}
|
|
278
|
+
export default authclient;
|
|
1009
279
|
```
|
|
1010
280
|
|
|
1011
|
-
|
|
281
|
+
```js
|
|
282
|
+
// routes/auth.js
|
|
283
|
+
import express from 'express';
|
|
284
|
+
import authclient from '../authClient.js';
|
|
1012
285
|
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
Create `src/components/Register.jsx`:
|
|
1016
|
-
|
|
1017
|
-
```jsx
|
|
1018
|
-
import React, { useState } from 'react';
|
|
1019
|
-
import { useAuth } from '../AuthContext.jsx';
|
|
1020
|
-
|
|
1021
|
-
export default function Register() {
|
|
1022
|
-
const { authClient } = useAuth();
|
|
1023
|
-
|
|
1024
|
-
const [email, setEmail] = useState('');
|
|
1025
|
-
const [password, setPassword] = useState('');
|
|
1026
|
-
const [name, setName] = useState('');
|
|
1027
|
-
const [username, setUsername] = useState('');
|
|
1028
|
-
const [error, setError] = useState(null);
|
|
1029
|
-
const [success, setSuccess] = useState(null);
|
|
1030
|
-
const [busy, setBusy] = useState(false);
|
|
1031
|
-
|
|
1032
|
-
async function handleSubmit(e) {
|
|
1033
|
-
e.preventDefault();
|
|
1034
|
-
setError(null);
|
|
1035
|
-
setSuccess(null);
|
|
1036
|
-
setBusy(true);
|
|
1037
|
-
|
|
1038
|
-
try {
|
|
1039
|
-
await authClient.register({
|
|
1040
|
-
email,
|
|
1041
|
-
username,
|
|
1042
|
-
password,
|
|
1043
|
-
name
|
|
1044
|
-
});
|
|
1045
|
-
|
|
1046
|
-
setSuccess('Registered! Please verify your email, then login.');
|
|
1047
|
-
|
|
1048
|
-
// Clear form
|
|
1049
|
-
setEmail('');
|
|
1050
|
-
setPassword('');
|
|
1051
|
-
setName('');
|
|
1052
|
-
setUsername('');
|
|
1053
|
-
} catch (err) {
|
|
1054
|
-
setError(err.message || 'Registration failed');
|
|
1055
|
-
} finally {
|
|
1056
|
-
setBusy(false);
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
return (
|
|
1061
|
-
<div className="register-container">
|
|
1062
|
-
<h2>Sign Up</h2>
|
|
1063
|
-
|
|
1064
|
-
<form onSubmit={handleSubmit}>
|
|
1065
|
-
<label>
|
|
1066
|
-
Name
|
|
1067
|
-
<input
|
|
1068
|
-
type="text"
|
|
1069
|
-
value={name}
|
|
1070
|
-
onChange={(e) => setName(e.target.value)}
|
|
1071
|
-
/>
|
|
1072
|
-
</label>
|
|
1073
|
-
|
|
1074
|
-
<label>
|
|
1075
|
-
Username
|
|
1076
|
-
<input
|
|
1077
|
-
type="text"
|
|
1078
|
-
value={username}
|
|
1079
|
-
onChange={(e) => setUsername(e.target.value)}
|
|
1080
|
-
/>
|
|
1081
|
-
</label>
|
|
1082
|
-
|
|
1083
|
-
<label>
|
|
1084
|
-
Email
|
|
1085
|
-
<input
|
|
1086
|
-
type="email"
|
|
1087
|
-
value={email}
|
|
1088
|
-
onChange={(e) => setEmail(e.target.value)}
|
|
1089
|
-
required
|
|
1090
|
-
/>
|
|
1091
|
-
</label>
|
|
1092
|
-
|
|
1093
|
-
<label>
|
|
1094
|
-
Password
|
|
1095
|
-
<input
|
|
1096
|
-
type="password"
|
|
1097
|
-
value={password}
|
|
1098
|
-
onChange={(e) => setPassword(e.target.value)}
|
|
1099
|
-
required
|
|
1100
|
-
/>
|
|
1101
|
-
</label>
|
|
1102
|
-
|
|
1103
|
-
<button type="submit" disabled={busy}>
|
|
1104
|
-
{busy ? 'Please wait…' : 'Create Account'}
|
|
1105
|
-
</button>
|
|
1106
|
-
</form>
|
|
1107
|
-
|
|
1108
|
-
{error && <p style={{ color: 'red' }}>{error}</p>}
|
|
1109
|
-
{success && <p style={{ color: 'green' }}>{success}</p>}
|
|
1110
|
-
</div>
|
|
1111
|
-
);
|
|
1112
|
-
}
|
|
1113
|
-
```
|
|
286
|
+
const router = express.Router();
|
|
1114
287
|
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
### Complete Home/Dashboard Component
|
|
1118
|
-
|
|
1119
|
-
Create `src/components/Home.jsx`:
|
|
1120
|
-
|
|
1121
|
-
```jsx
|
|
1122
|
-
import React, { useState } from 'react';
|
|
1123
|
-
import { useAuth } from '../AuthContext.jsx';
|
|
1124
|
-
|
|
1125
|
-
export default function Home() {
|
|
1126
|
-
const { user, token, logout, refreshProfile, loading, authClient } = useAuth();
|
|
1127
|
-
|
|
1128
|
-
const [feedback, setFeedback] = useState(null);
|
|
1129
|
-
const [busy, setBusy] = useState(false);
|
|
1130
|
-
|
|
1131
|
-
// Delete account state
|
|
1132
|
-
const [showDeleteForm, setShowDeleteForm] = useState(false);
|
|
1133
|
-
const [deleteFeedback, setDeleteFeedback] = useState(null);
|
|
1134
|
-
|
|
1135
|
-
// Handle password change request (sends email)
|
|
1136
|
-
async function doChangePassword(e) {
|
|
1137
|
-
e.preventDefault();
|
|
1138
|
-
setBusy(true);
|
|
1139
|
-
setFeedback(null);
|
|
1140
|
-
|
|
1141
|
-
try {
|
|
1142
|
-
await authClient.requestChangePasswordLink({ email: user?.email });
|
|
1143
|
-
setFeedback('Password change email sent. Check your inbox.');
|
|
1144
|
-
} catch (err) {
|
|
1145
|
-
setFeedback(err.message || 'Failed to send password change email');
|
|
1146
|
-
} finally {
|
|
1147
|
-
setBusy(false);
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
288
|
+
router.post('/login', async (req, res) => {
|
|
289
|
+
const { email, password, username } = req.body;
|
|
1150
290
|
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
try {
|
|
1163
|
-
await authClient.deleteAccount({ email: user?.email });
|
|
1164
|
-
setDeleteFeedback('Account deleted successfully. Logging out...');
|
|
1165
|
-
|
|
1166
|
-
// Auto logout after 1.5 seconds
|
|
1167
|
-
setTimeout(() => {
|
|
1168
|
-
logout();
|
|
1169
|
-
}, 1500);
|
|
1170
|
-
} catch (err) {
|
|
1171
|
-
setDeleteFeedback(err.message || 'Delete account failed');
|
|
1172
|
-
} finally {
|
|
1173
|
-
setBusy(false);
|
|
1174
|
-
}
|
|
291
|
+
try {
|
|
292
|
+
const result = await authclient.login(
|
|
293
|
+
email ? { email, password } : { username, password }
|
|
294
|
+
);
|
|
295
|
+
res.json(result);
|
|
296
|
+
} catch (err) {
|
|
297
|
+
res.status(err.status || 400).json({
|
|
298
|
+
success: false,
|
|
299
|
+
message: err.message || 'Login failed',
|
|
300
|
+
code: err.code || 'LOGIN_FAILED',
|
|
301
|
+
});
|
|
1175
302
|
}
|
|
1176
|
-
|
|
1177
|
-
return (
|
|
1178
|
-
<div className="home-container">
|
|
1179
|
-
<h2>Welcome</h2>
|
|
1180
|
-
|
|
1181
|
-
{/* User Profile Display */}
|
|
1182
|
-
<div className="user-box">
|
|
1183
|
-
<p><strong>User ID:</strong> {user?.id || 'n/a'}</p>
|
|
1184
|
-
<p><strong>Email:</strong> {user?.email}</p>
|
|
1185
|
-
<p><strong>Name:</strong> {user?.name || '—'}</p>
|
|
1186
|
-
<p><strong>Login Method:</strong> {user?.login_method || '—'}</p>
|
|
1187
|
-
<p><strong>Google Linked:</strong> {user?.google_linked ? 'Yes' : '—'}</p>
|
|
1188
|
-
<p><strong>Token (truncated):</strong> {token ? token.slice(0, 18) + '…' : 'none'}</p>
|
|
1189
|
-
</div>
|
|
1190
|
-
|
|
1191
|
-
{/* Action Buttons */}
|
|
1192
|
-
<div className="actions">
|
|
1193
|
-
<button onClick={refreshProfile} disabled={loading}>
|
|
1194
|
-
Refresh Profile
|
|
1195
|
-
</button>
|
|
1196
|
-
<button onClick={logout}>Logout</button>
|
|
1197
|
-
</div>
|
|
1198
|
-
|
|
1199
|
-
{/* Change Password (Email Link) */}
|
|
1200
|
-
<form onSubmit={doChangePassword} style={{ marginTop: 24 }}>
|
|
1201
|
-
<h3>Change Password</h3>
|
|
1202
|
-
<p>We will email a secure link to {user?.email} to change your password.</p>
|
|
1203
|
-
<button type="submit" disabled={busy}>
|
|
1204
|
-
{busy ? 'Sending…' : 'Send Change Password Email'}
|
|
1205
|
-
</button>
|
|
1206
|
-
</form>
|
|
1207
|
-
{feedback && <p>{feedback}</p>}
|
|
1208
|
-
|
|
1209
|
-
{/* Delete Account */}
|
|
1210
|
-
<div style={{ marginTop: 24 }}>
|
|
1211
|
-
<button
|
|
1212
|
-
onClick={() => setShowDeleteForm(!showDeleteForm)}
|
|
1213
|
-
style={{ background: 'red', color: 'white' }}
|
|
1214
|
-
>
|
|
1215
|
-
{showDeleteForm ? 'Cancel' : 'Delete Account'}
|
|
1216
|
-
</button>
|
|
1217
|
-
|
|
1218
|
-
{showDeleteForm && (
|
|
1219
|
-
<form onSubmit={doDeleteAccount}>
|
|
1220
|
-
<p style={{ color: 'red' }}>
|
|
1221
|
-
Are you sure? This cannot be undone.
|
|
1222
|
-
</p>
|
|
1223
|
-
<button type="submit" disabled={busy}>
|
|
1224
|
-
{busy ? 'Deleting…' : 'Confirm Delete Account'}
|
|
1225
|
-
</button>
|
|
1226
|
-
{deleteFeedback && <p>{deleteFeedback}</p>}
|
|
1227
|
-
</form>
|
|
1228
|
-
)}
|
|
1229
|
-
</div>
|
|
1230
|
-
</div>
|
|
1231
|
-
);
|
|
1232
|
-
}
|
|
1233
|
-
```
|
|
1234
|
-
|
|
1235
|
-
---
|
|
1236
|
-
|
|
1237
|
-
### React Native Examples
|
|
1238
|
-
|
|
1239
|
-
#### Complete React Native Login Screen
|
|
1240
|
-
|
|
1241
|
-
Create `src/screens/LoginScreen.js`:
|
|
1242
|
-
|
|
1243
|
-
```jsx
|
|
1244
|
-
import React, { useState } from 'react';
|
|
1245
|
-
import {
|
|
1246
|
-
View,
|
|
1247
|
-
Text,
|
|
1248
|
-
TextInput,
|
|
1249
|
-
TouchableOpacity,
|
|
1250
|
-
ScrollView,
|
|
1251
|
-
ActivityIndicator,
|
|
1252
|
-
Alert,
|
|
1253
|
-
} from 'react-native';
|
|
1254
|
-
import { GoogleSignin, GoogleSigninButton } from '@react-native-google-signin/google-signin';
|
|
1255
|
-
import { useAuth } from '../AuthContext';
|
|
1256
|
-
|
|
1257
|
-
GoogleSignin.configure({
|
|
1258
|
-
webClientId: 'YOUR_WEB_CLIENT_ID.apps.googleusercontent.com',
|
|
1259
303
|
});
|
|
1260
304
|
|
|
1261
|
-
|
|
1262
|
-
const {
|
|
1263
|
-
|
|
1264
|
-
const [email, setEmail] = useState('');
|
|
1265
|
-
const [password, setPassword] = useState('');
|
|
1266
|
-
const [busy, setBusy] = useState(false);
|
|
1267
|
-
const [error, setError] = useState(null);
|
|
1268
|
-
const [showReset, setShowReset] = useState(false);
|
|
1269
|
-
const [resetEmail, setResetEmail] = useState('');
|
|
1270
|
-
|
|
1271
|
-
async function handleSubmit() {
|
|
1272
|
-
setError(null);
|
|
1273
|
-
setBusy(true);
|
|
1274
|
-
|
|
1275
|
-
try {
|
|
1276
|
-
await login({ email, password });
|
|
1277
|
-
} catch (err) {
|
|
1278
|
-
setError(err.message || 'Login failed');
|
|
1279
|
-
} finally {
|
|
1280
|
-
setBusy(false);
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
async function handleGoogleSignIn() {
|
|
1285
|
-
try {
|
|
1286
|
-
await GoogleSignin.hasPlayServices();
|
|
1287
|
-
const userInfo = await GoogleSignin.signIn();
|
|
1288
|
-
|
|
1289
|
-
const result = await googleLogin(userInfo.idToken);
|
|
1290
|
-
if (result.isNewUser) {
|
|
1291
|
-
Alert.alert('Welcome!', 'New account created');
|
|
1292
|
-
}
|
|
1293
|
-
} catch (err) {
|
|
1294
|
-
setError(err.message || 'Google sign in failed');
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
async function handlePasswordReset() {
|
|
1299
|
-
setBusy(true);
|
|
1300
|
-
try {
|
|
1301
|
-
await authClient.requestPasswordReset({ email: resetEmail });
|
|
1302
|
-
Alert.alert('Success', 'Password reset email sent');
|
|
1303
|
-
setResetEmail('');
|
|
1304
|
-
setShowReset(false);
|
|
1305
|
-
} catch (err) {
|
|
1306
|
-
setError(err.message || 'Failed to send reset email');
|
|
1307
|
-
} finally {
|
|
1308
|
-
setBusy(false);
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
return (
|
|
1313
|
-
<ScrollView style={{ flex: 1, padding: 20, backgroundColor: '#fff' }}>
|
|
1314
|
-
<Text style={{ fontSize: 24, fontWeight: 'bold', marginBottom: 24 }}>Login</Text>
|
|
1315
|
-
|
|
1316
|
-
<TextInput
|
|
1317
|
-
placeholder="Email"
|
|
1318
|
-
value={email}
|
|
1319
|
-
onChangeText={setEmail}
|
|
1320
|
-
keyboardType="email-address"
|
|
1321
|
-
editable={!loading}
|
|
1322
|
-
style={{
|
|
1323
|
-
borderWidth: 1,
|
|
1324
|
-
borderColor: '#ddd',
|
|
1325
|
-
padding: 12,
|
|
1326
|
-
marginBottom: 12,
|
|
1327
|
-
borderRadius: 8,
|
|
1328
|
-
}}
|
|
1329
|
-
/>
|
|
1330
|
-
|
|
1331
|
-
<TextInput
|
|
1332
|
-
placeholder="Password"
|
|
1333
|
-
value={password}
|
|
1334
|
-
onChangeText={setPassword}
|
|
1335
|
-
secureTextEntry
|
|
1336
|
-
editable={!loading}
|
|
1337
|
-
style={{
|
|
1338
|
-
borderWidth: 1,
|
|
1339
|
-
borderColor: '#ddd',
|
|
1340
|
-
padding: 12,
|
|
1341
|
-
marginBottom: 20,
|
|
1342
|
-
borderRadius: 8,
|
|
1343
|
-
}}
|
|
1344
|
-
/>
|
|
1345
|
-
|
|
1346
|
-
<TouchableOpacity
|
|
1347
|
-
onPress={handleSubmit}
|
|
1348
|
-
disabled={loading || busy}
|
|
1349
|
-
style={{
|
|
1350
|
-
backgroundColor: '#0066cc',
|
|
1351
|
-
padding: 12,
|
|
1352
|
-
borderRadius: 8,
|
|
1353
|
-
alignItems: 'center',
|
|
1354
|
-
marginBottom: 20,
|
|
1355
|
-
}}
|
|
1356
|
-
>
|
|
1357
|
-
{loading || busy ? (
|
|
1358
|
-
<ActivityIndicator color="#fff" />
|
|
1359
|
-
) : (
|
|
1360
|
-
<Text style={{ color: '#fff', fontWeight: 'bold' }}>Login</Text>
|
|
1361
|
-
)}
|
|
1362
|
-
</TouchableOpacity>
|
|
1363
|
-
|
|
1364
|
-
{error && <Text style={{ color: 'red', marginBottom: 12 }}>{error}</Text>}
|
|
1365
|
-
|
|
1366
|
-
<View style={{ alignItems: 'center', marginVertical: 16 }}>
|
|
1367
|
-
<Text>or</Text>
|
|
1368
|
-
</View>
|
|
1369
|
-
|
|
1370
|
-
<GoogleSigninButton
|
|
1371
|
-
size={GoogleSigninButton.Size.Wide}
|
|
1372
|
-
color={GoogleSigninButton.Color.Dark}
|
|
1373
|
-
onPress={handleGoogleSignIn}
|
|
1374
|
-
disabled={loading}
|
|
1375
|
-
/>
|
|
1376
|
-
|
|
1377
|
-
<TouchableOpacity
|
|
1378
|
-
onPress={() => setShowReset(!showReset)}
|
|
1379
|
-
style={{ marginTop: 20 }}
|
|
1380
|
-
>
|
|
1381
|
-
<Text style={{ color: '#0066cc' }}>
|
|
1382
|
-
{showReset ? 'Hide' : 'Forgot password?'}
|
|
1383
|
-
</Text>
|
|
1384
|
-
</TouchableOpacity>
|
|
1385
|
-
|
|
1386
|
-
{showReset && (
|
|
1387
|
-
<View style={{ marginTop: 12 }}>
|
|
1388
|
-
<TextInput
|
|
1389
|
-
placeholder="Enter your email"
|
|
1390
|
-
value={resetEmail}
|
|
1391
|
-
onChangeText={setResetEmail}
|
|
1392
|
-
keyboardType="email-address"
|
|
1393
|
-
style={{
|
|
1394
|
-
borderWidth: 1,
|
|
1395
|
-
borderColor: '#ddd',
|
|
1396
|
-
padding: 12,
|
|
1397
|
-
marginBottom: 12,
|
|
1398
|
-
borderRadius: 8,
|
|
1399
|
-
}}
|
|
1400
|
-
/>
|
|
1401
|
-
<TouchableOpacity
|
|
1402
|
-
onPress={handlePasswordReset}
|
|
1403
|
-
disabled={busy}
|
|
1404
|
-
style={{
|
|
1405
|
-
backgroundColor: '#0066cc',
|
|
1406
|
-
padding: 12,
|
|
1407
|
-
borderRadius: 8,
|
|
1408
|
-
alignItems: 'center',
|
|
1409
|
-
}}
|
|
1410
|
-
>
|
|
1411
|
-
<Text style={{ color: '#fff' }}>
|
|
1412
|
-
{busy ? 'Sending...' : 'Send Reset Link'}
|
|
1413
|
-
</Text>
|
|
1414
|
-
</TouchableOpacity>
|
|
1415
|
-
</View>
|
|
1416
|
-
)}
|
|
1417
|
-
</ScrollView>
|
|
1418
|
-
);
|
|
1419
|
-
}
|
|
1420
|
-
```
|
|
1421
|
-
|
|
1422
|
-
#### Complete React Native Register Screen
|
|
1423
|
-
|
|
1424
|
-
Create `src/screens/RegisterScreen.js`:
|
|
1425
|
-
|
|
1426
|
-
```jsx
|
|
1427
|
-
import React, { useState } from 'react';
|
|
1428
|
-
import {
|
|
1429
|
-
View,
|
|
1430
|
-
Text,
|
|
1431
|
-
TextInput,
|
|
1432
|
-
TouchableOpacity,
|
|
1433
|
-
ScrollView,
|
|
1434
|
-
ActivityIndicator,
|
|
1435
|
-
Alert,
|
|
1436
|
-
} from 'react-native';
|
|
1437
|
-
import { useAuth } from '../AuthContext';
|
|
1438
|
-
|
|
1439
|
-
export default function RegisterScreen() {
|
|
1440
|
-
const { authClient } = useAuth();
|
|
1441
|
-
|
|
1442
|
-
const [email, setEmail] = useState('');
|
|
1443
|
-
const [password, setPassword] = useState('');
|
|
1444
|
-
const [name, setName] = useState('');
|
|
1445
|
-
const [username, setUsername] = useState('');
|
|
1446
|
-
const [error, setError] = useState(null);
|
|
1447
|
-
const [busy, setBusy] = useState(false);
|
|
1448
|
-
|
|
1449
|
-
async function handleSubmit() {
|
|
1450
|
-
setError(null);
|
|
1451
|
-
setBusy(true);
|
|
1452
|
-
|
|
1453
|
-
try {
|
|
1454
|
-
await authClient.register({
|
|
1455
|
-
email,
|
|
1456
|
-
username,
|
|
1457
|
-
password,
|
|
1458
|
-
name,
|
|
1459
|
-
});
|
|
1460
|
-
|
|
1461
|
-
Alert.alert(
|
|
1462
|
-
'Registration Successful',
|
|
1463
|
-
'Please verify your email, then login.'
|
|
1464
|
-
);
|
|
1465
|
-
|
|
1466
|
-
setEmail('');
|
|
1467
|
-
setPassword('');
|
|
1468
|
-
setName('');
|
|
1469
|
-
setUsername('');
|
|
1470
|
-
} catch (err) {
|
|
1471
|
-
setError(err.message || 'Registration failed');
|
|
1472
|
-
} finally {
|
|
1473
|
-
setBusy(false);
|
|
1474
|
-
}
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
|
-
return (
|
|
1478
|
-
<ScrollView style={{ flex: 1, padding: 20, backgroundColor: '#fff' }}>
|
|
1479
|
-
<Text style={{ fontSize: 24, fontWeight: 'bold', marginBottom: 24 }}>Sign Up</Text>
|
|
1480
|
-
|
|
1481
|
-
<TextInput
|
|
1482
|
-
placeholder="Name"
|
|
1483
|
-
value={name}
|
|
1484
|
-
onChangeText={setName}
|
|
1485
|
-
editable={!busy}
|
|
1486
|
-
style={{
|
|
1487
|
-
borderWidth: 1,
|
|
1488
|
-
borderColor: '#ddd',
|
|
1489
|
-
padding: 12,
|
|
1490
|
-
marginBottom: 12,
|
|
1491
|
-
borderRadius: 8,
|
|
1492
|
-
}}
|
|
1493
|
-
/>
|
|
1494
|
-
|
|
1495
|
-
<TextInput
|
|
1496
|
-
placeholder="Username"
|
|
1497
|
-
value={username}
|
|
1498
|
-
onChangeText={setUsername}
|
|
1499
|
-
editable={!busy}
|
|
1500
|
-
style={{
|
|
1501
|
-
borderWidth: 1,
|
|
1502
|
-
borderColor: '#ddd',
|
|
1503
|
-
padding: 12,
|
|
1504
|
-
marginBottom: 12,
|
|
1505
|
-
borderRadius: 8,
|
|
1506
|
-
}}
|
|
1507
|
-
/>
|
|
1508
|
-
|
|
1509
|
-
<TextInput
|
|
1510
|
-
placeholder="Email"
|
|
1511
|
-
value={email}
|
|
1512
|
-
onChangeText={setEmail}
|
|
1513
|
-
keyboardType="email-address"
|
|
1514
|
-
editable={!busy}
|
|
1515
|
-
style={{
|
|
1516
|
-
borderWidth: 1,
|
|
1517
|
-
borderColor: '#ddd',
|
|
1518
|
-
padding: 12,
|
|
1519
|
-
marginBottom: 12,
|
|
1520
|
-
borderRadius: 8,
|
|
1521
|
-
}}
|
|
1522
|
-
/>
|
|
1523
|
-
|
|
1524
|
-
<TextInput
|
|
1525
|
-
placeholder="Password"
|
|
1526
|
-
value={password}
|
|
1527
|
-
onChangeText={setPassword}
|
|
1528
|
-
secureTextEntry
|
|
1529
|
-
editable={!busy}
|
|
1530
|
-
style={{
|
|
1531
|
-
borderWidth: 1,
|
|
1532
|
-
borderColor: '#ddd',
|
|
1533
|
-
padding: 12,
|
|
1534
|
-
marginBottom: 20,
|
|
1535
|
-
borderRadius: 8,
|
|
1536
|
-
}}
|
|
1537
|
-
/>
|
|
1538
|
-
|
|
1539
|
-
<TouchableOpacity
|
|
1540
|
-
onPress={handleSubmit}
|
|
1541
|
-
disabled={busy}
|
|
1542
|
-
style={{
|
|
1543
|
-
backgroundColor: '#0066cc',
|
|
1544
|
-
padding: 12,
|
|
1545
|
-
borderRadius: 8,
|
|
1546
|
-
alignItems: 'center',
|
|
1547
|
-
}}
|
|
1548
|
-
>
|
|
1549
|
-
{busy ? (
|
|
1550
|
-
<ActivityIndicator color="#fff" />
|
|
1551
|
-
) : (
|
|
1552
|
-
<Text style={{ color: '#fff', fontWeight: 'bold' }}>Create Account</Text>
|
|
1553
|
-
)}
|
|
1554
|
-
</TouchableOpacity>
|
|
1555
|
-
|
|
1556
|
-
{error && <Text style={{ color: 'red', marginTop: 12 }}>{error}</Text>}
|
|
1557
|
-
</ScrollView>
|
|
1558
|
-
);
|
|
1559
|
-
}
|
|
1560
|
-
```
|
|
305
|
+
router.post('/register', async (req, res) => {
|
|
306
|
+
const { email, username, password, name, ...extra } = req.body;
|
|
1561
307
|
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
export default function HomeScreen() {
|
|
1579
|
-
const { user, token, logout, refreshProfile, loading, authClient } = useAuth();
|
|
1580
|
-
|
|
1581
|
-
const [feedback, setFeedback] = useState(null);
|
|
1582
|
-
const [busy, setBusy] = useState(false);
|
|
1583
|
-
const [showDeleteForm, setShowDeleteForm] = useState(false);
|
|
1584
|
-
|
|
1585
|
-
async function doChangePassword() {
|
|
1586
|
-
setBusy(true);
|
|
1587
|
-
setFeedback(null);
|
|
1588
|
-
|
|
1589
|
-
try {
|
|
1590
|
-
await authClient.requestChangePasswordLink({ email: user?.email });
|
|
1591
|
-
setFeedback('Password change email sent. Check your inbox.');
|
|
1592
|
-
} catch (err) {
|
|
1593
|
-
setFeedback(err.message || 'Failed to send password change email');
|
|
1594
|
-
} finally {
|
|
1595
|
-
setBusy(false);
|
|
1596
|
-
}
|
|
1597
|
-
}
|
|
1598
|
-
|
|
1599
|
-
async function doDeleteAccount() {
|
|
1600
|
-
Alert.alert(
|
|
1601
|
-
'Delete Account',
|
|
1602
|
-
'Are you sure? This cannot be undone.',
|
|
1603
|
-
[
|
|
1604
|
-
{ text: 'Cancel', style: 'cancel' },
|
|
1605
|
-
{
|
|
1606
|
-
text: 'Delete',
|
|
1607
|
-
style: 'destructive',
|
|
1608
|
-
onPress: async () => {
|
|
1609
|
-
setBusy(true);
|
|
1610
|
-
try {
|
|
1611
|
-
await authClient.deleteAccount({ email: user?.email });
|
|
1612
|
-
setFeedback('Account deleted. Logging out...');
|
|
1613
|
-
setTimeout(() => logout(), 1500);
|
|
1614
|
-
} catch (err) {
|
|
1615
|
-
setFeedback(err.message || 'Delete account failed');
|
|
1616
|
-
} finally {
|
|
1617
|
-
setBusy(false);
|
|
1618
|
-
}
|
|
1619
|
-
},
|
|
1620
|
-
},
|
|
1621
|
-
]
|
|
1622
|
-
);
|
|
308
|
+
try {
|
|
309
|
+
const result = await authclient.register({
|
|
310
|
+
email,
|
|
311
|
+
username,
|
|
312
|
+
password,
|
|
313
|
+
name,
|
|
314
|
+
...extra,
|
|
315
|
+
});
|
|
316
|
+
res.json(result);
|
|
317
|
+
} catch (err) {
|
|
318
|
+
res.status(err.status || 400).json({
|
|
319
|
+
success: false,
|
|
320
|
+
message: err.message || 'Registration failed',
|
|
321
|
+
code: err.code || 'REGISTER_FAILED',
|
|
322
|
+
});
|
|
1623
323
|
}
|
|
324
|
+
});
|
|
1624
325
|
|
|
1625
|
-
|
|
1626
|
-
<ScrollView style={{ flex: 1, padding: 20, backgroundColor: '#fff' }}>
|
|
1627
|
-
<Text style={{ fontSize: 24, fontWeight: 'bold', marginBottom: 24 }}>Welcome</Text>
|
|
1628
|
-
|
|
1629
|
-
<View style={{ backgroundColor: '#f5f5f5', padding: 16, borderRadius: 8, marginBottom: 24 }}>
|
|
1630
|
-
<Text style={{ marginBottom: 8 }}><Text style={{ fontWeight: 'bold' }}>User ID:</Text> {user?.id || 'n/a'}</Text>
|
|
1631
|
-
<Text style={{ marginBottom: 8 }}><Text style={{ fontWeight: 'bold' }}>Email:</Text> {user?.email}</Text>
|
|
1632
|
-
<Text style={{ marginBottom: 8 }}><Text style={{ fontWeight: 'bold' }}>Name:</Text> {user?.name || '—'}</Text>
|
|
1633
|
-
<Text style={{ marginBottom: 8 }}><Text style={{ fontWeight: 'bold' }}>Login Method:</Text> {user?.login_method || '—'}</Text>
|
|
1634
|
-
<Text><Text style={{ fontWeight: 'bold' }}>Token:</Text> {token ? token.slice(0, 18) + '…' : 'none'}</Text>
|
|
1635
|
-
</View>
|
|
1636
|
-
|
|
1637
|
-
<View style={{ flexDirection: 'row', gap: 12, marginBottom: 24 }}>
|
|
1638
|
-
<TouchableOpacity
|
|
1639
|
-
onPress={refreshProfile}
|
|
1640
|
-
disabled={loading}
|
|
1641
|
-
style={{
|
|
1642
|
-
flex: 1,
|
|
1643
|
-
backgroundColor: '#0066cc',
|
|
1644
|
-
padding: 12,
|
|
1645
|
-
borderRadius: 8,
|
|
1646
|
-
alignItems: 'center',
|
|
1647
|
-
}}
|
|
1648
|
-
>
|
|
1649
|
-
{loading ? (
|
|
1650
|
-
<ActivityIndicator color="#fff" />
|
|
1651
|
-
) : (
|
|
1652
|
-
<Text style={{ color: '#fff', fontWeight: 'bold' }}>Refresh Profile</Text>
|
|
1653
|
-
)}
|
|
1654
|
-
</TouchableOpacity>
|
|
1655
|
-
|
|
1656
|
-
<TouchableOpacity
|
|
1657
|
-
onPress={logout}
|
|
1658
|
-
style={{
|
|
1659
|
-
flex: 1,
|
|
1660
|
-
backgroundColor: '#999',
|
|
1661
|
-
padding: 12,
|
|
1662
|
-
borderRadius: 8,
|
|
1663
|
-
alignItems: 'center',
|
|
1664
|
-
}}
|
|
1665
|
-
>
|
|
1666
|
-
<Text style={{ color: '#fff', fontWeight: 'bold' }}>Logout</Text>
|
|
1667
|
-
</TouchableOpacity>
|
|
1668
|
-
</View>
|
|
1669
|
-
|
|
1670
|
-
<View style={{ marginBottom: 24 }}>
|
|
1671
|
-
<Text style={{ fontSize: 18, fontWeight: 'bold', marginBottom: 12 }}>Change Password</Text>
|
|
1672
|
-
<Text style={{ marginBottom: 12 }}>We will email a secure link to {user?.email} to change your password.</Text>
|
|
1673
|
-
<TouchableOpacity
|
|
1674
|
-
onPress={doChangePassword}
|
|
1675
|
-
disabled={busy}
|
|
1676
|
-
style={{
|
|
1677
|
-
backgroundColor: '#0066cc',
|
|
1678
|
-
padding: 12,
|
|
1679
|
-
borderRadius: 8,
|
|
1680
|
-
alignItems: 'center',
|
|
1681
|
-
}}
|
|
1682
|
-
>
|
|
1683
|
-
{busy ? (
|
|
1684
|
-
<ActivityIndicator color="#fff" />
|
|
1685
|
-
) : (
|
|
1686
|
-
<Text style={{ color: '#fff', fontWeight: 'bold' }}>Send Change Password Email</Text>
|
|
1687
|
-
)}
|
|
1688
|
-
</TouchableOpacity>
|
|
1689
|
-
</View>
|
|
1690
|
-
|
|
1691
|
-
{feedback && (
|
|
1692
|
-
<Text style={{ color: feedback.includes('successfully') || feedback.includes('sent') ? 'green' : 'red', marginBottom: 12 }}>
|
|
1693
|
-
{feedback}
|
|
1694
|
-
</Text>
|
|
1695
|
-
)}
|
|
1696
|
-
|
|
1697
|
-
<TouchableOpacity
|
|
1698
|
-
onPress={doDeleteAccount}
|
|
1699
|
-
style={{
|
|
1700
|
-
backgroundColor: '#d32f2f',
|
|
1701
|
-
padding: 12,
|
|
1702
|
-
borderRadius: 8,
|
|
1703
|
-
alignItems: 'center',
|
|
1704
|
-
}}
|
|
1705
|
-
>
|
|
1706
|
-
<Text style={{ color: '#fff', fontWeight: 'bold' }}>Delete Account</Text>
|
|
1707
|
-
</TouchableOpacity>
|
|
1708
|
-
</ScrollView>
|
|
1709
|
-
);
|
|
1710
|
-
}
|
|
326
|
+
export default router;
|
|
1711
327
|
```
|
|
1712
328
|
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
Create `src/screens/SplashScreen.js`:
|
|
1716
|
-
|
|
1717
|
-
```jsx
|
|
1718
|
-
import React from 'react';
|
|
1719
|
-
import { View, ActivityIndicator, Text } from 'react-native';
|
|
329
|
+
---
|
|
1720
330
|
|
|
1721
|
-
|
|
1722
|
-
return (
|
|
1723
|
-
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#fff' }}>
|
|
1724
|
-
<ActivityIndicator size="large" color="#0066cc" />
|
|
1725
|
-
<Text style={{ marginTop: 12 }}>Loading...</Text>
|
|
1726
|
-
</View>
|
|
1727
|
-
);
|
|
1728
|
-
}
|
|
1729
|
-
```
|
|
331
|
+
## Error Handling
|
|
1730
332
|
|
|
1731
|
-
|
|
333
|
+
All methods throw an `AuthError` when the request fails.
|
|
1732
334
|
|
|
1733
|
-
|
|
1734
|
-
import { useAuth } from './AuthContext';
|
|
335
|
+
Shape (simplified):
|
|
1735
336
|
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
}
|
|
1742
|
-
|
|
1743
|
-
return <p>Welcome, {user.name}!</p>;
|
|
337
|
+
```ts
|
|
338
|
+
class AuthError extends Error {
|
|
339
|
+
status: number; // HTTP status code
|
|
340
|
+
code: string; // machine-readable code, e.g. 'EMAIL_NOT_VERIFIED'
|
|
341
|
+
data: any; // full JSON response (optional)
|
|
1744
342
|
}
|
|
1745
343
|
```
|
|
1746
344
|
|
|
1747
|
-
|
|
345
|
+
Example pattern:
|
|
1748
346
|
|
|
1749
|
-
```
|
|
347
|
+
```js
|
|
1750
348
|
try {
|
|
1751
|
-
await
|
|
349
|
+
const res = await authclient.login({ email, password });
|
|
1752
350
|
} catch (err) {
|
|
1753
|
-
// Check error code
|
|
1754
351
|
if (err.code === 'EMAIL_NOT_VERIFIED') {
|
|
1755
|
-
//
|
|
352
|
+
// Ask user to verify email or call resendVerificationEmail
|
|
1756
353
|
} else if (err.status === 401) {
|
|
1757
354
|
// Invalid credentials
|
|
1758
355
|
} else {
|
|
1759
|
-
|
|
1760
|
-
console.error(err.message);
|
|
1761
|
-
}
|
|
1762
|
-
}
|
|
1763
|
-
```
|
|
1764
|
-
|
|
1765
|
-
### 3. Protect Routes/Components
|
|
1766
|
-
|
|
1767
|
-
```jsx
|
|
1768
|
-
function ProtectedRoute({ children }) {
|
|
1769
|
-
const { user } = useAuth();
|
|
1770
|
-
|
|
1771
|
-
if (!user) {
|
|
1772
|
-
return <Navigate to="/login" />;
|
|
356
|
+
console.error(err);
|
|
1773
357
|
}
|
|
1774
|
-
|
|
1775
|
-
return children;
|
|
1776
358
|
}
|
|
1777
359
|
```
|
|
1778
360
|
|
|
1779
|
-
### 4. Auto-refresh User Profile on Mount
|
|
1780
|
-
|
|
1781
|
-
```jsx
|
|
1782
|
-
useEffect(() => {
|
|
1783
|
-
if (user && token) {
|
|
1784
|
-
refreshProfile();
|
|
1785
|
-
}
|
|
1786
|
-
}, []);
|
|
1787
|
-
```
|
|
1788
|
-
|
|
1789
|
-
### 5. Handle Token Expiration
|
|
1790
|
-
|
|
1791
|
-
```jsx
|
|
1792
|
-
const { refreshProfile } = useAuth();
|
|
1793
|
-
|
|
1794
|
-
// Periodically check token validity
|
|
1795
|
-
useEffect(() => {
|
|
1796
|
-
const interval = setInterval(async () => {
|
|
1797
|
-
try {
|
|
1798
|
-
await refreshProfile();
|
|
1799
|
-
} catch (err) {
|
|
1800
|
-
if (err.status === 401) {
|
|
1801
|
-
// Token expired, redirect to login
|
|
1802
|
-
logout();
|
|
1803
|
-
}
|
|
1804
|
-
}
|
|
1805
|
-
}, 5 * 60 * 1000); // Check every 5 minutes
|
|
1806
|
-
|
|
1807
|
-
return () => clearInterval(interval);
|
|
1808
|
-
}, [refreshProfile, logout]);
|
|
1809
|
-
```
|
|
1810
|
-
|
|
1811
361
|
---
|
|
1812
362
|
|
|
1813
|
-
##
|
|
1814
|
-
|
|
1815
|
-
### 1. Handle Token Persistence (React Native)
|
|
1816
|
-
|
|
1817
|
-
```jsx
|
|
1818
|
-
// AuthContext.js already includes this in the bootstrap function
|
|
1819
|
-
React.useEffect(() => {
|
|
1820
|
-
const bootstrapAsync = async () => {
|
|
1821
|
-
try {
|
|
1822
|
-
const savedToken = await AsyncStorage.getItem('auth_user_token');
|
|
1823
|
-
if (savedToken) {
|
|
1824
|
-
authClient.setToken(savedToken);
|
|
1825
|
-
setToken(savedToken);
|
|
1826
|
-
// Fetch user profile
|
|
1827
|
-
const res = await authClient.getProfile();
|
|
1828
|
-
setUser(res.data.user);
|
|
1829
|
-
}
|
|
1830
|
-
} catch (err) {
|
|
1831
|
-
console.error('Failed to restore token', err);
|
|
1832
|
-
} finally {
|
|
1833
|
-
setIsReady(true);
|
|
1834
|
-
}
|
|
1835
|
-
};
|
|
1836
|
-
bootstrapAsync();
|
|
1837
|
-
}, []);
|
|
1838
|
-
```
|
|
1839
|
-
|
|
1840
|
-
### 2. Handle Deep Links (React Native)
|
|
1841
|
-
|
|
1842
|
-
```jsx
|
|
1843
|
-
import * as Linking from 'expo-linking';
|
|
1844
|
-
|
|
1845
|
-
const linking = {
|
|
1846
|
-
prefixes: ['myapp://', 'https://myapp.com'],
|
|
1847
|
-
config: {
|
|
1848
|
-
screens: {
|
|
1849
|
-
ResetPassword: 'reset/:token',
|
|
1850
|
-
VerifyEmail: 'verify/:token',
|
|
1851
|
-
},
|
|
1852
|
-
},
|
|
1853
|
-
};
|
|
1854
|
-
|
|
1855
|
-
export function RootNavigator() {
|
|
1856
|
-
const { user } = useAuth();
|
|
1857
|
-
|
|
1858
|
-
return (
|
|
1859
|
-
<NavigationContainer linking={linking}>
|
|
1860
|
-
{/* Navigation config */}
|
|
1861
|
-
</NavigationContainer>
|
|
1862
|
-
);
|
|
1863
|
-
}
|
|
1864
|
-
```
|
|
363
|
+
## Security Notes
|
|
1865
364
|
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
import { KeyboardAvoidingView, Platform } from 'react-native';
|
|
1870
|
-
|
|
1871
|
-
export default function LoginScreen() {
|
|
1872
|
-
return (
|
|
1873
|
-
<KeyboardAvoidingView
|
|
1874
|
-
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|
1875
|
-
style={{ flex: 1 }}
|
|
1876
|
-
>
|
|
1877
|
-
{/* Form content */}
|
|
1878
|
-
</KeyboardAvoidingView>
|
|
1879
|
-
);
|
|
1880
|
-
}
|
|
1881
|
-
```
|
|
1882
|
-
|
|
1883
|
-
### 4. Handle Biometric Authentication (React Native)
|
|
1884
|
-
|
|
1885
|
-
```jsx
|
|
1886
|
-
import * as SecureStore from 'expo-secure-store';
|
|
1887
|
-
import * as LocalAuthentication from 'expo-local-authentication';
|
|
1888
|
-
|
|
1889
|
-
async function handleBiometricLogin() {
|
|
1890
|
-
try {
|
|
1891
|
-
const compatible = await LocalAuthentication.hasHardwareAsync();
|
|
1892
|
-
if (!compatible) return;
|
|
1893
|
-
|
|
1894
|
-
const savedEmail = await SecureStore.getItemAsync('saved_email');
|
|
1895
|
-
const savedPassword = await SecureStore.getItemAsync('saved_password');
|
|
1896
|
-
|
|
1897
|
-
if (savedEmail && savedPassword) {
|
|
1898
|
-
const result = await LocalAuthentication.authenticateAsync({
|
|
1899
|
-
disableDeviceFallback: false,
|
|
1900
|
-
});
|
|
1901
|
-
|
|
1902
|
-
if (result.success) {
|
|
1903
|
-
await login({ email: savedEmail, password: savedPassword });
|
|
1904
|
-
}
|
|
1905
|
-
}
|
|
1906
|
-
} catch (err) {
|
|
1907
|
-
console.error('Biometric login failed', err);
|
|
1908
|
-
}
|
|
1909
|
-
}
|
|
1910
|
-
```
|
|
365
|
+
- **Backend-only**: Do not expose `apiSecret` in browser or mobile frontend code.
|
|
366
|
+
- Frontend apps should call **your backend**, and your backend uses `@mspkapps/auth-client` to talk to the MSPK Auth Platform.
|
|
367
|
+
- Always keep `apiKey`, `apiSecret`, and `googleClientId` in environment variables in production.
|
|
1911
368
|
|
|
1912
369
|
---
|
|
1913
370
|
|
|
1914
|
-
##
|
|
1915
|
-
|
|
1916
|
-
### Issue: "Failed to execute 'fetch' on 'Window': Illegal invocation"
|
|
371
|
+
## Links & Support
|
|
1917
372
|
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
```javascript
|
|
1921
|
-
fetch: (input, init = {}) => {
|
|
1922
|
-
// ...
|
|
1923
|
-
return window.fetch(input, { ...init, headers });
|
|
1924
|
-
}
|
|
1925
|
-
```
|
|
1926
|
-
|
|
1927
|
-
### Issue: 401 Unauthorized on authenticated requests
|
|
1928
|
-
|
|
1929
|
-
**Solution:** Ensure token is being set and sent correctly:
|
|
1930
|
-
|
|
1931
|
-
1. Check if `authClient.setToken()` is called after login
|
|
1932
|
-
2. Verify the Authorization header format matches your server's expectations
|
|
1933
|
-
3. Check if token is expired
|
|
1934
|
-
|
|
1935
|
-
### Issue: Google Sign-In not working
|
|
1936
|
-
|
|
1937
|
-
**Solution:**
|
|
1938
|
-
1. Verify Google Client ID is correct
|
|
1939
|
-
2. Make sure `GoogleOAuthProvider` wraps your app
|
|
1940
|
-
3. Check browser console for CORS errors
|
|
1941
|
-
4. Ensure your domain is authorized in Google Cloud Console
|
|
1942
|
-
|
|
1943
|
-
### Issue: Email verification emails not arriving
|
|
1944
|
-
|
|
1945
|
-
**Solution:**
|
|
1946
|
-
1. Check spam/junk folder
|
|
1947
|
-
2. Verify email is correct
|
|
1948
|
-
3. Wait a few minutes (email delivery can be delayed)
|
|
1949
|
-
4. Use `resendVerificationEmail()` to request a new one
|
|
1950
|
-
|
|
1951
|
-
### Issue: Password reset link not working
|
|
1952
|
-
|
|
1953
|
-
**Solution:**
|
|
1954
|
-
1. Check if link has expired (usually 1 hour)
|
|
1955
|
-
2. Request a new link using `requestPasswordReset()`
|
|
1956
|
-
3. Ensure you're using the correct email address
|
|
1957
|
-
|
|
1958
|
-
---
|
|
1959
|
-
|
|
1960
|
-
## Platform-Specific Considerations
|
|
1961
|
-
|
|
1962
|
-
### React Web vs React Native
|
|
1963
|
-
|
|
1964
|
-
| Feature | React Web | React Native |
|
|
1965
|
-
|---------|-----------|--------------|
|
|
1966
|
-
| **Storage** | localStorage (default) | AsyncStorage |
|
|
1967
|
-
| **Navigation** | React Router | React Navigation |
|
|
1968
|
-
| **Google Sign-In** | @react-oauth/google | @react-native-google-signin/google-signin |
|
|
1969
|
-
| **Token Persistence** | Optional (storage: null) | Recommended (AsyncStorage) |
|
|
1970
|
-
| **Network Requests** | window.fetch | Global fetch |
|
|
1971
|
-
| **UI Components** | HTML/CSS | React Native components |
|
|
1972
|
-
| **Deep Linking** | Browser URL | Expo Linking or Deep Linking |
|
|
1973
|
-
|
|
1974
|
-
### Key Differences in Implementation
|
|
1975
|
-
|
|
1976
|
-
#### 1. Token Storage
|
|
1977
|
-
|
|
1978
|
-
**React Web:**
|
|
1979
|
-
```javascript
|
|
1980
|
-
storage: null // In-memory only
|
|
1981
|
-
// or
|
|
1982
|
-
storage: window.localStorage // Persistent
|
|
1983
|
-
```
|
|
1984
|
-
|
|
1985
|
-
**React Native:**
|
|
1986
|
-
```javascript
|
|
1987
|
-
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
1988
|
-
storage: AsyncStorage // Persistent and secure
|
|
1989
|
-
```
|
|
1990
|
-
|
|
1991
|
-
#### 2. Navigation
|
|
1992
|
-
|
|
1993
|
-
**React Web:**
|
|
1994
|
-
```jsx
|
|
1995
|
-
// Uses React Router or conditional rendering
|
|
1996
|
-
{user ? <Home /> : <Login />}
|
|
1997
|
-
```
|
|
1998
|
-
|
|
1999
|
-
**React Native:**
|
|
2000
|
-
```jsx
|
|
2001
|
-
// Uses React Navigation
|
|
2002
|
-
{user ? (
|
|
2003
|
-
<Stack.Screen name="Home" component={HomeScreen} />
|
|
2004
|
-
) : (
|
|
2005
|
-
<Stack.Screen name="Login" component={LoginScreen} />
|
|
2006
|
-
)}
|
|
2007
|
-
```
|
|
2008
|
-
|
|
2009
|
-
#### 3. Loading States
|
|
2010
|
-
|
|
2011
|
-
**React Web:**
|
|
2012
|
-
```jsx
|
|
2013
|
-
<button disabled={loading}>
|
|
2014
|
-
{loading ? 'Please wait…' : 'Login'}
|
|
2015
|
-
</button>
|
|
2016
|
-
```
|
|
2017
|
-
|
|
2018
|
-
**React Native:**
|
|
2019
|
-
```jsx
|
|
2020
|
-
<TouchableOpacity disabled={loading}>
|
|
2021
|
-
{loading ? <ActivityIndicator /> : <Text>Login</Text>}
|
|
2022
|
-
</TouchableOpacity>
|
|
2023
|
-
```
|
|
2024
|
-
|
|
2025
|
-
#### 4. Styling
|
|
2026
|
-
|
|
2027
|
-
**React Web:**
|
|
2028
|
-
```jsx
|
|
2029
|
-
<button style={{ color: 'white', backgroundColor: 'blue' }}>
|
|
2030
|
-
Login
|
|
2031
|
-
</button>
|
|
2032
|
-
```
|
|
2033
|
-
|
|
2034
|
-
**React Native:**
|
|
2035
|
-
```jsx
|
|
2036
|
-
<TouchableOpacity style={{ backgroundColor: 'blue' }}>
|
|
2037
|
-
<Text style={{ color: 'white' }}>Login</Text>
|
|
2038
|
-
</TouchableOpacity>
|
|
2039
|
-
```
|
|
2040
|
-
|
|
2041
|
-
---
|
|
2042
|
-
|
|
2043
|
-
## Security Best Practices
|
|
2044
|
-
|
|
2045
|
-
### 1. **Never expose API secrets in production**
|
|
2046
|
-
|
|
2047
|
-
In production, move API secret to your backend server:
|
|
2048
|
-
|
|
2049
|
-
```javascript
|
|
2050
|
-
// Instead of this (insecure):
|
|
2051
|
-
const authClient = AuthClient.create(PUBLIC_KEY, API_SECRET);
|
|
2052
|
-
|
|
2053
|
-
// Do this (secure):
|
|
2054
|
-
// Have your backend proxy all auth requests
|
|
2055
|
-
// Your frontend only sends requests to YOUR server
|
|
2056
|
-
// Your server adds the API secret and forwards to auth server
|
|
2057
|
-
```
|
|
2058
|
-
|
|
2059
|
-
### 2. **Use HTTPS in production**
|
|
2060
|
-
|
|
2061
|
-
Always serve your app over HTTPS to prevent token interception.
|
|
2062
|
-
|
|
2063
|
-
### 3. **Implement token refresh**
|
|
2064
|
-
|
|
2065
|
-
Periodically refresh the user's profile to detect token expiration early.
|
|
2066
|
-
|
|
2067
|
-
### 4. **Clear sensitive data on logout**
|
|
2068
|
-
|
|
2069
|
-
```javascript
|
|
2070
|
-
const logout = () => {
|
|
2071
|
-
setUser(null);
|
|
2072
|
-
setToken(null);
|
|
2073
|
-
authClient.token = null;
|
|
2074
|
-
// Clear any other sensitive state
|
|
2075
|
-
};
|
|
2076
|
-
```
|
|
2077
|
-
|
|
2078
|
-
### 5. **Validate user input**
|
|
2079
|
-
|
|
2080
|
-
Always validate email format, password strength, etc. before sending to server.
|
|
2081
|
-
|
|
2082
|
-
---
|
|
2083
|
-
|
|
2084
|
-
## Additional Resources
|
|
2085
|
-
|
|
2086
|
-
- **npm Package:** [@mspkapps/auth-client](https://www.npmjs.com/package/@mspkapps/auth-client)
|
|
2087
|
-
- **Google OAuth Setup (Web):** [React OAuth Google Docs](https://www.npmjs.com/package/@react-oauth/google)
|
|
2088
|
-
- **Google OAuth Setup (React Native):** [@react-native-google-signin](https://www.npmjs.com/package/@react-native-google-signin/google-signin)
|
|
2089
|
-
- **React Documentation:** [React Official Docs](https://react.dev)
|
|
2090
|
-
- **React Native Documentation:** [React Native Official Docs](https://reactnative.dev)
|
|
2091
|
-
- **React Navigation:** [React Navigation Docs](https://reactnavigation.org)
|
|
2092
|
-
- **AsyncStorage (React Native):** [AsyncStorage Documentation](https://react-native-async-storage.github.io)
|
|
2093
|
-
|
|
2094
|
-
---
|
|
2095
|
-
|
|
2096
|
-
## Support
|
|
2097
|
-
|
|
2098
|
-
For questions or issues:
|
|
2099
|
-
1. Check this documentation first
|
|
2100
|
-
2. Review the demo code in this project
|
|
2101
|
-
3. Check package documentation on npm
|
|
2102
|
-
4. Contact the auth server maintainers
|
|
2103
|
-
|
|
2104
|
-
---
|
|
373
|
+
- npm: `@mspkapps/auth-client`
|
|
374
|
+
- MSPK™ Auth Platform dashboard: (URL you provide in your docs)
|
|
2105
375
|
|
|
2106
|
-
|
|
376
|
+
For issues or feature requests, open an issue in your repository or contact MSPK™ Auth Platform support.
|