@canmingir/link 1.2.21 → 1.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canmingir/link",
3
- "version": "1.2.21",
3
+ "version": "1.2.23",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./index.js",
@@ -34,6 +34,7 @@
34
34
  "@nucleoidai/react-event": "^1.1.9",
35
35
  "@nucleoidjs/webstorage": "^1.0.5",
36
36
  "autosuggest-highlight": "^3.3.4",
37
+ "aws-amplify": "^6.16.0",
37
38
  "axios": "^1.10.0",
38
39
  "axios-auth-refresh": "^3.3.6",
39
40
  "axios-retry": "^4.4.1",
package/src/Platform.jsx CHANGED
@@ -1,4 +1,5 @@
1
1
  import "./global.css";
2
+ import "./widgets/Login/amplifyAuth";
2
3
 
3
4
  import ContextProvider from "./ContextProvider/ContextProvider";
4
5
  import GlobalSnackMessage from "./GlobalSnackMessage/GlobalSnackMessage";
@@ -9,6 +10,7 @@ import { SettingsProvider } from "./components/settings";
9
10
  import { SnackbarProvider } from "notistack";
10
11
  import ThemeProvider from "./theme";
11
12
  import config from "./config/config";
13
+ import { configureAmplify } from "./widgets/Login/amplifyConfig";
12
14
  import http from "./http";
13
15
  import { init } from "./config/config";
14
16
  import oauth from "./http/oauth";
@@ -19,6 +21,10 @@ import { publish, subscribe, useEvent } from "@nucleoidai/react-event";
19
21
 
20
22
  init();
21
23
 
24
+ if (config().credentials?.provider === "COGNITO") {
25
+ configureAmplify();
26
+ }
27
+
22
28
  window["@nucleoidai"] = {
23
29
  Event: { publish, subscribe, useEvent },
24
30
  };
@@ -15,6 +15,7 @@ export const ConfigSchema = Joi.object({
15
15
  region: Joi.string().optional(),
16
16
  userPoolId: Joi.string().optional(),
17
17
  clientId: Joi.string().optional(),
18
+ requestUrl: Joi.string().uri().optional(),
18
19
  }).optional(),
