@canmingir/link 1.2.49 → 1.2.51

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canmingir/link",
3
- "version": "1.2.49",
3
+ "version": "1.2.51",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "link-server": "./server/server.ts"
package/src/http/index.js CHANGED
@@ -40,6 +40,20 @@ instance.interceptors.response.use(
40
40
  async (error) => {
41
41
  publish("LOADED", { loading: false });
42
42
  const statusCode = error.response?.status;
43
+ const identityProvider = storage.get("link", "identityProvider");
44
+
45
+ if (statusCode === 500 && identityProvider === "DEMO") {
46
+ const token = storage.get("link", "accessToken");
47
+ try {
48
+ const decodedToken = jwtDecode(token);
49
+ if (decodedToken.exp * 1000 < Date.now()) {
50
+ return Promise.reject(error);
51
+ }
52
+ } catch (_) {
53
+ return Promise.reject(error);
54
+ }
55
+ }
56
+
43
57
  switch (statusCode) {
44
58
  case 400:
45
59
  publish("GLOBAL_MESSAGE_POSTED", {
@@ -78,7 +92,7 @@ instance.interceptors.response.use(
78
92
  break;
79
93
  }
80
94
  return Promise.reject(error);
81
- }
95
+ },
82
96
  );
83
97
 
84
98
  export const fetcher = (url) => instance.get(url).then((res) => res.data);
@@ -128,26 +142,41 @@ const refreshInterceptor =
128
142
  : createAuthRefreshInterceptor.default;
129
143
 
130
144
  refreshInterceptor(instance, refreshAuthLogic, {
131
- statusCodes: [401, 403],
132
- shouldRefresh: () => {
133
- const { name, base } = config();
145
+ statusCodes: [401, 403, 500],
146
+ pauseInstanceWhileRefreshing: true,
147
+ shouldRefresh: (error) => {
148
+ const { base } = config();
134
149
  const token = storage.get("link", "accessToken");
150
+ const identityProvider = storage.get("link", "identityProvider");
151
+ const statusCode = error.response?.status;
135
152
 
136
153
  if (!token) {
137
154
  window.location.href = `${window.location.origin}${base}/login`;
138
155
  return false;
139
156
  }
140
157
 
158
+ if (identityProvider === "DEMO") {
159
+ if (statusCode !== 500) {
160
+ return true;
161
+ }
162
+
163
+ try {
164
+ const decodedToken = jwtDecode(token);
165
+ return decodedToken.exp * 1000 < Date.now();
166
+ } catch (_) {
167
+ // If we can't decode, attempt refresh once.
168
+ return true;
169
+ }
170
+ }
171
+
172
+ if (statusCode === 500) {
173
+ return false;
174
+ }
175
+
141
176
  try {
142
177
  const decodedToken = jwtDecode(token);
143
178
  if (decodedToken.exp * 1000 < Date.now()) {
144
179
  return true;
145
- } else {
146
- publish("GLOBAL_MESSAGE_POSTED", {
147
- status: true,
148
- message: "UNAUTHORIZED",
149
- severity: "warning",
150
- });
151
180
  }
152
181
  } catch (err) {
153
182
  window.location.href = `${window.location.origin}${base}/login`;
package/src/http/user.js CHANGED
@@ -43,7 +43,7 @@ instance.getUserDetails = async () => {
43
43
 
44
44
  if (isExpired) {
45
45
  console.log(
46
- "Access token expired, refreshing before fetching user details..."
46
+ "Access token expired, refreshing before fetching user details...",
47
47
  );
48
48
  const { appId } = config();
49
49
  const projectId = storage.get("projectId");
@@ -54,12 +54,16 @@ instance.getUserDetails = async () => {
54
54
  appId,
55
55
  projectId,
56
56
  identityProvider,
57
+ ...(identityProvider === "DEMO" && {
58
+ username: "admin",
59
+ password: "admin",
60
+ }),
57
61
  });
58
62
 
59
63
  accessToken = data.accessToken;
60
64
  storage.set("link", "accessToken", accessToken);
61
65
  console.log(
62
- "Access token refreshed successfully, now fetching user details"
66
+ "Access token refreshed successfully, now fetching user details",
63
67
  );
64
68
  }
65
69
  } catch (error) {
@@ -86,25 +90,91 @@ instance.getUserDetails = async () => {
86
90
  };
87
91
 
88
92
  instance.getPermittedUsers = async () => {
89
- const userIds = [];
90
- const refreshToken = await storage.get("link", "refreshToken");
93
+ const { appId } = config();
94
+ const projectId = storage.get("projectId");
95
+ const identityProvider = storage.get("link", "identityProvider");
96
+
91
97
  const response = await http.get("/permissions");
92
98
 
93
- response.data.forEach((permission) => {
94
- userIds.push(permission.userId);
95
- });
96
-
97
- const users = await Promise.all(
98
- userIds.map(async (userId) => {
99
- const response = await axios.get(
100
- `https://api.github.com/user/${userId}`,
101
- { headers: { Authorization: `Bearer ${refreshToken}` } }
102
- );
103
- return response.data;
104
- })
105
- );
106
-
107
- return users;
99
+ const uniqueUserIds = [
100
+ ...new Set(
101
+ response.data
102
+ .filter((p) => p.appId === appId && p.projectId === projectId)
103
+ .map((p) => p.userId),
104
+ ),
105
+ ];
106
+
107
+ const UUID_REGEX =
108
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
109
+
110
+ if (identityProvider?.toUpperCase() === "GITHUB") {
111
+ const results = await Promise.allSettled(
112
+ uniqueUserIds.map(async (userId) => {
113
+ if (UUID_REGEX.test(userId)) {
114
+ return {
115
+ id: String(userId),
116
+ identityProvider,
117
+ name: String(userId),
118
+ displayName: null,
119
+ avatarUrl: `https://api.dicebear.com/7.x/bottts/svg?seed=${userId}`,
120
+ email: null,
121
+ };
122
+ }
123
+ const { data } = await axios.get(
124
+ `https://api.github.com/user/${userId}`,
125
+ { headers: { Accept: "application/vnd.github+json" } },
126
+ );
127
+ return {
128
+ id: String(data.id),
129
+ identityProvider: "GITHUB",
130
+ name: data.login,
131
+ displayName: data.name || null,
132
+ avatarUrl: data.avatar_url || null,
133
+ email: data.email || null,
134
+ };
135
+ }),
136
+ );
137
+
138
+ return results
139
+ .filter((r) => r.status === "fulfilled" && r.value !== null)
140
+ .map((r) => r.value);
141
+ }
142
+
143
+ if (identityProvider?.toUpperCase() === "DEMO") {
144
+ return [
145
+ {
146
+ id: "1001",
147
+ identityProvider: "DEMO",
148
+ name: "admin",
149
+ displayName: "Demo Admin",
150
+ avatarUrl: `https://api.dicebear.com/7.x/bottts/svg?seed=1001`,
151
+ email: "admin@demo.local",
152
+ },
153
+ ];
154
+ }
155
+
156
+ if (identityProvider?.toUpperCase() === "COGNITO") {
157
+ const accessToken = await storage.get("link", "accessToken");
158
+ let currentUserId = null;
159
+
160
+ try {
161
+ const decoded = jwtDecode(accessToken);
162
+ currentUserId = decoded.sub;
163
+ } catch (_) {}
164
+
165
+ return uniqueUserIds
166
+ .filter((id) => !UUID_REGEX.test(id))
167
+ .map((id) => ({
168
+ id: String(id),
169
+ identityProvider: "COGNITO",
170
+ name: id === currentUserId ? "You" : "Cognito User",
171
+ displayName: id === currentUserId ? "Cognito Admin" : null,
172
+ avatarUrl: `https://api.dicebear.com/7.x/bottts/svg?seed=${id}`,
173
+ email: null,
174
+ }));
175
+ }
176
+
177
+ return [];
108
178
  };
109
179
 
110
180
  export default instance;
@@ -1,10 +1,3 @@
1
- import Iconify from "../components/Iconify";
2
- import config from "../config/config";
3
- import { useEvent } from "@nucleoidai/react-event";
4
- import useSettings from "../hooks/useSettings";
5
- import { useSettingsContext } from "../components/settings/context";
6
- import { useUser } from "../hooks/use-user";
7
-
8
1
  import {
9
2
  Avatar,
10
3
  Box,
@@ -30,6 +23,13 @@ import {
30
23
  import { Button, Dialog, DialogActions, DialogContent } from "@mui/material";
31
24
  import React, { useEffect, useState } from "react";
32
25
 
26
+ import Iconify from "../components/Iconify";
27
+ import config from "../config/config";
28
+ import { useEvent } from "@nucleoidai/react-event";
29
+ import useSettings from "../hooks/useSettings";
30
+ import { useSettingsContext } from "../components/settings/context";
31
+ import { useUser } from "../hooks/use-user";
32
+
33
33
  let pkg = {
34
34
  name: "",
35
35
  version: "",
@@ -173,7 +173,7 @@ function SettingsDialog({ handleClose, open }) {
173
173
  color: "white",
174
174
  minHeight: 600,
175
175
  },
176
- }
176
+ },
177
177
  }}
178
178
  >
179
179
  <DialogContent>
@@ -211,16 +211,24 @@ const Permission = () => {
211
211
 
212
212
  useEffect(() => {
213
213
  getPermittedUsers();
214
+ // eslint-disable-next-line react-hooks/exhaustive-deps
214
215
  }, [event, event2]);
215
216
 
216
217
  return (
217
- <Stack direction="column" spacing={2} sx={{
218
- p: 2
219
- }}>
218
+ <Stack
219
+ direction="column"
220
+ spacing={2}
221
+ sx={{
222
+ p: 2,
223
+ }}
224
+ >
220
225
  <Typography variant="h6">Users</Typography>
221
- <Typography variant="subtitle2" sx={{
222
- color: "text.secondary"
223
- }}>
226
+ <Typography
227
+ variant="subtitle2"
228
+ sx={{
229
+ color: "text.secondary",
230
+ }}
231
+ >
224
232
  Users with access to the project.
225
233
  </Typography>
226
234
  <List>
@@ -238,7 +246,7 @@ const Permission = () => {
238
246
  }}
239
247
  >
240
248
  <ListItemAvatar>
241
- <Avatar src={user.avatar_url} alt={user.name} />
249
+ <Avatar src={user.avatarUrl} alt={user.name} />
242
250
  </ListItemAvatar>
243
251
  <ListItemText primary={user.name} />
244
252
  <ListItemSecondaryAction>
@@ -306,13 +314,20 @@ const Settings = () => {
306
314
  };
307
315
 
308
316
  return (
309
- <Stack direction="column" spacing={2} sx={{
310
- p: 2
311
- }}>
317
+ <Stack
318
+ direction="column"
319
+ spacing={2}
320
+ sx={{
321
+ p: 2,
322
+ }}
323
+ >
312
324
  <Typography variant="h6">Settings</Typography>
313
- <Typography variant="subtitle2" sx={{
314
- color: "text.secondary"
315
- }}>
325
+ <Typography
326
+ variant="subtitle2"
327
+ sx={{
328
+ color: "text.secondary",
329
+ }}
330
+ >
316
331
  Configure your application settings.
317
332
  </Typography>
318
333
  <List>
@@ -373,9 +388,13 @@ const About = () => {
373
388
  const description = pkg.description;
374
389
 
375
390
  return (
376
- <Stack direction="column" spacing={2} sx={{
377
- p: 2
378
- }}>
391
+ <Stack
392
+ direction="column"
393
+ spacing={2}
394
+ sx={{
395
+ p: 2,
396
+ }}
397
+ >
379
398
  <Box
380
399
  sx={{
381
400
  p: 3,
@@ -386,9 +405,13 @@ const About = () => {
386
405
  "linear-gradient(135deg, rgba(255,255,255,0.06), rgba(255,255,255,0.01))",
387
406
  }}
388
407
  >
389
- <Stack direction="row" spacing={2.5} sx={{
390
- alignItems: "center"
391
- }}>
408
+ <Stack
409
+ direction="row"
410
+ spacing={2.5}
411
+ sx={{
412
+ alignItems: "center",
413
+ }}
414
+ >
392
415
  <Avatar
393
416
  src={iconSrc}
394
417
  variant="rounded"
@@ -404,9 +427,12 @@ const About = () => {
404
427
  </Avatar>
405
428
 
406
429
  <Box sx={{ flex: 1, minWidth: 0 }}>
407
- <Typography variant="h5" sx={{
408
- fontWeight: 600
409
- }}>
430
+ <Typography
431
+ variant="h5"
432
+ sx={{
433
+ fontWeight: 600,
434
+ }}
435
+ >
410
436
  {appName.toUpperCase()}
411
437
  </Typography>
412
438
 
@@ -416,8 +442,9 @@ const About = () => {
416
442
  sx={{
417
443
  color: "text.secondary",
418
444
  mt: 0.5,
419
- maxWidth: 520
420
- }}>
445
+ maxWidth: 520,
446
+ }}
447
+ >
421
448
  {description}
422
449
  </Typography>
423
450
  )}
@@ -460,9 +487,12 @@ const About = () => {
460
487
  textAlign: "center",
461
488
  }}
462
489
  >
463
- <Typography variant="caption" sx={{
464
- color: "text.secondary"
465
- }}>
490
+ <Typography
491
+ variant="caption"
492
+ sx={{
493
+ color: "text.secondary",
494
+ }}
495
+ >
466
496
  © 2026 greycollar.ai. All rights reserved.
467
497
  </Typography>
468
498
  </Box>
@@ -473,15 +503,20 @@ const About = () => {
473
503
 
474
504
  const InfoRow = ({ label, value, link }) => {
475
505
  return (
476
- <Stack direction="row" spacing={1.5} sx={{
477
- alignItems: "center"
478
- }}>
506
+ <Stack
507
+ direction="row"
508
+ spacing={1.5}
509
+ sx={{
510
+ alignItems: "center",
511
+ }}
512
+ >
479
513
  <Typography
480
514
  variant="body2"
481
515
  sx={{
482
516
  color: "text.secondary",
483
- minWidth: 110
484
- }}>
517
+ minWidth: 110,
518
+ }}
519
+ >
485
520
  {label}
486
521
  </Typography>
487
522
  {link ? (