@mspkapps/auth-client 0.1.24 → 0.1.26
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 +568 -261
- package/package.json +1 -1
- package/src/AuthClient.js +3 -3
package/README.md
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
|
-
#
|
|
1
|
+
# AuthClient NPM Package Documentation (Backend-Only Usage)
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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(...)`
|
|
3
|
+
> All usage of `@mspkapps/auth-client` and all API keys/secrets **must live in your backend only**.
|
|
4
|
+
> Frontend apps (React / React Native) should **never** import this package or see the keys.
|
|
5
|
+
> They only call your backend, and your backend calls the AuthClient.
|
|
13
6
|
|
|
14
7
|
---
|
|
15
8
|
|
|
16
|
-
##
|
|
9
|
+
## 1. Backend Setup (Node / Express)
|
|
10
|
+
|
|
11
|
+
### 1.1 Install in Backend Only
|
|
12
|
+
|
|
13
|
+
In your backend project (e.g. `Backend/`):
|
|
17
14
|
|
|
18
15
|
```bash
|
|
19
16
|
npm install @mspkapps/auth-client
|
|
@@ -21,356 +18,666 @@ npm install @mspkapps/auth-client
|
|
|
21
18
|
yarn add @mspkapps/auth-client
|
|
22
19
|
```
|
|
23
20
|
|
|
24
|
-
|
|
21
|
+
Do **not** install this package in your frontend projects.
|
|
25
22
|
|
|
26
|
-
|
|
23
|
+
### 1.2 Environment Variables (Backend)
|
|
27
24
|
|
|
28
|
-
|
|
25
|
+
Create `.env` in your backend root:
|
|
29
26
|
|
|
30
|
-
|
|
27
|
+
```env
|
|
28
|
+
MSPK_AUTH_API_KEY=your_api_key_here
|
|
29
|
+
MSPK_AUTH_API_SECRET=your_api_secret_here
|
|
30
|
+
GOOGLE_CLIENT_ID=your_google_client_id_here
|
|
31
|
+
```
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
### 1.3 Initialize AuthClient Singleton
|
|
34
|
+
|
|
35
|
+
Create `src/auth/authClient.js` in your backend:
|
|
36
|
+
|
|
37
|
+
```javascript
|
|
34
38
|
import authclient from '@mspkapps/auth-client';
|
|
35
39
|
|
|
40
|
+
// Initialize once at backend startup
|
|
36
41
|
authclient.init({
|
|
37
42
|
apiKey: process.env.MSPK_AUTH_API_KEY,
|
|
38
43
|
apiSecret: process.env.MSPK_AUTH_API_SECRET,
|
|
39
|
-
googleClientId: process.env.GOOGLE_CLIENT_ID
|
|
40
|
-
//
|
|
44
|
+
googleClientId: process.env.GOOGLE_CLIENT_ID
|
|
45
|
+
// storage: omit on backend (no localStorage)
|
|
46
|
+
// fetch: use global fetch (Node 18+) or pass custom
|
|
41
47
|
});
|
|
42
48
|
|
|
43
49
|
export default authclient;
|
|
44
50
|
```
|
|
45
51
|
|
|
46
|
-
###
|
|
52
|
+
### 1.4 Express Routes That Proxy to AuthClient
|
|
47
53
|
|
|
48
|
-
|
|
49
|
-
// exampleRoute.js
|
|
50
|
-
import authclient from './authClient.js';
|
|
54
|
+
Example `src/routes/authRoutes.js`:
|
|
51
55
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
```javascript
|
|
57
|
+
import express from 'express';
|
|
58
|
+
import authclient from '../auth/authClient.js';
|
|
59
|
+
import { AuthError } from '@mspkapps/auth-client';
|
|
55
60
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
res.status(err.status || 400).json({
|
|
61
|
+
const router = express.Router();
|
|
62
|
+
|
|
63
|
+
// Helper to normalize errors for the frontend
|
|
64
|
+
function handleError(res, err, fallback = 'Request failed') {
|
|
65
|
+
if (err instanceof AuthError) {
|
|
66
|
+
return res.status(err.status || 400).json({
|
|
62
67
|
success: false,
|
|
63
|
-
message: err.message ||
|
|
64
|
-
code: err.code || '
|
|
68
|
+
message: err.message || fallback,
|
|
69
|
+
code: err.code || 'REQUEST_FAILED',
|
|
70
|
+
data: err.response?.data ?? null,
|
|
65
71
|
});
|
|
66
72
|
}
|
|
73
|
+
|
|
74
|
+
console.error('Unexpected auth error:', err);
|
|
75
|
+
return res.status(500).json({
|
|
76
|
+
success: false,
|
|
77
|
+
message: fallback,
|
|
78
|
+
code: 'INTERNAL_ERROR',
|
|
79
|
+
});
|
|
67
80
|
}
|
|
68
|
-
```
|
|
69
81
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
// POST /api/auth/register
|
|
83
|
+
router.post('/register', async (req, res) => {
|
|
84
|
+
try {
|
|
85
|
+
const { email, username, password, name, extra } = req.body;
|
|
86
|
+
const resp = await authclient.register({ email, username, password, name, extra });
|
|
87
|
+
return res.json(resp);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
return handleError(res, err, 'Registration failed');
|
|
90
|
+
}
|
|
91
|
+
});
|
|
74
92
|
|
|
93
|
+
// POST /api/auth/login
|
|
94
|
+
router.post('/login', async (req, res) => {
|
|
75
95
|
try {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
password,
|
|
80
|
-
name,
|
|
81
|
-
...extra, // custom fields, if configured in MSPK
|
|
82
|
-
});
|
|
83
|
-
res.json(result);
|
|
96
|
+
const { email, username, password } = req.body;
|
|
97
|
+
const resp = await authclient.login({ email, username, password });
|
|
98
|
+
return res.json(resp);
|
|
84
99
|
} catch (err) {
|
|
85
|
-
res
|
|
86
|
-
success: false,
|
|
87
|
-
message: err.message || 'Registration failed',
|
|
88
|
-
code: err.code || 'REGISTER_FAILED',
|
|
89
|
-
});
|
|
100
|
+
return handleError(res, err, 'Login failed');
|
|
90
101
|
}
|
|
91
|
-
}
|
|
92
|
-
```
|
|
102
|
+
});
|
|
93
103
|
|
|
94
|
-
|
|
104
|
+
// POST /api/auth/google
|
|
105
|
+
router.post('/google', async (req, res) => {
|
|
106
|
+
try {
|
|
107
|
+
const { id_token } = req.body; // Frontend sends Google ID token
|
|
108
|
+
const resp = await authclient.googleAuth({ id_token });
|
|
109
|
+
return res.json(resp);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
return handleError(res, err, 'Google auth failed');
|
|
112
|
+
}
|
|
113
|
+
});
|
|
95
114
|
|
|
96
|
-
|
|
115
|
+
// POST /api/auth/request-password-reset
|
|
116
|
+
router.post('/request-password-reset', async (req, res) => {
|
|
117
|
+
try {
|
|
118
|
+
const { email } = req.body;
|
|
119
|
+
const resp = await authclient.client.requestPasswordReset({ email });
|
|
120
|
+
return res.json(resp);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
return handleError(res, err, 'Password reset request failed');
|
|
123
|
+
}
|
|
124
|
+
});
|
|
97
125
|
|
|
98
|
-
|
|
126
|
+
// POST /api/auth/request-change-password-link
|
|
127
|
+
router.post('/request-change-password-link', async (req, res) => {
|
|
128
|
+
try {
|
|
129
|
+
const { email } = req.body;
|
|
130
|
+
const resp = await authclient.client.requestChangePasswordLink({ email });
|
|
131
|
+
return res.json(resp);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
return handleError(res, err, 'Change password link request failed');
|
|
134
|
+
}
|
|
135
|
+
});
|
|
99
136
|
|
|
100
|
-
|
|
101
|
-
|
|
137
|
+
// POST /api/auth/resend-verification
|
|
138
|
+
router.post('/resend-verification', async (req, res) => {
|
|
139
|
+
try {
|
|
140
|
+
const { email, purpose } = req.body;
|
|
141
|
+
const resp = await authclient.client.resendVerificationEmail({ email, purpose });
|
|
142
|
+
return res.json(resp);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
return handleError(res, err, 'Resend verification failed');
|
|
145
|
+
}
|
|
146
|
+
});
|
|
102
147
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
148
|
+
// POST /api/auth/delete-account
|
|
149
|
+
router.post('/delete-account', async (req, res) => {
|
|
150
|
+
try {
|
|
151
|
+
const { email, password } = req.body;
|
|
152
|
+
const resp = await authclient.client.deleteAccount({ email, password });
|
|
153
|
+
return res.json(resp);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
return handleError(res, err, 'Delete account failed');
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// POST /api/auth/verify-token
|
|
160
|
+
router.post('/verify-token', async (req, res) => {
|
|
161
|
+
try {
|
|
162
|
+
const { accessToken } = req.body;
|
|
163
|
+
const data = await authclient.verifyToken(accessToken);
|
|
164
|
+
return res.json({ success: true, data });
|
|
165
|
+
} catch (err) {
|
|
166
|
+
return handleError(res, err, 'Token verification failed');
|
|
167
|
+
}
|
|
107
168
|
});
|
|
169
|
+
|
|
170
|
+
export default router;
|
|
108
171
|
```
|
|
109
172
|
|
|
110
|
-
###
|
|
173
|
+
### 1.5 Protected User Routes (Backend)
|
|
111
174
|
|
|
112
|
-
|
|
175
|
+
You typically store the `user_token` provided by AuthClient in your own session/JWT.
|
|
176
|
+
Example `src/middleware/requireAuth.js` that verifies your own access token using `authclient.verifyToken`:
|
|
113
177
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
- `password` (string)
|
|
117
|
-
- Returns:
|
|
118
|
-
- User data and user token; token is stored internally via `setToken`.
|
|
178
|
+
```javascript
|
|
179
|
+
import authclient from '../auth/authClient.js';
|
|
119
180
|
|
|
120
|
-
|
|
181
|
+
export async function requireAuth(req, res, next) {
|
|
182
|
+
try {
|
|
183
|
+
const authHeader = req.headers.authorization || '';
|
|
184
|
+
const token = authHeader.replace(/^Bearer\s+/i, '');
|
|
121
185
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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.
|
|
186
|
+
if (!token) {
|
|
187
|
+
return res.status(401).json({ success: false, message: 'Missing access token' });
|
|
188
|
+
}
|
|
131
189
|
|
|
132
|
-
|
|
133
|
-
|
|
190
|
+
const data = await authclient.verifyToken(token);
|
|
191
|
+
req.user = data; // attach decoded user data to request
|
|
192
|
+
return next();
|
|
193
|
+
} catch (err) {
|
|
194
|
+
console.error('Auth middleware error:', err);
|
|
195
|
+
return res.status(401).json({ success: false, message: 'Invalid or expired token' });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
134
199
|
|
|
135
|
-
|
|
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`.
|
|
200
|
+
Example profile routes in `src/routes/userRoutes.js`:
|
|
142
201
|
|
|
143
|
-
|
|
202
|
+
```javascript
|
|
203
|
+
import express from 'express';
|
|
204
|
+
import authclient from '../auth/authClient.js';
|
|
205
|
+
import { requireAuth } from '../middleware/requireAuth.js';
|
|
144
206
|
|
|
145
|
-
|
|
207
|
+
const router = express.Router();
|
|
146
208
|
|
|
147
|
-
|
|
209
|
+
// GET /api/user/profile
|
|
210
|
+
router.get('/profile', requireAuth, async (req, res) => {
|
|
211
|
+
try {
|
|
212
|
+
const resp = await authclient.getProfile();
|
|
213
|
+
return res.json(resp);
|
|
214
|
+
} catch (err) {
|
|
215
|
+
console.error('Get profile failed:', err);
|
|
216
|
+
return res.status(500).json({ success: false, message: 'Get profile failed' });
|
|
217
|
+
}
|
|
218
|
+
});
|
|
148
219
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
220
|
+
// PATCH /api/user/profile
|
|
221
|
+
router.patch('/profile', requireAuth, async (req, res) => {
|
|
222
|
+
try {
|
|
223
|
+
const updates = req.body;
|
|
224
|
+
const resp = await authclient.updateProfile(updates);
|
|
225
|
+
return res.json(resp);
|
|
226
|
+
} catch (err) {
|
|
227
|
+
console.error('Update profile failed:', err);
|
|
228
|
+
return res.status(500).json({ success: false, message: 'Update profile failed' });
|
|
229
|
+
}
|
|
230
|
+
});
|
|
152
231
|
|
|
153
|
-
|
|
232
|
+
export default router;
|
|
233
|
+
```
|
|
154
234
|
|
|
155
|
-
|
|
156
|
-
- `email` (string)
|
|
157
|
-
- Use when: logged-in user requests a “change password” email from settings/profile.
|
|
235
|
+
### 1.6 Wire Up Routes in Backend App
|
|
158
236
|
|
|
159
|
-
|
|
237
|
+
In your backend `src/app.js`:
|
|
160
238
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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`.
|
|
239
|
+
```javascript
|
|
240
|
+
import express from 'express';
|
|
241
|
+
import cors from 'cors';
|
|
242
|
+
import authRoutes from './routes/authRoutes.js';
|
|
243
|
+
import userRoutes from './routes/userRoutes.js';
|
|
172
244
|
|
|
173
|
-
|
|
245
|
+
const app = express();
|
|
174
246
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
247
|
+
app.use(cors({ origin: ['http://localhost:5173', 'http://localhost:3000'], credentials: true }));
|
|
248
|
+
app.use(express.json());
|
|
249
|
+
|
|
250
|
+
app.use('/api/auth', authRoutes);
|
|
251
|
+
app.use('/api/user', userRoutes);
|
|
252
|
+
|
|
253
|
+
export default app;
|
|
254
|
+
```
|
|
178
255
|
|
|
179
256
|
---
|
|
180
257
|
|
|
181
|
-
|
|
258
|
+
## 2. React (Vite) Frontend – Call Your Backend
|
|
182
259
|
|
|
183
|
-
|
|
260
|
+
Your React app **does not** import `@mspkapps/auth-client` and knows nothing about API keys.
|
|
261
|
+
It only calls your backend routes like `/api/auth/login`, `/api/auth/register`, etc.
|
|
184
262
|
|
|
185
|
-
|
|
186
|
-
- `email` (string)
|
|
187
|
-
- `password` (string) – current password (depending on your server rules).
|
|
188
|
-
- Use when: user confirms account deletion.
|
|
263
|
+
### 2.1 API Service
|
|
189
264
|
|
|
190
|
-
|
|
265
|
+
Create `src/services/authApi.js` in your React app:
|
|
191
266
|
|
|
192
|
-
|
|
267
|
+
```javascript
|
|
268
|
+
const API_BASE_URL = import.meta.env.VITE_BACKEND_URL || 'http://localhost:4000';
|
|
193
269
|
|
|
194
|
-
|
|
270
|
+
export async function apiLogin({ email, password, username }) {
|
|
271
|
+
const resp = await fetch(`${API_BASE_URL}/api/auth/login`, {
|
|
272
|
+
method: 'POST',
|
|
273
|
+
headers: { 'Content-Type': 'application/json' },
|
|
274
|
+
body: JSON.stringify({ email, password, username }),
|
|
275
|
+
});
|
|
195
276
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
277
|
+
const json = await resp.json();
|
|
278
|
+
if (!resp.ok || json?.success === false) {
|
|
279
|
+
throw new Error(json?.message || 'Login failed');
|
|
280
|
+
}
|
|
281
|
+
return json;
|
|
282
|
+
}
|
|
199
283
|
|
|
200
|
-
|
|
284
|
+
export async function apiRegister(payload) {
|
|
285
|
+
const resp = await fetch(`${API_BASE_URL}/api/auth/register`, {
|
|
286
|
+
method: 'POST',
|
|
287
|
+
headers: { 'Content-Type': 'application/json' },
|
|
288
|
+
body: JSON.stringify(payload),
|
|
289
|
+
});
|
|
290
|
+
const json = await resp.json();
|
|
291
|
+
if (!resp.ok || json?.success === false) {
|
|
292
|
+
throw new Error(json?.message || 'Registration failed');
|
|
293
|
+
}
|
|
294
|
+
return json;
|
|
295
|
+
}
|
|
201
296
|
|
|
202
|
-
|
|
203
|
-
|
|
297
|
+
export async function apiGoogleLogin(idToken) {
|
|
298
|
+
const resp = await fetch(`${API_BASE_URL}/api/auth/google`, {
|
|
299
|
+
method: 'POST',
|
|
300
|
+
headers: { 'Content-Type': 'application/json' },
|
|
301
|
+
body: JSON.stringify({ id_token: idToken }),
|
|
302
|
+
});
|
|
303
|
+
const json = await resp.json();
|
|
304
|
+
if (!resp.ok || json?.success === false) {
|
|
305
|
+
throw new Error(json?.message || 'Google login failed');
|
|
306
|
+
}
|
|
307
|
+
return json;
|
|
308
|
+
}
|
|
204
309
|
|
|
205
|
-
|
|
310
|
+
export async function apiGetProfile(accessToken) {
|
|
311
|
+
const resp = await fetch(`${API_BASE_URL}/api/user/profile`, {
|
|
312
|
+
method: 'GET',
|
|
313
|
+
headers: {
|
|
314
|
+
'Content-Type': 'application/json',
|
|
315
|
+
Authorization: `Bearer ${accessToken}`,
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
const json = await resp.json();
|
|
319
|
+
if (!resp.ok || json?.success === false) {
|
|
320
|
+
throw new Error(json?.message || 'Get profile failed');
|
|
321
|
+
}
|
|
322
|
+
return json;
|
|
323
|
+
}
|
|
324
|
+
```
|
|
206
325
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
326
|
+
### 2.2 Simple Auth Context (Frontend-Only State)
|
|
327
|
+
|
|
328
|
+
Create `src/context/AuthContext.jsx`:
|
|
329
|
+
|
|
330
|
+
```jsx
|
|
331
|
+
import { createContext, useContext, useState, useEffect } from 'react';
|
|
332
|
+
import { apiLogin, apiRegister, apiGoogleLogin, apiGetProfile } from '../services/authApi';
|
|
333
|
+
|
|
334
|
+
const AuthContext = createContext(null);
|
|
335
|
+
|
|
336
|
+
export const AuthProvider = ({ children }) => {
|
|
337
|
+
const [user, setUser] = useState(null);
|
|
338
|
+
const [accessToken, setAccessToken] = useState(null);
|
|
339
|
+
const [loading, setLoading] = useState(true);
|
|
340
|
+
|
|
341
|
+
useEffect(() => {
|
|
342
|
+
const token = window.localStorage.getItem('access_token');
|
|
343
|
+
if (token) {
|
|
344
|
+
setAccessToken(token);
|
|
345
|
+
refreshProfile(token).finally(() => setLoading(false));
|
|
346
|
+
} else {
|
|
347
|
+
setLoading(false);
|
|
348
|
+
}
|
|
349
|
+
}, []);
|
|
350
|
+
|
|
351
|
+
const refreshProfile = async (token) => {
|
|
352
|
+
try {
|
|
353
|
+
const resp = await apiGetProfile(token);
|
|
354
|
+
setUser(resp.data);
|
|
355
|
+
} catch {
|
|
356
|
+
setUser(null);
|
|
357
|
+
setAccessToken(null);
|
|
358
|
+
window.localStorage.removeItem('access_token');
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const login = async (credentials) => {
|
|
363
|
+
const resp = await apiLogin(credentials);
|
|
364
|
+
const token = resp?.data?.access_token || resp?.data?.user_token;
|
|
365
|
+
if (token) {
|
|
366
|
+
setAccessToken(token);
|
|
367
|
+
window.localStorage.setItem('access_token', token);
|
|
368
|
+
await refreshProfile(token);
|
|
369
|
+
}
|
|
370
|
+
return resp;
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const register = async (payload) => {
|
|
374
|
+
const resp = await apiRegister(payload);
|
|
375
|
+
const token = resp?.data?.access_token || resp?.data?.user_token;
|
|
376
|
+
if (token) {
|
|
377
|
+
setAccessToken(token);
|
|
378
|
+
window.localStorage.setItem('access_token', token);
|
|
379
|
+
await refreshProfile(token);
|
|
380
|
+
}
|
|
381
|
+
return resp;
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const googleLogin = async (idToken) => {
|
|
385
|
+
const resp = await apiGoogleLogin(idToken);
|
|
386
|
+
const token = resp?.data?.access_token || resp?.data?.user_token;
|
|
387
|
+
if (token) {
|
|
388
|
+
setAccessToken(token);
|
|
389
|
+
window.localStorage.setItem('access_token', token);
|
|
390
|
+
await refreshProfile(token);
|
|
391
|
+
}
|
|
392
|
+
return resp;
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const logout = () => {
|
|
396
|
+
setUser(null);
|
|
397
|
+
setAccessToken(null);
|
|
398
|
+
window.localStorage.removeItem('access_token');
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
return (
|
|
402
|
+
<AuthContext.Provider
|
|
403
|
+
value={{ user, accessToken, loading, login, register, googleLogin, logout, isAuthenticated: !!user }}
|
|
404
|
+
>
|
|
405
|
+
{children}
|
|
406
|
+
</AuthContext.Provider>
|
|
407
|
+
);
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
export const useAuth = () => {
|
|
411
|
+
const ctx = useContext(AuthContext);
|
|
412
|
+
if (!ctx) throw new Error('useAuth must be used within AuthProvider');
|
|
413
|
+
return ctx;
|
|
414
|
+
};
|
|
217
415
|
```
|
|
218
416
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
417
|
+
### 2.3 Example Login Page (React Vite)
|
|
418
|
+
|
|
419
|
+
```jsx
|
|
420
|
+
import { useState } from 'react';
|
|
421
|
+
import { useAuth } from '../context/AuthContext';
|
|
422
|
+
|
|
423
|
+
function LoginPage() {
|
|
424
|
+
const { login } = useAuth();
|
|
425
|
+
const [email, setEmail] = useState('');
|
|
426
|
+
const [password, setPassword] = useState('');
|
|
427
|
+
const [error, setError] = useState('');
|
|
428
|
+
|
|
429
|
+
const handleSubmit = async (e) => {
|
|
430
|
+
e.preventDefault();
|
|
431
|
+
setError('');
|
|
432
|
+
try {
|
|
433
|
+
await login({ email, password });
|
|
434
|
+
// navigate to dashboard
|
|
435
|
+
} catch (err) {
|
|
436
|
+
setError(err.message);
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
return (
|
|
441
|
+
<form onSubmit={handleSubmit}>
|
|
442
|
+
<input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" />
|
|
443
|
+
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Password" />
|
|
444
|
+
<button type="submit">Login</button>
|
|
445
|
+
{error && <div style={{ color: 'red' }}>{error}</div>}
|
|
446
|
+
</form>
|
|
447
|
+
);
|
|
448
|
+
}
|
|
223
449
|
|
|
224
|
-
|
|
450
|
+
export default LoginPage;
|
|
451
|
+
```
|
|
225
452
|
|
|
226
|
-
###
|
|
453
|
+
### 2.4 Google Login (React Vite)
|
|
227
454
|
|
|
228
|
-
|
|
455
|
+
Frontend just gets the Google `credential` and posts it to your backend:
|
|
229
456
|
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
method: 'PATCH',
|
|
233
|
-
body: { name: 'Another Name' },
|
|
234
|
-
headers: {
|
|
235
|
-
'X-Custom-Header': '123',
|
|
236
|
-
},
|
|
237
|
-
});
|
|
457
|
+
```bash
|
|
458
|
+
npm install @react-oauth/google
|
|
238
459
|
```
|
|
239
460
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
461
|
+
```jsx
|
|
462
|
+
import { GoogleOAuthProvider, GoogleLogin } from '@react-oauth/google';
|
|
463
|
+
import { useAuth } from '../context/AuthContext';
|
|
464
|
+
|
|
465
|
+
// In your root
|
|
466
|
+
<GoogleOAuthProvider clientId={import.meta.env.VITE_GOOGLE_CLIENT_ID}>
|
|
467
|
+
<AuthProvider>
|
|
468
|
+
<App />
|
|
469
|
+
</AuthProvider>
|
|
470
|
+
</GoogleOAuthProvider>;
|
|
471
|
+
|
|
472
|
+
// In login page
|
|
473
|
+
function LoginPage() {
|
|
474
|
+
const { googleLogin } = useAuth();
|
|
475
|
+
|
|
476
|
+
return (
|
|
477
|
+
<GoogleLogin
|
|
478
|
+
onSuccess={async (credentialResponse) => {
|
|
479
|
+
try {
|
|
480
|
+
await googleLogin(credentialResponse.credential);
|
|
481
|
+
} catch (err) {
|
|
482
|
+
console.error('Google login failed', err);
|
|
483
|
+
}
|
|
484
|
+
}}
|
|
485
|
+
onError={() => console.log('Google login error')}
|
|
486
|
+
/>
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
```
|
|
249
490
|
|
|
250
|
-
|
|
491
|
+
The Google ID token is sent to `/api/auth/google` on your backend, which then calls `authclient.googleAuth`.
|
|
251
492
|
|
|
252
|
-
|
|
493
|
+
---
|
|
253
494
|
|
|
254
|
-
|
|
255
|
-
- `token` (string or `null`).
|
|
256
|
-
- Usually not needed, because:
|
|
257
|
-
- `login`, `register`, and `googleAuth` will set the token automatically.
|
|
495
|
+
## 3. React Native CLI – Call Your Backend
|
|
258
496
|
|
|
259
|
-
|
|
497
|
+
React Native app also **never** imports `@mspkapps/auth-client`. It talks only to your backend.
|
|
260
498
|
|
|
261
|
-
|
|
262
|
-
- Clears the stored token in memory (and storage, if configured).
|
|
499
|
+
### 3.1 API Service (React Native)
|
|
263
500
|
|
|
264
|
-
|
|
501
|
+
Create `src/services/authApi.js`:
|
|
265
502
|
|
|
266
|
-
|
|
503
|
+
```javascript
|
|
504
|
+
const API_BASE_URL = process.env.BACKEND_URL || 'http://10.0.2.2:4000'; // Android emulator example
|
|
267
505
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
506
|
+
async function request(path, { method = 'GET', body, token } = {}) {
|
|
507
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
508
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
271
509
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
});
|
|
510
|
+
const resp = await fetch(`${API_BASE_URL}${path}`, {
|
|
511
|
+
method,
|
|
512
|
+
headers,
|
|
513
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
514
|
+
});
|
|
515
|
+
const json = await resp.json();
|
|
516
|
+
if (!resp.ok || json?.success === false) {
|
|
517
|
+
throw new Error(json?.message || 'Request failed');
|
|
518
|
+
}
|
|
519
|
+
return json;
|
|
520
|
+
}
|
|
277
521
|
|
|
278
|
-
export
|
|
522
|
+
export const apiLogin = (payload) => request('/api/auth/login', { method: 'POST', body: payload });
|
|
523
|
+
export const apiRegister = (payload) => request('/api/auth/register', { method: 'POST', body: payload });
|
|
524
|
+
export const apiGoogleLogin = (idToken) =>
|
|
525
|
+
request('/api/auth/google', { method: 'POST', body: { id_token: idToken } });
|
|
526
|
+
export const apiGetProfile = (token) => request('/api/user/profile', { method: 'GET', token });
|
|
279
527
|
```
|
|
280
528
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
import
|
|
285
|
-
|
|
286
|
-
|
|
529
|
+
### 3.2 Auth Context (React Native)
|
|
530
|
+
|
|
531
|
+
```javascript
|
|
532
|
+
import React, { createContext, useContext, useEffect, useState } from 'react';
|
|
533
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
534
|
+
import { apiLogin, apiRegister, apiGoogleLogin, apiGetProfile } from '../services/authApi';
|
|
535
|
+
|
|
536
|
+
const AuthContext = createContext(null);
|
|
537
|
+
|
|
538
|
+
export const AuthProvider = ({ children }) => {
|
|
539
|
+
const [user, setUser] = useState(null);
|
|
540
|
+
const [accessToken, setAccessToken] = useState(null);
|
|
541
|
+
const [loading, setLoading] = useState(true);
|
|
542
|
+
|
|
543
|
+
useEffect(() => {
|
|
544
|
+
(async () => {
|
|
545
|
+
const token = await AsyncStorage.getItem('access_token');
|
|
546
|
+
if (token) {
|
|
547
|
+
setAccessToken(token);
|
|
548
|
+
await refreshProfile(token);
|
|
549
|
+
}
|
|
550
|
+
setLoading(false);
|
|
551
|
+
})();
|
|
552
|
+
}, []);
|
|
553
|
+
|
|
554
|
+
const refreshProfile = async (token) => {
|
|
555
|
+
try {
|
|
556
|
+
const resp = await apiGetProfile(token);
|
|
557
|
+
setUser(resp.data);
|
|
558
|
+
} catch {
|
|
559
|
+
setUser(null);
|
|
560
|
+
setAccessToken(null);
|
|
561
|
+
await AsyncStorage.removeItem('access_token');
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
const login = async (payload) => {
|
|
566
|
+
const resp = await apiLogin(payload);
|
|
567
|
+
const token = resp?.data?.access_token || resp?.data?.user_token;
|
|
568
|
+
if (token) {
|
|
569
|
+
setAccessToken(token);
|
|
570
|
+
await AsyncStorage.setItem('access_token', token);
|
|
571
|
+
await refreshProfile(token);
|
|
572
|
+
}
|
|
573
|
+
return resp;
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
const register = async (payload) => {
|
|
577
|
+
const resp = await apiRegister(payload);
|
|
578
|
+
const token = resp?.data?.access_token || resp?.data?.user_token;
|
|
579
|
+
if (token) {
|
|
580
|
+
setAccessToken(token);
|
|
581
|
+
await AsyncStorage.setItem('access_token', token);
|
|
582
|
+
await refreshProfile(token);
|
|
583
|
+
}
|
|
584
|
+
return resp;
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
const googleLogin = async (idToken) => {
|
|
588
|
+
const resp = await apiGoogleLogin(idToken);
|
|
589
|
+
const token = resp?.data?.access_token || resp?.data?.user_token;
|
|
590
|
+
if (token) {
|
|
591
|
+
setAccessToken(token);
|
|
592
|
+
await AsyncStorage.setItem('access_token', token);
|
|
593
|
+
await refreshProfile(token);
|
|
594
|
+
}
|
|
595
|
+
return resp;
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
const logout = async () => {
|
|
599
|
+
setUser(null);
|
|
600
|
+
setAccessToken(null);
|
|
601
|
+
await AsyncStorage.removeItem('access_token');
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
return (
|
|
605
|
+
<AuthContext.Provider
|
|
606
|
+
value={{ user, accessToken, loading, login, register, googleLogin, logout, isAuthenticated: !!user }}
|
|
607
|
+
>
|
|
608
|
+
{children}
|
|
609
|
+
</AuthContext.Provider>
|
|
610
|
+
);
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
export const useAuth = () => {
|
|
614
|
+
const ctx = useContext(AuthContext);
|
|
615
|
+
if (!ctx) throw new Error('useAuth must be used within AuthProvider');
|
|
616
|
+
return ctx;
|
|
617
|
+
};
|
|
618
|
+
```
|
|
287
619
|
|
|
288
|
-
|
|
289
|
-
const { email, password, username } = req.body;
|
|
620
|
+
### 3.3 Google Sign-In (React Native)
|
|
290
621
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
});
|
|
302
|
-
}
|
|
303
|
-
});
|
|
622
|
+
```bash
|
|
623
|
+
npm install @react-native-google-signin/google-signin
|
|
624
|
+
```
|
|
304
625
|
|
|
305
|
-
|
|
306
|
-
|
|
626
|
+
```javascript
|
|
627
|
+
import { GoogleSignin } from '@react-native-google-signin/google-signin';
|
|
628
|
+
import { useAuth } from '../context/AuthContext';
|
|
307
629
|
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
});
|
|
323
|
-
}
|
|
630
|
+
GoogleSignin.configure({
|
|
631
|
+
webClientId: 'YOUR_WEB_CLIENT_ID_FROM_GOOGLE',
|
|
324
632
|
});
|
|
325
633
|
|
|
326
|
-
|
|
634
|
+
function LoginScreen() {
|
|
635
|
+
const { login, googleLogin } = useAuth();
|
|
636
|
+
|
|
637
|
+
const handleGoogle = async () => {
|
|
638
|
+
try {
|
|
639
|
+
await GoogleSignin.hasPlayServices();
|
|
640
|
+
const userInfo = await GoogleSignin.signIn();
|
|
641
|
+
const idToken = userInfo.idToken;
|
|
642
|
+
await googleLogin(idToken); // sends to backend /api/auth/google
|
|
643
|
+
} catch (err) {
|
|
644
|
+
console.error('Google sign-in failed', err);
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
// ...render buttons, etc.
|
|
649
|
+
}
|
|
327
650
|
```
|
|
328
651
|
|
|
329
652
|
---
|
|
330
653
|
|
|
331
|
-
##
|
|
654
|
+
## 4. Key & Security Guidelines
|
|
332
655
|
|
|
333
|
-
|
|
656
|
+
- `@mspkapps/auth-client` is **backend-only**.
|
|
657
|
+
- API key, secret, and `googleClientId` live **only in backend env vars**.
|
|
658
|
+
- Frontend talks to backend over HTTPS (`/api/auth/*`, `/api/user/*`).
|
|
659
|
+
- Frontend stores only **user-level access token** (e.g. in `localStorage` / `AsyncStorage`).
|
|
660
|
+
- Never expose API key/secret in web or mobile bundles.
|
|
334
661
|
|
|
335
|
-
|
|
662
|
+
---
|
|
336
663
|
|
|
337
|
-
|
|
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)
|
|
342
|
-
}
|
|
343
|
-
```
|
|
664
|
+
## 5. Troubleshooting
|
|
344
665
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
```js
|
|
348
|
-
try {
|
|
349
|
-
const res = await authclient.login({ email, password });
|
|
350
|
-
} catch (err) {
|
|
351
|
-
if (err.code === 'EMAIL_NOT_VERIFIED') {
|
|
352
|
-
// Ask user to verify email or call resendVerificationEmail
|
|
353
|
-
} else if (err.status === 401) {
|
|
354
|
-
// Invalid credentials
|
|
355
|
-
} else {
|
|
356
|
-
console.error(err);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
```
|
|
666
|
+
### Frontend gets 4xx/5xx from backend
|
|
360
667
|
|
|
361
|
-
|
|
668
|
+
- Inspect backend logs; most errors will be `AuthError` thrown by AuthClient.
|
|
669
|
+
- Make sure backend env vars (`MSPK_AUTH_API_KEY`, `MSPK_AUTH_API_SECRET`) are set.
|
|
362
670
|
|
|
363
|
-
|
|
671
|
+
### Google login succeeds on client but fails on backend
|
|
364
672
|
|
|
365
|
-
-
|
|
366
|
-
-
|
|
367
|
-
- Always keep `apiKey`, `apiSecret`, and `googleClientId` in environment variables in production.
|
|
673
|
+
- Ensure `GOOGLE_CLIENT_ID` in backend matches the client ID used on the frontend.
|
|
674
|
+
- Check that the frontend sends `credential` / `id_token` to `/api/auth/google` correctly.
|
|
368
675
|
|
|
369
676
|
---
|
|
370
677
|
|
|
371
|
-
##
|
|
372
|
-
|
|
373
|
-
- npm: `@mspkapps/auth-client`
|
|
374
|
-
- MSPK™ Auth Platform dashboard: (URL you provide in your docs)
|
|
678
|
+
## 6. Summary
|
|
375
679
|
|
|
376
|
-
|
|
680
|
+
- Install and initialize `@mspkapps/auth-client` **only in your backend**.
|
|
681
|
+
- Implement clean REST endpoints in your backend that call `authclient` methods.
|
|
682
|
+
- React and React Native frontends call those endpoints with plain HTTP (fetch/axios).
|
|
683
|
+
- This keeps API keys safe and maintains a clean separation between frontend and backend.
|
package/package.json
CHANGED
package/src/AuthClient.js
CHANGED
|
@@ -94,8 +94,8 @@ export class AuthClient {
|
|
|
94
94
|
return json;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
async googleAuth({ id_token
|
|
98
|
-
if (!id_token
|
|
97
|
+
async googleAuth({ id_token }) {
|
|
98
|
+
if (!id_token) {
|
|
99
99
|
throw new AuthError(
|
|
100
100
|
'Either id_token or access_token is required for Google authentication',
|
|
101
101
|
400,
|
|
@@ -105,7 +105,7 @@ export class AuthClient {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
// include googleClientId in body too (helpful if backend needs it)
|
|
108
|
-
const body = { id_token
|
|
108
|
+
const body = { id_token };
|
|
109
109
|
if (this.googleClientId) body.google_client_id = this.googleClientId;
|
|
110
110
|
|
|
111
111
|
const resp = await this.fetch(this._buildUrl('auth/google'), {
|