19
20
  project: Joi.object({
20
21
  nucleoid: Joi.object().optional(),
package/src/http/index.js CHANGED
@@ -89,21 +89,16 @@ const refreshAuthLogic = async (failedRequest) => {
89
89
  const projectId = storage.get("projectId");
90
90
  const identityProvider = storage.get("link", "identityProvider");
91
91
 
92
- const isDemo = identityProvider?.toUpperCase() === "DEMO";
93
-
94
- const { data } = isDemo
95
- ? await oauth.post("/oauth/demo", {
96
- appId,
97
- projectId,
98
- username: "admin",
99
- password: "admin",
100
- })
101
- : await oauth.post("/oauth", {
102
- refreshToken: storage.get("link", "refreshToken"),
103
- appId,
104
- projectId,
105
- identityProvider,
106
- });
92
+ const { data } = await oauth.post("/oauth", {
93
+ refreshToken: storage.get("link", "refreshToken"),
94
+ appId,
95
+ projectId,
96
+ identityProvider,
97
+ ...(identityProvider === "DEMO" && {
98
+ username: "admin",
99
+ password: "admin",
100
+ }),
101
+ });
107
102
 
108
103
  const { accessToken, refreshToken } = data;
109
104
 
@@ -29,6 +29,8 @@ export default function AuthModernLayout({ image }) {
29
29
  maxSize={140}
30
30
  sx={{
31
31
  mb: { xs: 1, md: 2 },
32
+ width: 80,
33
+ height: 80,
32
34
  }}
33
35
  />
34
36
 
@@ -91,24 +91,23 @@ function ProjectBar() {
91
91
  const handleSelect = (project) => {
92
92
  const { id: projectId } = project;
93
93
 
94
- const refreshToken = storage.get("link", "refreshToken");
95
- const identityProvider = storage.get("link", "identityProvider");
96
-
97
- const isDemo = identityProvider?.toUpperCase() === "DEMO";
98
-
99
- const request = isDemo
100
- ? oauth.post("/oauth/demo", {
101
- appId,
102
- projectId,
103
- username: "admin",
104
- password: "admin",
105
- })
106
- : oauth.post("/oauth", {
107
- appId,
108
- refreshToken,
109
- projectId,
110
- identityProvider,
111
- });
94
+ const identityProviderRaw = storage.get("link", "identityProvider");
95
+ const identityProvider = identityProviderRaw?.toUpperCase();
96
+
97
+ const payload = {
98
+ appId,
99
+ projectId,
100
+ identityProvider,
101
+ };
102
+
103
+ if (identityProvider === "DEMO") {
104
+ payload.username = "admin";
105
+ payload.password = "admin";
106
+ } else {
107
+ payload.refreshToken = storage.get("link", "refreshToken");
108
+ }
109
+
110
+ const request = oauth.post("/oauth", payload);
112
111
 
113
112
  request
114
113
  .then(({ data }) => {
@@ -1,5 +1,3 @@
1
- import CustomPopover, { usePopover } from "../../components/custom-popover";
2
-
3
1
  import Avatar from "@mui/material/Avatar";
4
2
  import Box from "@mui/material/Box";
5
3
  import Divider from "@mui/material/Divider";
@@ -16,6 +14,8 @@ import { useRouter } from "../../routes/hooks";
16
14
  import { useUser } from "../../hooks/use-user";
17
15
  import { varHover } from "../../components/animate";
18
16
 
17
+ import CustomPopover, { usePopover } from "../../components/custom-popover";
18
+
19
19
  // ----------------------------------------------------------------------
20
20
 
21
21
  export default function AccountPopover() {
@@ -27,11 +27,7 @@ export default function AccountPopover() {
27
27
 
28
28
  const handleLogout = async () => {
29
29
  try {
30
- storage.remove("link", "accessToken");
31
- storage.remove("link", "refreshToken");
32
- storage.remove("link", "identityProvider");
33
- storage.remove("projectId");
34
- storage.remove("landingLevel");
30
+ storage.clear();
35
31
  popover.onClose();
36
32
  router.replace("/login");
37
33
  } catch (error) {
@@ -33,7 +33,7 @@ function Callback() {
33
33
 
34
34
  if (state) {
35
35
  stateData = JSON.parse(decodeURIComponent(state));
36
- identityProvider = stateData.identityProvider;
36
+ identityProvider = stateData.identityProvider.toUpperCase();
37
37
  }
38
38
 
39
39
  if (error) {
@@ -54,9 +54,9 @@ function Callback() {
54
54
  hasProcessed.current = true;
55
55
 
56
56
  const providerConfigs = {
57
- github,
58
- linkedin,
59
- google,
57
+ GITHUB: github,
58
+ LINKEDIN: linkedin,
59
+ GOOGLE: google,
60
60
  };
61
61
 
62
62
  const providerConfig = providerConfigs[identityProvider];
@@ -1,45 +1,335 @@
1
- import { loginWithCognito } from "./cognitoAuth";
1
+ import config from "../../config/config";
2
+ import { publish } from "@nucleoidai/react-event";
2
3
  import { storage } from "@nucleoidjs/webstorage";
4
+ import { useNavigate } from "react-router-dom";
3
5
  import { useState } from "react";
4
6
 
5
- import { Button, Stack, TextField, Typography } from "@mui/material";
7
+ import {
8
+ Box,
9
+ Button,
10
+ IconButton,
11
+ InputAdornment,
12
+ Stack,
13
+ TextField,
14
+ Typography,
15
+ alpha,
16
+ } from "@mui/material";
17
+ import {
18
+ CheckOutlined,
19
+ EmailOutlined,
20
+ LockOutlined,
21
+ MarkEmailReadOutlined,
22
+ Visibility,
23
+ VisibilityOff,
24
+ } from "@mui/icons-material";
25
+ import { confirmSignup, getTokens, login, signup } from "./amplifyAuth";
26
+
27
+ const inputSx = {
28
+ "& .MuiOutlinedInput-root": {
29
+ fontSize: "1rem",
30
+ "& input": { py: 1.5 },
31
+ "&:hover fieldset": { borderColor: "primary.main" },
32
+ },
33
+ };
34
+
35
+ const primaryButtonSx = {
36
+ py: 1.5,
37
+ fontSize: "1rem",
38
+ fontWeight: 600,
39
+ textTransform: "none",
40
+ borderRadius: 1.5,
41
+ "&:active": { transform: "translateY(0px)" },
42
+ };
6
43
 
7
44
  export default function CognitoLogin() {
8
- const [username, setUsername] = useState("");
45
+ const [mode, setMode] = useState("login");
46
+
47
+ const [email, setEmail] = useState("");
9
48
  const [password, setPassword] = useState("");
49
+ const [confirmPassword, setConfirmPassword] = useState("");
50
+ const [code, setCode] = useState("");
51
+ const [showPassword, setShowPassword] = useState(false);
52
+ const [showConfirmPassword, setShowConfirmPassword] = useState(false);
53
+
54
+ const navigate = useNavigate();
55
+
56
+ const { appId, credentials } = config();
10
57
 
11
58
  const handleLogin = async () => {
12
59
  try {
13
- const tokens = await loginWithCognito(username, password);
14
- console.log("Login successful, tokens:", tokens);
15
- storage.set("link", "accessToken", tokens.AccessToken);
16
- storage.set("link", "refreshToken", tokens.RefreshToken);
17
- window.location.href = "/";
60
+ await login(email, password);
61
+
62
+ const tokens = await getTokens();
63
+ if (!tokens?.accessToken)
64
+ throw new Error("No Cognito access token received");
65
+
66
+ const requestUrl = credentials.requestUrl || "/api/oauth";
67
+
68
+ const res = await fetch(requestUrl, {
69
+ method: "POST",
70
+ headers: { "Content-Type": "application/json" },
71
+ body: JSON.stringify({
72
+ appId,
73
+ projectId: "cb16e069-6214-47f1-9922-1f7fe7629525",
74
+ identityProvider: "COGNITO",
75
+ refreshToken: tokens.accessToken,
76
+ }),
77
+ });
78
+
79
+ if (!res.ok) throw new Error("Backend OAuth exchange failed");
80
+
81
+ const data = await res.json();
82
+
83
+ storage.set("link", "accessToken", data.accessToken);
84
+ storage.set("link", "refreshToken", data.refreshToken);
85
+ storage.set("link", "identityProvider", "COGNITO");
86
+
87
+ navigate("/");
18
88
  } catch (e) {
19
89
  console.error("Login error:", e);
20
- alert(`Login failed: ${e.message || e}`);
90
+ publish("GLOBAL_MESSAGE_POSTED", {
91
+ status: true,
92
+ message: e.message || "Login failed",
93
+ severity: "error",
94
+ });
95
+ }
96
+ };
97
+
98
+ const handleSignup = async () => {
99
+ try {
100
+ await signup(email, password);
101
+ publish("GLOBAL_MESSAGE_POSTED", {
102
+ status: true,
103
+ message:
104
+ "Signup successful! Please check your email for the confirmation code.",
105
+ severity: "success",
106
+ });
107
+ setMode("confirm");
108
+ } catch (e) {
109
+ publish("GLOBAL_MESSAGE_POSTED", {
110
+ status: true,
111
+ message: e.message || "Signup failed",
112
+ severity: "error",
113
+ });
114
+ }
115
+ };
116
+
117
+ const handleConfirm = async () => {
118
+ try {
119
+ await confirmSignup(email, code);
120
+ publish("GLOBAL_MESSAGE_POSTED", {
121
+ status: true,
122
+ message: "Account confirmed! You can now log in.",
123
+ severity: "success",
124
+ });
125
+ setMode("login");
126
+ } catch (e) {
127
+ publish("GLOBAL_MESSAGE_POSTED", {
128
+ status: true,
129
+ message: e.message || "Confirmation failed",
130
+ severity: "error",
131
+ });
21
132
  }
22
133
  };
23
134
 
135
+ const titles = {
136
+ login: { heading: "Sign in", sub: "Welcome back! Enter your credentials." },
137
+ signup: {
138
+ heading: "Create account",
139
+ sub: "Sign up to get started for free.",
140
+ },
141
+ confirm: {
142
+ heading: "Verify your email",
143
+ sub: "Enter the confirmation code sent to your inbox.",
144
+ },
145
+ };
146
+
147
+ const passwordAdornment = (
148
+ <InputAdornment position="end">
149
+ <IconButton
150
+ onClick={() => setShowPassword(!showPassword)}
151
+ edge="end"
152
+ size="small"
153
+ tabIndex={-1}
154
+ >
155
+ {showPassword ? <VisibilityOff /> : <Visibility />}
156
+ </IconButton>
157
+ </InputAdornment>
158
+ );
159
+
24
160
  return (
25
- <>
26
- <Typography variant="h4">Login</Typography>
161
+ <Stack spacing={3}>
162
+ <Box sx={{ mb: 1, textAlign: "center" }}>
163
+ <Box
164
+ sx={{
165
+ width: 52,
166
+ height: 52,
167
+ borderRadius: 2,
168
+ display: "inline-flex",
169
+ alignItems: "center",
170
+ justifyContent: "center",
171
+ mb: 2,
172
+ bgcolor: (theme) => alpha(theme.palette.primary.main, 0.1),
173
+ color: "primary.main",
174
+ }}
175
+ >
176
+ {mode === "confirm" ? (
177
+ <MarkEmailReadOutlined sx={{ fontSize: 26 }} />
178
+ ) : (
179
+ <LockOutlined sx={{ fontSize: 26 }} />
180
+ )}
181
+ </Box>
182
+
183
+ <Typography variant="h4" fontWeight={700} gutterBottom>
184
+ {titles[mode].heading}
185
+ </Typography>
186
+ <Typography variant="body2" color="text.secondary">
187
+ {titles[mode].sub}
188
+ </Typography>
189
+ </Box>
190
+
27
191
  <Stack spacing={2}>
28
192
  <TextField
29
- label="Username or Email"
30
- value={username}
31
- onChange={(e) => setUsername(e.target.value)}
193
+ label="Email"
194
+ value={email}
195
+ onChange={(e) => setEmail(e.target.value)}
196
+ fullWidth
197
+ InputProps={{
198
+ startAdornment: (
199
+ <InputAdornment position="start">
200
+ <EmailOutlined sx={{ color: "text.secondary", fontSize: 22 }} />
201
+ </InputAdornment>
202
+ ),
203
+ }}
204
+ sx={inputSx}
32
205
  />
33
- <TextField
34
- type="password"
35
- label="Password"
36
- value={password}
37
- onChange={(e) => setPassword(e.target.value)}
38
- />
39
- <Button variant="contained" onClick={handleLogin}>
40
- Login
41
- </Button>
206
+
207
+ {mode !== "confirm" && (
208
+ <TextField
209
+ type={showPassword ? "text" : "password"}
210
+ label="Password"
211
+ value={password}
212
+ onChange={(e) => setPassword(e.target.value)}
213
+ fullWidth
214
+ InputProps={{
215
+ startAdornment: (
216
+ <InputAdornment position="start">
217
+ <LockOutlined
218
+ sx={{ color: "text.secondary", fontSize: 22 }}
219
+ />
220
+ </InputAdornment>
221
+ ),
222
+ endAdornment: passwordAdornment,
223
+ }}
224
+ sx={inputSx}
225
+ />
226
+ )}
227
+
228
+ {mode === "signup" && (
229
+ <TextField
230
+ type={showConfirmPassword ? "text" : "password"}
231
+ label="Confirm Password"
232
+ value={confirmPassword}
233
+ onChange={(e) => setConfirmPassword(e.target.value)}
234
+ fullWidth
235
+ InputProps={{
236
+ startAdornment: (
237
+ <InputAdornment position="start">
238
+ <LockOutlined
239
+ sx={{ color: "text.secondary", fontSize: 22 }}
240
+ />
241
+ </InputAdornment>
242
+ ),
243
+ endAdornment: (
244
+ <InputAdornment position="end">
245
+ <IconButton
246
+ onClick={() => setShowConfirmPassword(!showConfirmPassword)}
247
+ edge="end"
248
+ size="small"
249
+ tabIndex={-1}
250
+ >
251
+ {showConfirmPassword ? <VisibilityOff /> : <Visibility />}
252
+ </IconButton>
253
+ </InputAdornment>
254
+ ),
255
+ }}
256
+ sx={inputSx}
257
+ />
258
+ )}
259
+
260
+ {mode === "confirm" && (
261
+ <TextField
262
+ label="Confirmation Code"
263
+ value={code}
264
+ onChange={(e) => setCode(e.target.value)}
265
+ fullWidth
266
+ InputProps={{
267
+ startAdornment: (
268
+ <InputAdornment position="start">
269
+ <CheckOutlined
270
+ sx={{ color: "text.secondary", fontSize: 22 }}
271
+ />
272
+ </InputAdornment>
273
+ ),
274
+ }}
275
+ sx={inputSx}
276
+ />
277
+ )}
42
278
  </Stack>
43
- </>
279
+
280
+ {mode === "login" && (
281
+ <Stack spacing={1.5}>
282
+ <Button
283
+ variant="contained"
284
+ onClick={handleLogin}
285
+ size="large"
286
+ fullWidth
287
+ sx={primaryButtonSx}
288
+ >
289
+ Sign in
290
+ </Button>
291
+ <Button
292
+ onClick={() => setMode("signup")}
293
+ fullWidth
294
+ sx={{ textTransform: "none", fontWeight: 500 }}
295
+ >
296
+ Create an account
297
+ </Button>
298
+ </Stack>
299
+ )}
300
+
301
+ {mode === "signup" && (
302
+ <Stack spacing={1.5}>
303
+ <Button
304
+ variant="contained"
305
+ onClick={handleSignup}
306
+ size="large"
307
+ fullWidth
308
+ sx={primaryButtonSx}
309
+ >
310
+ Sign Up
311
+ </Button>
312
+ <Button
313
+ onClick={() => setMode("login")}
314
+ fullWidth
315
+ sx={{ textTransform: "none", fontWeight: 500 }}
316
+ >
317
+ Back to login
318
+ </Button>
319
+ </Stack>
320
+ )}
321
+
322
+ {mode === "confirm" && (
323
+ <Button
324
+ variant="contained"
325
+ onClick={handleConfirm}
326
+ size="large"
327
+ fullWidth
328
+ sx={primaryButtonSx}
329
+ >
330
+ Confirm
331
+ </Button>
332
+ )}
333
+ </Stack>
44
334
  );
45
335
  }
@@ -1,3 +1,7 @@
1
+ import config from "../../config/config";
2
+ import { storage } from "@nucleoidjs/webstorage";
3
+ import { useNavigate } from "react-router-dom";
4
+
1
5
  import {
2
6
  Box,
3
7
  Button,
@@ -16,20 +20,18 @@ import {
16
20
  } from "@mui/icons-material";
17
21
  import React, { useState } from "react";
18
22
 
19
- import config from "../../config/config";
20
- import { storage } from "@nucleoidjs/webstorage";
21
- import { useNavigate } from "react-router-dom";
22
-
23
23
  export default function DemoLogin() {
24
24
  const [username, setUsername] = useState("");
25
25
  const [password, setPassword] = useState("");
26
26
  const [showPassword, setShowPassword] = useState(false);
27
27
  const navigate = useNavigate();
28
28
 
29
- const { appId } = config();
29
+ const { appId, credentials } = config();
30
30
 
31
31
  async function handleLogin() {
32
- const res = await fetch("/api/oauth/demo", {
32
+ const requestUrl = credentials.requestUrl || "/api/oauth";
33
+
34
+ const res = await fetch(requestUrl, {
33
35
  method: "POST",
34
36
  headers: { "Content-Type": "application/json" },
35
37
  body: JSON.stringify({
@@ -37,6 +39,7 @@ export default function DemoLogin() {
37
39
  projectId: "cb16e069-6214-47f1-9922-1f7fe7629525",
38
40
  username,
39
41
  password,
42
+ identityProvider: "DEMO",
40
43
  }),
41
44
  });
42
45
 
@@ -46,7 +49,7 @@ export default function DemoLogin() {
46
49
 
47
50
  storage.set("link", "accessToken", data.accessToken);
48
51
  storage.set("link", "refreshToken", data.refreshToken);
49
- storage.set("link", "identityProvider", "Demo");
52
+ storage.set("link", "identityProvider", "DEMO");
50
53
 
51
54
  navigate("/");
52
55
  }
@@ -0,0 +1,44 @@
1
+ import {
2
+ confirmSignUp,
3
+ fetchAuthSession,
4
+ signIn,
5
+ signOut,
6
+ signUp,
7
+ } from "aws-amplify/auth";
8
+
9
+ export async function login(email, password) {
10
+ return signIn({ username: email, password });
11
+ }
12
+
13
+ export async function signup(email, password) {
14
+ return signUp({
15
+ username: email,
16
+ password,
17
+ options: {
18
+ userAttributes: {
19
+ email,
20
+ },
21
+ },
22
+ });
23
+ }
24
+
25
+ export async function confirmSignup(email, code) {
26
+ return confirmSignUp({
27
+ username: email,
28
+ confirmationCode: code,
29
+ });
30
+ }
31
+
32
+ export async function logout() {
33
+ await signOut();
34
+ }
35
+
36
+ export async function getTokens() {
37
+ const session = await fetchAuthSession();
38
+
39
+ return {
40
+ accessToken: session.tokens?.accessToken?.toString(),
41
+ refreshToken: session.tokens?.refreshToken?.toString(),
42
+ idToken: session.tokens?.idToken?.toString(),
43
+ };
44
+ }
@@ -0,0 +1,23 @@
1
+ import { Amplify } from "aws-amplify";
2
+ import config from "../../config/config";
3
+
4
+ export function configureAmplify() {
5
+ const { credentials } = config();
6
+
7
+ if (!credentials) {
8
+ throw new Error("CONFIG not initialized yet");
9
+ }
10
+
11
+ Amplify.configure({
12
+ Auth: {
13
+ Cognito: {
14
+ userPoolId: credentials.userPoolId,
15
+ userPoolClientId: credentials.clientId,
16
+ region: credentials.region,
17
+ loginWith: {
18
+ email: true,
19
+ },
20
+ },
21
+ },
22
+ });
23
+ }
@@ -1,3 +1,10 @@
1
+ import Iconify from "../components/Iconify";
2
+ import config from "../config/config";
3
+ import pkg from "../../../../../package.json";
4
+ import { useEvent } from "@nucleoidai/react-event";
5
+ import useSettings from "../hooks/useSettings";
6
+ import { useUser } from "../hooks/use-user";
7
+
1
8
  import {
2
9
  Avatar,
3
10
  Box,
@@ -22,13 +29,6 @@ import {
22
29
  import { Button, Dialog, DialogActions, DialogContent } from "@mui/material";
23
30
  import React, { useEffect, useState } from "react";
24
31
 
25
- import Iconify from "../components/Iconify";
26
- import config from "../config/config";
27
- import pkg from "../../../../../../package.json";
28
- import { useEvent } from "@nucleoidai/react-event";
29
- import useSettings from "../hooks/useSettings";
30
- import { useUser } from "../hooks/use-user";
31
-
32
32
  function a11yProps(index) {
33
33
  return {
34
34
  id: `vertical-tab-${index}`,