@mspkapps/auth-client 0.1.28 → 0.1.31

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 CHANGED
@@ -232,7 +232,160 @@ router.patch('/profile', requireAuth, async (req, res) => {
232
232
  export default router;
233
233
  ```
234
234
 
235
- ### 1.6 Wire Up Routes in Backend App
235
+ ### 1.6 Developer Data Routes (Backend)
236
+
237
+ The AuthClient provides APIs to fetch developer-specific data (groups, apps, users). The **developer ID is automatically extracted** from the API key & secret, so you don't need to pass it explicitly.
238
+
239
+ Example `src/routes/developerRoutes.js`:
240
+
241
+ ```javascript
242
+ import express from 'express';
243
+ import authclient from '../auth/authClient.js';
244
+ import { AuthError } from '@mspkapps/auth-client';
245
+
246
+ const router = express.Router();
247
+
248
+ function handleError(res, err, fallback = 'Request failed') {
249
+ if (err instanceof AuthError) {
250
+ return res.status(err.status || 400).json({
251
+ success: false,
252
+ message: err.message || fallback,
253
+ code: err.code || 'REQUEST_FAILED',
254
+ data: err.response?.data ?? null,
255
+ });
256
+ }
257
+ console.error('Unexpected error:', err);
258
+ return res.status(500).json({
259
+ success: false,
260
+ message: fallback,
261
+ code: 'INTERNAL_ERROR',
262
+ });
263
+ }
264
+
265
+ // GET /api/developer/groups
266
+ // Fetch all groups belonging to the authenticated developer
267
+ router.get('/groups', async (req, res) => {
268
+ try {
269
+ const resp = await authclient.getDeveloperGroups();
270
+ return res.json(resp);
271
+ } catch (err) {
272
+ return handleError(res, err, 'Failed to fetch groups');
273
+ }
274
+ });
275
+
276
+ // GET /api/developer/apps?group_id=123
277
+ // Fetch developer's apps
278
+ // - No query params: returns ALL apps (with and without groups)
279
+ // - ?group_id=123: returns apps in specific group
280
+ // - ?group_id=null: returns only apps NOT in any group
281
+ router.get('/apps', async (req, res) => {
282
+ try {
283
+ const { group_id } = req.query;
284
+
285
+ let groupId;
286
+ if (group_id === 'null' || group_id === '') {
287
+ groupId = null; // Apps without groups
288
+ } else if (group_id !== undefined) {
289
+ groupId = group_id; // Specific group
290
+ } // else undefined = all apps
291
+
292
+ const resp = await authclient.getDeveloperApps(groupId);
293
+ return res.json(resp);
294
+ } catch (err) {
295
+ return handleError(res, err, 'Failed to fetch apps');
296
+ }
297
+ });
298
+
299
+ // GET /api/developer/users?app_id=123&page=1&limit=50
300
+ // Fetch users for a specific app (with pagination)
301
+ // Returns all user data EXCEPT password, including extra fields
302
+ router.get('/users', async (req, res) => {
303
+ try {
304
+ const { app_id, page = 1, limit = 50 } = req.query;
305
+
306
+ if (!app_id) {
307
+ return res.status(400).json({
308
+ success: false,
309
+ message: 'app_id query parameter is required'
310
+ });
311
+ }
312
+
313
+ const resp = await authclient.getAppUsers({
314
+ appId: app_id,
315
+ page: parseInt(page),
316
+ limit: parseInt(limit)
317
+ });
318
+ return res.json(resp);
319
+ } catch (err) {
320
+ return handleError(res, err, 'Failed to fetch users');
321
+ }
322
+ });
323
+
324
+ // GET /api/developer/user/:user_id
325
+ // Fetch specific user data by user ID
326
+ // Returns complete user data (no password) including extra fields
327
+ router.get('/user/:user_id', async (req, res) => {
328
+ try {
329
+ const { user_id } = req.params;
330
+ const resp = await authclient.getUserData(user_id);
331
+ return res.json(resp);
332
+ } catch (err) {
333
+ return handleError(res, err, 'Failed to fetch user data');
334
+ }
335
+ });
336
+
337
+ export default router;
338
+ ```
339
+
340
+ **Usage Examples:**
341
+
342
+ ```javascript
343
+ // Get all groups
344
+ GET /api/developer/groups
345
+ Response: { success: true, data: [...groups] }
346
+
347
+ // Get all apps (both in groups and standalone)
348
+ GET /api/developer/apps
349
+ Response: { success: true, data: [...apps] }
350
+
351
+ // Get apps in a specific group
352
+ GET /api/developer/apps?group_id=123
353
+ Response: { success: true, data: [...apps] }
354
+
355
+ // Get only apps NOT in any group
356
+ GET /api/developer/apps?group_id=null
357
+ Response: { success: true, data: [...apps] }
358
+
359
+ // Get users for an app (paginated)
360
+ GET /api/developer/users?app_id=456&page=1&limit=50
361
+ Response: {
362
+ success: true,
363
+ data: [...users],
364
+ pagination: {
365
+ currentPage: 1,
366
+ totalPages: 5,
367
+ totalUsers: 234,
368
+ limit: 50
369
+ }
370
+ }
371
+
372
+ // Get specific user
373
+ GET /api/developer/user/789
374
+ Response: {
375
+ success: true,
376
+ data: {
377
+ id: 789,
378
+ username: "john_doe",
379
+ email: "john@example.com",
380
+ name: "John Doe",
381
+ extra: { country: "USA", age: 30 },
382
+ is_email_verified: true,
383
+ // ... no password field
384
+ }
385
+ }
386
+ ```
387
+
388
+ ### 1.7 Wire Up Routes in Backend App
236
389
 
237
390
  In your backend `src/app.js`:
238
391
 
@@ -241,6 +394,7 @@ import express from 'express';
241
394
  import cors from 'cors';
242
395
  import authRoutes from './routes/authRoutes.js';
243
396
  import userRoutes from './routes/userRoutes.js';
397
+ import developerRoutes from './routes/developerRoutes.js'; // NEW
244
398
 
245
399
  const app = express();
246
400
 
@@ -249,6 +403,7 @@ app.use(express.json());
249
403
 
250
404
  app.use('/api/auth', authRoutes);
251
405
  app.use('/api/user', userRoutes);
406
+ app.use('/api/developer', developerRoutes); // NEW
252
407
 
253
408
  export default app;
254
409
  ```
@@ -321,6 +476,61 @@ export async function apiGetProfile(accessToken) {
321
476
  }
322
477
  return json;
323
478
  }
479
+
480
+ // Developer Data APIs (if building a developer dashboard)
481
+ export async function apiGetGroups() {
482
+ const resp = await fetch(`${API_BASE_URL}/api/developer/groups`, {
483
+ method: 'GET',
484
+ headers: { 'Content-Type': 'application/json' },
485
+ });
486
+ const json = await resp.json();
487
+ if (!resp.ok || json?.success === false) {
488
+ throw new Error(json?.message || 'Get groups failed');
489
+ }
490
+ return json;
491
+ }
492
+
493
+ export async function apiGetApps(groupId) {
494
+ const url = groupId !== undefined
495
+ ? `${API_BASE_URL}/api/developer/apps?group_id=${groupId}`
496
+ : `${API_BASE_URL}/api/developer/apps`;
497
+ const resp = await fetch(url, {
498
+ method: 'GET',
499
+ headers: { 'Content-Type': 'application/json' },
500
+ });
501
+ const json = await resp.json();
502
+ if (!resp.ok || json?.success === false) {
503
+ throw new Error(json?.message || 'Get apps failed');
504
+ }
505
+ return json;
506
+ }
507
+
508
+ export async function apiGetAppUsers(appId, page = 1, limit = 50) {
509
+ const resp = await fetch(
510
+ `${API_BASE_URL}/api/developer/users?app_id=${appId}&page=${page}&limit=${limit}`,
511
+ {
512
+ method: 'GET',
513
+ headers: { 'Content-Type': 'application/json' },
514
+ }
515
+ );
516
+ const json = await resp.json();
517
+ if (!resp.ok || json?.success === false) {
518
+ throw new Error(json?.message || 'Get users failed');
519
+ }
520
+ return json;
521
+ }
522
+
523
+ export async function apiGetUserData(userId) {
524
+ const resp = await fetch(`${API_BASE_URL}/api/developer/user/${userId}`, {
525
+ method: 'GET',
526
+ headers: { 'Content-Type': 'application/json' },
527
+ });
528
+ const json = await resp.json();
529
+ if (!resp.ok || json?.success === false) {
530
+ throw new Error(json?.message || 'Get user data failed');
531
+ }
532
+ return json;
533
+ }
324
534
  ```
325
535
 
326
536
  ### 2.2 Simple Auth Context (Frontend-Only State)
@@ -490,6 +700,96 @@ function LoginPage() {
490
700
 
491
701
  The Google ID token is sent to `/api/auth/google` on your backend, which then calls `authclient.googleAuth`.
492
702
 
703
+ ### 2.5 Example Developer Dashboard Page (React Vite)
704
+
705
+ If you're building a developer dashboard that displays groups, apps, and users:
706
+
707
+ ```jsx
708
+ import { useState, useEffect } from 'react';
709
+ import { apiGetGroups, apiGetApps, apiGetAppUsers } from '../services/authApi';
710
+
711
+ function DeveloperDashboard() {
712
+ const [groups, setGroups] = useState([]);
713
+ const [apps, setApps] = useState([]);
714
+ const [selectedApp, setSelectedApp] = useState(null);
715
+ const [users, setUsers] = useState([]);
716
+ const [loading, setLoading] = useState(true);
717
+
718
+ useEffect(() => {
719
+ loadData();
720
+ }, []);
721
+
722
+ const loadData = async () => {
723
+ try {
724
+ const [groupsResp, appsResp] = await Promise.all([
725
+ apiGetGroups(),
726
+ apiGetApps() // Get all apps
727
+ ]);
728
+ setGroups(groupsResp.data);
729
+ setApps(appsResp.data);
730
+ } catch (err) {
731
+ console.error('Failed to load data:', err);
732
+ } finally {
733
+ setLoading(false);
734
+ }
735
+ };
736
+
737
+ const loadUsers = async (appId) => {
738
+ try {
739
+ const resp = await apiGetAppUsers(appId, 1, 50);
740
+ setUsers(resp.data);
741
+ setSelectedApp(appId);
742
+ } catch (err) {
743
+ console.error('Failed to load users:', err);
744
+ }
745
+ };
746
+
747
+ if (loading) return <div>Loading...</div>;
748
+
749
+ return (
750
+ <div>
751
+ <h1>Developer Dashboard</h1>
752
+
753
+ <section>
754
+ <h2>Groups ({groups.length})</h2>
755
+ <ul>
756
+ {groups.map(group => (
757
+ <li key={group.id}>{group.name}</li>
758
+ ))}
759
+ </ul>
760
+ </section>
761
+
762
+ <section>
763
+ <h2>Apps ({apps.length})</h2>
764
+ <ul>
765
+ {apps.map(app => (
766
+ <li key={app.id}>
767
+ {app.app_name} {app.group_name && `(Group: ${app.group_name})`}
768
+ <button onClick={() => loadUsers(app.id)}>View Users</button>
769
+ </li>
770
+ ))}
771
+ </ul>
772
+ </section>
773
+
774
+ {selectedApp && (
775
+ <section>
776
+ <h2>Users for App {selectedApp}</h2>
777
+ <ul>
778
+ {users.map(user => (
779
+ <li key={user.id}>
780
+ {user.email} - {user.name}
781
+ </li>
782
+ ))}
783
+ </ul>
784
+ </section>
785
+ )}
786
+ </div>
787
+ );
788
+ }
789
+
790
+ export default DeveloperDashboard;
791
+ ```
792
+
493
793
  ---
494
794
 
495
795
  ## 3. React Native CLI – Call Your Backend
@@ -524,6 +824,18 @@ export const apiRegister = (payload) => request('/api/auth/register', { method:
524
824
  export const apiGoogleLogin = (idToken) =>
525
825
  request('/api/auth/google', { method: 'POST', body: { id_token: idToken } });
526
826
  export const apiGetProfile = (token) => request('/api/user/profile', { method: 'GET', token });
827
+
828
+ // Developer Data APIs
829
+ export const apiGetGroups = () => request('/api/developer/groups', { method: 'GET' });
830
+ export const apiGetApps = (groupId) => {
831
+ const path = groupId !== undefined
832
+ ? `/api/developer/apps?group_id=${groupId}`
833
+ : '/api/developer/apps';
834
+ return request(path, { method: 'GET' });
835
+ };
836
+ export const apiGetAppUsers = (appId, page = 1, limit = 50) =>
837
+ request(`/api/developer/users?app_id=${appId}&page=${page}&limit=${limit}`, { method: 'GET' });
838
+ export const apiGetUserData = (userId) => request(`/api/developer/user/${userId}`, { method: 'GET' });
527
839
  ```
528
840
 
529
841
  ### 3.2 Auth Context (React Native)
@@ -655,13 +967,51 @@ function LoginScreen() {
655
967
 
656
968
  - `@mspkapps/auth-client` is **backend-only**.
657
969
  - API key, secret, and `googleClientId` live **only in backend env vars**.
658
- - Frontend talks to backend over HTTPS (`/api/auth/*`, `/api/user/*`).
970
+ - Frontend talks to backend over HTTPS (`/api/auth/*`, `/api/user/*`, `/api/developer/*`).
659
971
  - Frontend stores only **user-level access token** (e.g. in `localStorage` / `AsyncStorage`).
660
972
  - Never expose API key/secret in web or mobile bundles.
973
+ - **Developer ID is automatically extracted** from API key/secret by the backend - no need to pass it explicitly.
661
974
 
662
975
  ---
663
976
 
664
- ## 5. Troubleshooting
977
+ ## 5. API Reference
978
+
979
+ ### 5.1 Authentication APIs
980
+
981
+ | Method | Endpoint | Description |
982
+ |--------|----------|-------------|
983
+ | `authclient.register()` | `POST /api/auth/register` | Register new user |
984
+ | `authclient.login()` | `POST /api/auth/login` | Login user |
985
+ | `authclient.googleAuth()` | `POST /api/auth/google` | Google OAuth login |
986
+ | `authclient.verifyToken()` | `POST /api/auth/verify-token` | Verify access token |
987
+
988
+ ### 5.2 User Profile APIs
989
+
990
+ | Method | Endpoint | Description |
991
+ |--------|----------|-------------|
992
+ | `authclient.getProfile()` | `GET /api/user/profile` | Get user profile |
993
+ | `authclient.updateProfile()` | `PATCH /api/user/profile` | Update user profile |
994
+
995
+ ### 5.3 Developer Data APIs (NEW)
996
+
997
+ | Method | Endpoint | Description |
998
+ |--------|----------|-------------|
999
+ | `authclient.getDeveloperGroups()` | `GET /api/developer/groups` | Get all groups for the authenticated developer |
1000
+ | `authclient.getDeveloperApps()` | `GET /api/developer/apps` | Get all apps (with and without groups) |
1001
+ | `authclient.getDeveloperApps(123)` | `GET /api/developer/apps?group_id=123` | Get apps in specific group |
1002
+ | `authclient.getDeveloperApps(null)` | `GET /api/developer/apps?group_id=null` | Get apps NOT in any group |
1003
+ | `authclient.getAppUsers({ appId, page, limit })` | `GET /api/developer/users?app_id=X&page=Y&limit=Z` | Get users for specific app (paginated) |
1004
+ | `authclient.getUserData(userId)` | `GET /api/developer/user/:user_id` | Get specific user data |
1005
+
1006
+ **Notes:**
1007
+ - All user data responses **exclude the password field** for security
1008
+ - User data includes all custom `extra` fields configured for the app
1009
+ - `developer_id` is **automatically extracted** from API key & secret - you never pass it manually
1010
+ - Pagination is supported for user lists with `page` and `limit` parameters
1011
+
1012
+ ---
1013
+
1014
+ ## 6. Troubleshooting
665
1015
 
666
1016
  ### Frontend gets 4xx/5xx from backend
667
1017
 
@@ -673,11 +1023,24 @@ function LoginScreen() {
673
1023
  - Ensure `GOOGLE_CLIENT_ID` in backend matches the client ID used on the frontend.
674
1024
  - Check that the frontend sends `credential` / `id_token` to `/api/auth/google` correctly.
675
1025
 
1026
+ ### Developer data APIs return 403/404
1027
+
1028
+ - Verify that the API key & secret in your backend env vars are correct.
1029
+ - The developer ID is extracted automatically from these credentials.
1030
+ - Ensure you're querying data that belongs to the authenticated developer.
1031
+
1032
+ ### App users query returns empty results
1033
+
1034
+ - Verify the `app_id` belongs to the authenticated developer.
1035
+ - Check that the app actually has registered users.
1036
+
676
1037
  ---
677
1038
 
678
- ## 6. Summary
1039
+ ## 7. Summary
679
1040
 
680
1041
  - Install and initialize `@mspkapps/auth-client` **only in your backend**.
681
1042
  - Implement clean REST endpoints in your backend that call `authclient` methods.
682
1043
  - React and React Native frontends call those endpoints with plain HTTP (fetch/axios).
683
1044
  - This keeps API keys safe and maintains a clean separation between frontend and backend.
1045
+ - Use the new **Developer Data APIs** to build dashboards that display groups, apps, and users.
1046
+ - Developer ID is **automatically extracted** from API credentials - never passed manually.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mspkapps/auth-client",
3
- "version": "0.1.28",
3
+ "version": "0.1.31",
4
4
  "description": "Lightweight client for Your Auth Service",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/AuthClient.js CHANGED
@@ -14,7 +14,7 @@ export class AuthClient {
14
14
  storage,
15
15
  fetch: fetchFn,
16
16
  keyInPath = true,
17
- googleClientId = null, // new option
17
+ googleClientId = null,
18
18
  } = {}) {
19
19
  if (!apiKey) throw new Error('apiKey is required');
20
20
  if (!apiSecret) throw new Error('apiSecret is required');
@@ -22,7 +22,7 @@ export class AuthClient {
22
22
  this.apiSecret = apiSecret;
23
23
  this.baseUrl = baseUrl.replace(/\/$/, '');
24
24
  this.keyInPath = !!keyInPath;
25
- this.googleClientId = googleClientId || null; // store it
25
+ this.googleClientId = googleClientId || null;
26
26
 
27
27
  const f = fetchFn || (typeof window !== 'undefined' ? window.fetch : (typeof fetch !== 'undefined' ? fetch : null));
28
28
  if (!f) throw new Error('No fetch available. Pass { fetch } or run on Node 18+/browsers.');
@@ -104,7 +104,6 @@ export class AuthClient {
104
104
  );
105
105
  }
106
106
 
107
- // include googleClientId in body too (helpful if backend needs it)
108
107
  const body = { id_token };
109
108
  if (this.googleClientId) body.google_client_id = this.googleClientId;
110
109
 
@@ -232,6 +231,78 @@ export class AuthClient {
232
231
  if (!resp.ok || json?.success === false) throw toError(resp, json, 'Request failed');
233
232
  return json;
234
233
  }
234
+
235
+ // ---------- Developer Data APIs ----------
236
+ // Note: developer_id is automatically extracted from API key & secret on the backend
237
+
238
+ async getDeveloperGroups() {
239
+ const resp = await this.fetch(this._buildUrl('developer/groups'), {
240
+ method: 'GET',
241
+ headers: this._headers()
242
+ });
243
+ const json = await safeJson(resp);
244
+ if (!resp.ok || json?.success === false) throw toError(resp, json, 'Get developer groups failed');
245
+ return json;
246
+ }
247
+
248
+ /**
249
+ * Get developer's apps
250
+ * @param {number|string|null} groupId - Optional. Filter by group ID, pass null/'null' for apps without groups, omit for all apps
251
+ * @returns {Promise} API response with app data
252
+ * @example
253
+ * // Get all apps (with and without groups)
254
+ * await client.getDeveloperApps();
255
+ *
256
+ * // Get apps in specific group
257
+ * await client.getDeveloperApps(123);
258
+ *
259
+ * // Get only apps NOT in any group
260
+ * await client.getDeveloperApps(null);
261
+ */
262
+ async getDeveloperApps(groupId = undefined) {
263
+ let url = this._buildUrl('developer/apps');
264
+
265
+ if (groupId !== undefined) {
266
+ if (groupId === null || groupId === 'null') {
267
+ // Get apps without groups
268
+ url += '?group_id=null';
269
+ } else {
270
+ // Get apps for specific group
271
+ url += `?group_id=${encodeURIComponent(groupId)}`;
272
+ }
273
+ }
274
+
275
+ const resp = await this.fetch(url, {
276
+ method: 'GET',
277
+ headers: this._headers()
278
+ });
279
+ const json = await safeJson(resp);
280
+ if (!resp.ok || json?.success === false) throw toError(resp, json, 'Get developer apps failed');
281
+ return json;
282
+ }
283
+
284
+ async getAppUsers({ appId, page = 1, limit = 50 }) {
285
+ if (!appId) throw new AuthError('appId is required', 400, 'MISSING_APP_ID', null);
286
+ const url = this._buildUrl(`developer/users?app_id=${encodeURIComponent(appId)}&page=${page}&limit=${limit}`);
287
+ const resp = await this.fetch(url, {
288
+ method: 'GET',
289
+ headers: this._headers()
290
+ });
291
+ const json = await safeJson(resp);
292
+ if (!resp.ok || json?.success === false) throw toError(resp, json, 'Get app users failed');
293
+ return json;
294
+ }
295
+
296
+ async getUserData(userId) {
297
+ if (!userId) throw new AuthError('userId is required', 400, 'MISSING_USER_ID', null);
298
+ const resp = await this.fetch(this._buildUrl(`developer/user/${encodeURIComponent(userId)}`), {
299
+ method: 'GET',
300
+ headers: this._headers()
301
+ });
302
+ const json = await safeJson(resp);
303
+ if (!resp.ok || json?.success === false) throw toError(resp, json, 'Get user data failed');
304
+ return json;
305
+ }
235
306
  }
236
307
 
237
308
  // ---------- helpers ----------
@@ -249,7 +320,6 @@ function toError(resp, json, fallback) {
249
320
 
250
321
  // ---- Singleton-style convenience API ----
251
322
 
252
- // Internal holder
253
323
  const _singleton = { client: null };
254
324
 
255
325
  function ensureClient() {
@@ -261,13 +331,12 @@ function ensureClient() {
261
331
  return _singleton.client;
262
332
  }
263
333
 
264
- // Initialize once in your backend (typically at startup)
265
334
  function init({
266
335
  apiKey = process.env.MSPK_AUTH_API_KEY,
267
336
  apiSecret = process.env.MSPK_AUTH_API_SECRET,
268
337
  googleClientId = process.env.GOOGLE_CLIENT_ID,
269
- baseUrl, // optional override
270
- storage, // usually omit on backend
338
+ baseUrl,
339
+ storage,
271
340
  fetch: fetchFn,
272
341
  keyInPath,
273
342
  } = {}) {
@@ -283,7 +352,6 @@ function init({
283
352
  return _singleton.client;
284
353
  }
285
354
 
286
- // Thin wrappers delegating to the singleton
287
355
  const authclient = {
288
356
  init,
289
357
  get client() {
@@ -324,7 +392,21 @@ const authclient = {
324
392
  verifyToken(accessToken) {
325
393
  return ensureClient().verifyToken(accessToken);
326
394
  },
395
+
396
+ // developer data APIs (dev_id extracted automatically from API credentials)
397
+ getDeveloperGroups() {
398
+ return ensureClient().getDeveloperGroups();
399
+ },
400
+ getDeveloperApps(groupId = undefined) {
401
+ return ensureClient().getDeveloperApps(groupId);
402
+ },
403
+ getAppUsers({ appId, page, limit }) {
404
+ return ensureClient().getAppUsers({ appId, page, limit });
405
+ },
406
+ getUserData(userId) {
407
+ return ensureClient().getUserData(userId);
408
+ },
327
409
  };
328
410
 
329
- export { authclient, init }; // named exports if someone prefers them
330
- export default authclient; // default export for your desired DX
411
+ export { authclient, init };
412
+ export default authclient;