@plutonhq/core-frontend 0.1.21 → 0.1.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,7 +1,7 @@
1
1
  {
2
2
  "name": "@plutonhq/core-frontend",
3
3
  "description": "Pluton Core Frontend Library",
4
- "version": "0.1.21",
4
+ "version": "0.1.22",
5
5
  "author": "Plutonhq",
6
6
  "license": "Apache-2.0",
7
7
  "publishConfig": {
@@ -166,3 +166,77 @@
166
166
  }
167
167
  }
168
168
  }
169
+
170
+ .oauthContainer {
171
+ background-color: var(--background-color);
172
+ border: 1px solid var(--line-color);
173
+ border-radius: 6px;
174
+ box-sizing: border-box;
175
+ padding: 12px;
176
+ word-break: break-all;
177
+ &.success {
178
+ background-color: var(--success-bg-color);
179
+ border-color: var(--success-bg-color);
180
+ color: var(--success-text-color);
181
+ }
182
+ &.error {
183
+ background-color: var(--error-bg-color);
184
+ border-color: var(--error-bg-color);
185
+ color: var(--error-text-color);
186
+ }
187
+ }
188
+
189
+ .oauthButton {
190
+ display: flex;
191
+ width: 100%;
192
+ flex-direction: column;
193
+ align-items: flex-end;
194
+ .oauthAuthorizeBtn {
195
+ display: flex;
196
+ align-items: center;
197
+ gap: 8px;
198
+ padding: 10px 18px;
199
+ font-size: 13px;
200
+ font-weight: 600;
201
+ color: #fff;
202
+ background-color: var(--primary-color);
203
+ border: none;
204
+ border-radius: 6px;
205
+ cursor: pointer;
206
+ transition: background-color 0.2s;
207
+ &:hover {
208
+ opacity: 0.9;
209
+ }
210
+ }
211
+ }
212
+
213
+ .oauthInnerBtn {
214
+ display: inline-flex;
215
+ align-items: center;
216
+ gap: 6px;
217
+ padding: 6px 14px;
218
+ font-size: 12px;
219
+ font-weight: 500;
220
+ color: var(--content-text-color);
221
+ background-color: var(--content-background-color);
222
+ border: none;
223
+ border-radius: 6px;
224
+ cursor: pointer;
225
+ margin-top: 8px;
226
+ &:hover {
227
+ background-color: var(--primary-color);
228
+ color: var(--primary-text-color);
229
+ }
230
+ }
231
+
232
+ .oauthProgress {
233
+ p {
234
+ margin: 6px 0;
235
+ font-size: 13px;
236
+ line-height: 1.5;
237
+ a {
238
+ color: var(--primary-color);
239
+ text-decoration: underline;
240
+ }
241
+ }
242
+ }
@@ -1,9 +1,10 @@
1
- import { useEffect, useState } from 'react';
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import { storageOptionField } from '../../../@types/storages';
3
3
  import Tristate from '../../common/form/Tristate/Tristate';
4
4
  import Icon from '../../common/Icon/Icon';
5
5
  import classes from '../AddStorage/AddStorage.module.scss';
6
6
  import StorageSettings from '../StorageSettings/StorageSettings';
7
+ import { startStorageAuthorize, getStorageAuthorizeStatus, cancelStorageAuthorize } from '../../../services/storage';
7
8
 
