@canmingir/link 1.2.21 → 1.2.22

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.22",
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
  };
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
 
@@ -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,45 +1,333 @@
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 } = 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 res = await fetch("/api/oauth", {
67
+ method: "POST",
68
+ headers: { "Content-Type": "application/json" },
69
+ body: JSON.stringify({
70
+ appId,
71
+ projectId: "cb16e069-6214-47f1-9922-1f7fe7629525",
72
+ identityProvider: "COGNITO",
73
+ refreshToken: tokens.accessToken,
74
+ }),
75
+ });
76
+
77
+ if (!res.ok) throw new Error("Backend OAuth exchange failed");
78
+
79
+ const data = await res.json();
80
+
81
+ storage.set("link", "accessToken", data.accessToken);
82
+ storage.set("link", "refreshToken", data.refreshToken);
83
+ storage.set("link", "identityProvider", "COGNITO");
84
+
85
+ navigate("/");
18
86
  } catch (e) {
19
87
  console.error("Login error:", e);
20
- alert(`Login failed: ${e.message || e}`);
88
+ publish("GLOBAL_MESSAGE_POSTED", {
89
+ status: true,
90
+ message: e.message || "Login failed",
91
+ severity: "error",
92
+ });
21
93
  }
22
94
  };
23
95
 
96
+ const handleSignup = async () => {
97
+ try {
98
+ await signup(email, password);
99
+ publish("GLOBAL_MESSAGE_POSTED", {
100
+ status: true,
101
+ message:
102
+ "Signup successful! Please check your email for the confirmation code.",
103
+ severity: "success",
104
+ });
105
+ setMode("confirm");
106
+ } catch (e) {
107
+ publish("GLOBAL_MESSAGE_POSTED", {
108
+ status: true,
109
+ message: e.message || "Signup failed",
110
+ severity: "error",
111
+ });
112
+ }
113
+ };
114
+
115
+ const handleConfirm = async () => {
116
+ try {
117
+ await confirmSignup(email, code);
118
+ publish("GLOBAL_MESSAGE_POSTED", {
119
+ status: true,
120
+ message: "Account confirmed! You can now log in.",
121
+ severity: "success",
122
+ });
123
+ setMode("login");
124
+ } catch (e) {
125
+ publish("GLOBAL_MESSAGE_POSTED", {
126
+ status: true,
127
+ message: e.message || "Confirmation failed",
128
+ severity: "error",
129
+ });
130
+ }
131
+ };
132
+
133
+ const titles = {
134
+ login: { heading: "Sign in", sub: "Welcome back! Enter your credentials." },
135
+ signup: {
136
+ heading: "Create account",
137
+ sub: "Sign up to get started for free.",
138
+ },
139
+ confirm: {
140
+ heading: "Verify your email",
141
+ sub: "Enter the confirmation code sent to your inbox.",
142
+ },
143
+ };
144
+
145
+ const passwordAdornment = (
146
+ <InputAdornment position="end">
147
+ <IconButton
148
+ onClick={() => setShowPassword(!showPassword)}
149
+ edge="end"
150
+ size="small"
151
+ tabIndex={-1}
152
+ >
153
+ {showPassword ? <VisibilityOff /> : <Visibility />}
154
+ </IconButton>
155
+ </InputAdornment>
156
+ );
157
+
24
158
  return (
25
- <>
26
- <Typography variant="h4">Login</Typography>
159
+ <Stack spacing={3}>
160
+ <Box sx={{ mb: 1, textAlign: "center" }}>
161
+ <Box
162
+ sx={{
163
+ width: 52,
164
+ height: 52,
165
+ borderRadius: 2,
166
+ display: "inline-flex",
167
+ alignItems: "center",
168
+ justifyContent: "center",
169
+ mb: 2,
170
+ bgcolor: (theme) => alpha(theme.palette.primary.main, 0.1),
171
+ color: "primary.main",
172
+ }}
173
+ >
174
+ {mode === "confirm" ? (
175
+ <MarkEmailReadOutlined sx={{ fontSize: 26 }} />
176
+ ) : (
177
+ <LockOutlined sx={{ fontSize: 26 }} />
178
+ )}
179
+ </Box>
180
+
181
+ <Typography variant="h4" fontWeight={700} gutterBottom>
182
+ {titles[mode].heading}
183
+ </Typography>
184
+ <Typography variant="body2" color="text.secondary">
185
+ {titles[mode].sub}
186
+ </Typography>
187
+ </Box>
188
+
27
189
  <Stack spacing={2}>
28
190
  <TextField
29
- label="Username or Email"
30
- value={username}
31
- onChange={(e) => setUsername(e.target.value)}
32
- />
33
- <TextField
34
- type="password"
35
- label="Password"
36
- value={password}
37
- onChange={(e) => setPassword(e.target.value)}
191
+ label="Email"
192
+ value={email}
193
+ onChange={(e) => setEmail(e.target.value)}
194
+ fullWidth
195
+ InputProps={{
196
+ startAdornment: (
197
+ <InputAdornment position="start">
198
+ <EmailOutlined sx={{ color: "text.secondary", fontSize: 22 }} />
199
+ </InputAdornment>
200
+ ),
201
+ }}
202
+ sx={inputSx}
38
203
  />
39
- <Button variant="contained" onClick={handleLogin}>
40
- Login
41
- </Button>
204
+
205
+ {mode !== "confirm" && (
206
+ <TextField
207
+ type={showPassword ? "text" : "password"}
208
+ label="Password"
209
+ value={password}
210
+ onChange={(e) => setPassword(e.target.value)}
211
+ fullWidth
212
+ InputProps={{
213
+ startAdornment: (
214
+ <InputAdornment position="start">
215
+ <LockOutlined
216
+ sx={{ color: "text.secondary", fontSize: 22 }}
217
+ />
218
+ </InputAdornment>
219
+ ),
220
+ endAdornment: passwordAdornment,
221
+ }}
222
+ sx={inputSx}
223
+ />
224
+ )}
225
+
226
+ {mode === "signup" && (
227
+ <TextField
228
+ type={showConfirmPassword ? "text" : "password"}
229
+ label="Confirm Password"
230
+ value={confirmPassword}
231
+ onChange={(e) => setConfirmPassword(e.target.value)}
232
+ fullWidth
233
+ InputProps={{
234
+ startAdornment: (
235
+ <InputAdornment position="start">
236
+ <LockOutlined
237
+ sx={{ color: "text.secondary", fontSize: 22 }}
238
+ />
239
+ </InputAdornment>
240
+ ),
241
+ endAdornment: (
242
+ <InputAdornment position="end">
243
+ <IconButton
244
+ onClick={() => setShowConfirmPassword(!showConfirmPassword)}
245
+ edge="end"
246
+ size="small"
247
+ tabIndex={-1}
248
+ >
249
+ {showConfirmPassword ? <VisibilityOff /> : <Visibility />}
250
+ </IconButton>
251
+ </InputAdornment>
252
+ ),
253
+ }}
254
+ sx={inputSx}
255
+ />
256
+ )}
257
+
258
+ {mode === "confirm" && (
259
+ <TextField
260
+ label="Confirmation Code"
261
+ value={code}
262
+ onChange={(e) => setCode(e.target.value)}
263
+ fullWidth
264
+ InputProps={{
265
+ startAdornment: (
266
+ <InputAdornment position="start">
267
+ <CheckOutlined
268
+ sx={{ color: "text.secondary", fontSize: 22 }}
269
+ />
270
+ </InputAdornment>
271
+ ),
272
+ }}
273
+ sx={inputSx}
274
+ />
275
+ )}
42
276
  </Stack>
43
- </>
277
+
278
+ {mode === "login" && (
279
+ <Stack spacing={1.5}>
280
+ <Button
281
+ variant="contained"
282
+ onClick={handleLogin}
283
+ size="large"
284
+ fullWidth
285
+ sx={primaryButtonSx}
286
+ >
287
+ Sign in
288
+ </Button>
289
+ <Button
290
+ onClick={() => setMode("signup")}
291
+ fullWidth
292
+ sx={{ textTransform: "none", fontWeight: 500 }}
293
+ >
294
+ Create an account
295
+ </Button>
296
+ </Stack>
297
+ )}
298
+
299
+ {mode === "signup" && (
300
+ <Stack spacing={1.5}>
301
+ <Button
302
+ variant="contained"
303
+ onClick={handleSignup}
304
+ size="large"
305
+ fullWidth
306
+ sx={primaryButtonSx}
307
+ >
308
+ Sign Up
309
+ </Button>
310
+ <Button
311
+ onClick={() => setMode("login")}
312
+ fullWidth
313
+ sx={{ textTransform: "none", fontWeight: 500 }}
314
+ >
315
+ Back to login
316
+ </Button>
317
+ </Stack>
318
+ )}
319
+
320
+ {mode === "confirm" && (
321
+ <Button
322
+ variant="contained"
323
+ onClick={handleConfirm}
324
+ size="large"
325
+ fullWidth
326
+ sx={primaryButtonSx}
327
+ >
328
+ Confirm
329
+ </Button>
330
+ )}
331
+ </Stack>
44
332
  );
45
333
  }
@@ -29,7 +29,7 @@ export default function DemoLogin() {
29
29
  const { appId } = config();
30
30
 
31
31
  async function handleLogin() {
32
- const res = await fetch("/api/oauth/demo", {
32
+ const res = await fetch("/api/oauth", {
33
33
  method: "POST",
34
34
  headers: { "Content-Type": "application/json" },
35
35
  body: JSON.stringify({
@@ -37,6 +37,7 @@ export default function DemoLogin() {
37
37
  projectId: "cb16e069-6214-47f1-9922-1f7fe7629525",
38
38
  username,
39
39
  password,
40
+ identityProvider: "DEMO",
40
41
  }),
41
42
  });
42
43
 
@@ -46,7 +47,7 @@ export default function DemoLogin() {
46
47
 
47
48
  storage.set("link", "accessToken", data.accessToken);
48
49
  storage.set("link", "refreshToken", data.refreshToken);
49
- storage.set("link", "identityProvider", "Demo");
50
+ storage.set("link", "identityProvider", "DEMO");
50
51
 
51
52
  navigate("/");
52
53
  }
@@ -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
+ }