8
9
  interface StorageAuthSettingsProps {
9
10
  storageType: string;
@@ -16,6 +17,8 @@ interface StorageAuthSettingsProps {
16
17
  onAuthTypeChange: (authType: string) => void;
17
18
  }
18
19
 
20
+ type OAuthStatus = 'idle' | 'authorizing' | 'success' | 'error';
21
+
19
22
  const StorageAuthSettings = ({
20
23
  storageType,
21
24
  fields,
@@ -29,6 +32,16 @@ const StorageAuthSettings = ({
29
32
  const [showAdvanced, setShowAdvanced] = useState(true);
30
33
  const [showOAuthDoc, setShowOAuthDoc] = useState(false);
31
34
 
35
+ // OAuth auto-authorize state
36
+ const [oauthStatus, setOauthStatus] = useState<OAuthStatus>('idle');
37
+ const [authSessionId, setAuthSessionId] = useState<string | null>(null);
38
+ const [authUrl, setAuthUrl] = useState<string | null>(null);
39
+ const [authError, setAuthError] = useState<string | null>(null);
40
+ const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null);
41
+
42
+ const installType = (window as any).plutonInstallType;
43
+ const isDesktop = installType === 'binary' || installType === 'dev';
44
+
32
45
  console.log('availableAuthTypes :', fields, authTypes, currentAuthType);
33
46
 
34
47
  useEffect(() => {
@@ -37,6 +50,76 @@ const StorageAuthSettings = ({
37
50
  }
38
51
  }, [authTypes, currentAuthType, onAuthTypeChange]);
39
52
 
53
+ // Cleanup polling on unmount
54
+ useEffect(() => {
55
+ return () => {
56
+ if (pollingRef.current) {
57
+ clearInterval(pollingRef.current);
58
+ }
59
+ };
60
+ }, []);
61
+
62
+ const stopPolling = useCallback(() => {
63
+ if (pollingRef.current) {
64
+ clearInterval(pollingRef.current);
65
+ pollingRef.current = null;
66
+ }
67
+ }, []);
68
+
69
+ const startOAuthAuthorize = useCallback(async () => {
70
+ setOauthStatus('authorizing');
71
+ setAuthUrl(null);
72
+ setAuthError(null);
73
+
74
+ try {
75
+ const { sessionId } = await startStorageAuthorize(storageType);
76
+ setAuthSessionId(sessionId);
77
+
78
+ // Start polling every 2 seconds
79
+ pollingRef.current = setInterval(async () => {
80
+ try {
81
+ const result = await getStorageAuthorizeStatus(sessionId);
82
+
83
+ if (result.authUrl) {
84
+ setAuthUrl(result.authUrl);
85
+ }
86
+
87
+ if (result.status === 'success' && result.token) {
88
+ stopPolling();
89
+ setOauthStatus('success');
90
+ onUpdate({ ...settings, token: result.token });
91
+ } else if (result.status === 'error') {
92
+ stopPolling();
93
+ setOauthStatus('error');
94
+ setAuthError(result.error || 'Authorization failed');
95
+ }
96
+ } catch (err: any) {
97
+ stopPolling();
98
+ setOauthStatus('error');
99
+ setAuthError(err?.message || 'Failed to check authorization status');
100
+ }
101
+ }, 2000);
102
+ } catch (err: any) {
103
+ setOauthStatus('error');
104
+ setAuthError(err?.message || 'Failed to start authorization');
105
+ }
106
+ }, [storageType, settings, onUpdate, stopPolling]);
107
+
108
+ const handleCancelAuthorize = useCallback(async () => {
109
+ stopPolling();
110
+ if (authSessionId) {
111
+ try {
112
+ await cancelStorageAuthorize(authSessionId);
113
+ } catch {
114
+ // Ignore cancel errors
115
+ }
116
+ }
117
+ setOauthStatus('idle');
118
+ setAuthSessionId(null);
119
+ setAuthUrl(null);
120
+ setAuthError(null);
121
+ }, [authSessionId, stopPolling]);
122
+
40
123
  return (
41
124
  <div className={classes.authSettings}>
42
125
  <div
@@ -67,14 +150,64 @@ const StorageAuthSettings = ({
67
150
  />
68
151
  </div>
69
152
  )}
70
-
71
153
  <StorageSettings
72
154
  fields={fields.filter((f) => f.authFieldType === currentAuthType)}
73
155
  settings={settings}
74
156
  onUpdate={(newSettings) => onUpdate(newSettings)}
75
157
  errors={errors}
76
158
  />
77
- {currentAuthType === 'oauth' && (
159
+
160
+ {currentAuthType === 'oauth' && isDesktop && oauthStatus === 'idle' && (
161
+ <div className={classes.oauthButton}>
162
+ <button className={classes.oauthAuthorizeBtn} onClick={startOAuthAuthorize}>
163
+ <Icon type={'key'} size={14} /> Authorize & Get Access Token
164
+ </button>
165
+ </div>
166
+ )}
167
+
168
+ {currentAuthType === 'oauth' && isDesktop && oauthStatus !== 'idle' && (
169
+ <div className={`${classes.oauthContainer} ${classes[oauthStatus]}`}>
170
+ {oauthStatus === 'authorizing' && (
171
+ <div className={classes.oauthProgress}>
172
+ <p>
173
+ <strong>Waiting for authorization...</strong>
174
+ <br />A browser window should have opened. Please authorize the connection in your browser.
175
+ </p>
176
+ {authUrl && (
177
+ <p>
178
+ If the browser didn't open automatically,{' '}
179
+ <a href={authUrl} target="_blank" rel="noopener noreferrer">
180
+ click here to authorize
181
+ </a>
182
+ .
183
+ </p>
184
+ )}
185
+ <button className={classes.oauthInnerBtn} onClick={handleCancelAuthorize}>
186
+ Cancel
187
+ </button>
188
+ </div>
189
+ )}
190
+ {oauthStatus === 'success' && (
191
+ <div className={classes.oauthProgress}>
192
+ <p>
193
+ <Icon type={'check'} size={14} /> <strong>Authorization successful!</strong> Token has been automatically filled in.
194
+ </p>
195
+ </div>
196
+ )}
197
+ {oauthStatus === 'error' && (
198
+ <div className={classes.oauthProgress}>
199
+ <p>
200
+ <strong>Authorization failed:</strong> {authError}
201
+ </p>
202
+ <button className={classes.oauthInnerBtn} onClick={startOAuthAuthorize}>
203
+ <Icon type={'key'} size={14} /> Try Again
204
+ </button>
205
+ </div>
206
+ )}
207
+ </div>
208
+ )}
209
+
210
+ {currentAuthType === 'oauth' && !isDesktop && (
78
211
  <div className={classes.oauthDoc}>
79
212
  <h4 onClick={() => setShowOAuthDoc(!showOAuthDoc)}>
80
213
  <Icon type={'key'} size={14} /> Acquiring the OAuth Access Token
@@ -198,3 +198,52 @@ export function useVerifyStorage() {
198
198
  },
199
199
  });
200
200
  }
201
+
202
+ // ── OAuth Authorization ─────────────────────────────────────────────
203
+ export async function startStorageAuthorize(type: string): Promise<{ sessionId: string }> {
204
+ const header = new Headers({ 'Content-Type': 'application/json', Accept: 'application/json' });
205
+ const res = await fetch(`${API_URL}/storages/authorize`, {
206
+ method: 'POST',
207
+ credentials: 'include',
208
+ headers: header,
209
+ body: JSON.stringify({ type }),
210
+ });
211
+ const data = await res.json();
212
+ if (!data.success) {
213
+ throw new Error(data.error);
214
+ }
215
+ return data.result;
216
+ }
217
+
218
+ export async function getStorageAuthorizeStatus(sessionId: string): Promise<{
219
+ status: 'pending' | 'success' | 'error';
220
+ token?: string;
221
+ error?: string;
222
+ authUrl?: string;
223
+ }> {
224
+ const url = new URL(`${API_URL}/storages/authorize/status`);
225
+ url.searchParams.set('sessionId', sessionId);
226
+ const res = await fetch(url.toString(), {
227
+ method: 'GET',
228
+ credentials: 'include',
229
+ });
230
+ const data = await res.json();
231
+ if (!data.success) {
232
+ throw new Error(data.error);
233
+ }
234
+ return data.result;
235
+ }
236
+
237
+ export async function cancelStorageAuthorize(sessionId: string): Promise<void> {
238
+ const header = new Headers({ 'Content-Type': 'application/json', Accept: 'application/json' });
239
+ const res = await fetch(`${API_URL}/storages/authorize/cancel`, {
240
+ method: 'POST',
241
+ credentials: 'include',
242
+ headers: header,
243
+ body: JSON.stringify({ sessionId }),
244
+ });
245
+ const data = await res.json();
246
+ if (!data.success) {
247
+ throw new Error(data.error);
248
+ }
249
+ }