@qidcloud/sdk 1.2.2 → 1.2.3
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 +133 -18
- package/dist/components/QidCloudLogin.d.ts +16 -0
- package/dist/hooks/useQidAuth.d.ts +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +220 -1913
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +220 -1914
- package/dist/index.mjs.map +1 -1
- package/dist/modules/auth.d.ts +4 -0
- package/package.json +5 -3
- package/src/components/QidCloudLogin.tsx +64 -0
- package/src/components/QidSignInButton.tsx +92 -6
- package/src/hooks/useQidAuth.ts +94 -19
- package/src/index.ts +1 -0
- package/src/modules/auth.ts +32 -3
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { QidCloud } from '../index';
|
|
3
|
+
import { QidSignInButton } from './QidSignInButton';
|
|
4
|
+
import { QidUser } from '../types';
|
|
5
|
+
|
|
6
|
+
interface QidCloudLoginProps {
|
|
7
|
+
sdk: QidCloud;
|
|
8
|
+
onSuccess: (user: QidUser, token: string) => void;
|
|
9
|
+
onError?: (error: string) => void;
|
|
10
|
+
className?: string; // Wrapper class
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A comprehensive login component for QidCloud.
|
|
15
|
+
* STRICT SECURITY: ONLY supports QR Scanning (Mobile App) for Post-Quantum security.
|
|
16
|
+
* Conventional login is not available in the SDK to prevent password transmission in third-party apps.
|
|
17
|
+
*/
|
|
18
|
+
export const QidCloudLogin: React.FC<QidCloudLoginProps> = ({
|
|
19
|
+
sdk,
|
|
20
|
+
onSuccess,
|
|
21
|
+
onError,
|
|
22
|
+
className
|
|
23
|
+
}) => {
|
|
24
|
+
return (
|
|
25
|
+
<div className={`qid-login-container ${className || ''}`} style={{
|
|
26
|
+
fontFamily: "'Inter', sans-serif",
|
|
27
|
+
maxWidth: '380px',
|
|
28
|
+
margin: '0 auto',
|
|
29
|
+
padding: '30px',
|
|
30
|
+
backgroundColor: '#050505',
|
|
31
|
+
borderRadius: '24px',
|
|
32
|
+
border: '1px solid #333',
|
|
33
|
+
boxShadow: '0 20px 80px rgba(0,0,0,0.8)',
|
|
34
|
+
color: '#fff',
|
|
35
|
+
textAlign: 'center'
|
|
36
|
+
}}>
|
|
37
|
+
<div style={{ marginBottom: '30px' }}>
|
|
38
|
+
<h2 style={{ fontSize: '1.5rem', fontWeight: 700, margin: '0 0 10px 0', letterSpacing: '-0.02em' }}>
|
|
39
|
+
QidCloud Login
|
|
40
|
+
</h2>
|
|
41
|
+
<p style={{ color: '#888', fontSize: '0.9rem', margin: 0 }}>
|
|
42
|
+
Secure Post-Quantum Access
|
|
43
|
+
</p>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div style={{ marginBottom: '20px', padding: '10px', backgroundColor: '#111', borderRadius: '16px', border: '1px dashed #333' }}>
|
|
47
|
+
<p style={{ fontSize: '0.85rem', color: '#888', marginBottom: '20px', lineHeight: '1.5' }}>
|
|
48
|
+
Login instantly by scanning the secure QR code with your <strong>QidCloud App</strong>.
|
|
49
|
+
</p>
|
|
50
|
+
<QidSignInButton
|
|
51
|
+
sdk={sdk}
|
|
52
|
+
onSuccess={onSuccess}
|
|
53
|
+
onError={onError}
|
|
54
|
+
buttonText="Login with QIDCLOUD"
|
|
55
|
+
className="qid-qr-btn-full"
|
|
56
|
+
/>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<p style={{ fontSize: '0.75rem', color: '#444', marginTop: '20px' }}>
|
|
60
|
+
Credentials are never entered on this device.
|
|
61
|
+
</p>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
@@ -20,7 +20,7 @@ export const QidSignInButton: React.FC<QidSignInButtonProps> = ({
|
|
|
20
20
|
onSuccess,
|
|
21
21
|
onError,
|
|
22
22
|
className,
|
|
23
|
-
buttonText = '
|
|
23
|
+
buttonText = 'LOGIN WITH QIDCLOUD'
|
|
24
24
|
}: QidSignInButtonProps) => {
|
|
25
25
|
const {
|
|
26
26
|
user,
|
|
@@ -28,10 +28,14 @@ export const QidSignInButton: React.FC<QidSignInButtonProps> = ({
|
|
|
28
28
|
error,
|
|
29
29
|
session,
|
|
30
30
|
initializing,
|
|
31
|
+
isExpired,
|
|
32
|
+
timeLeft,
|
|
31
33
|
login,
|
|
32
34
|
cancel
|
|
33
35
|
} = useQidAuth(sdk);
|
|
34
36
|
|
|
37
|
+
const [appError, setAppError] = React.useState(false);
|
|
38
|
+
|
|
35
39
|
// Watch for success
|
|
36
40
|
React.useEffect(() => {
|
|
37
41
|
if (user && token) {
|
|
@@ -132,6 +136,7 @@ export const QidSignInButton: React.FC<QidSignInButtonProps> = ({
|
|
|
132
136
|
size={220}
|
|
133
137
|
level="H"
|
|
134
138
|
includeMargin={false}
|
|
139
|
+
style={{ opacity: isExpired ? 0.1 : 1, transition: 'opacity 0.3s' }}
|
|
135
140
|
imageSettings={{
|
|
136
141
|
src: "https://api.qidcloud.com/favicon.ico",
|
|
137
142
|
x: undefined,
|
|
@@ -141,14 +146,52 @@ export const QidSignInButton: React.FC<QidSignInButtonProps> = ({
|
|
|
141
146
|
excavate: true,
|
|
142
147
|
}}
|
|
143
148
|
/>
|
|
149
|
+
{isExpired && (
|
|
150
|
+
<div style={{
|
|
151
|
+
position: 'absolute',
|
|
152
|
+
top: '50%',
|
|
153
|
+
left: '50%',
|
|
154
|
+
transform: 'translate(-50%, -50%)',
|
|
155
|
+
width: '100%'
|
|
156
|
+
}}>
|
|
157
|
+
<button
|
|
158
|
+
onClick={login}
|
|
159
|
+
style={{
|
|
160
|
+
backgroundColor: '#00e5ff',
|
|
161
|
+
color: '#000',
|
|
162
|
+
border: 'none',
|
|
163
|
+
padding: '12px 20px',
|
|
164
|
+
borderRadius: '12px',
|
|
165
|
+
fontWeight: '900',
|
|
166
|
+
cursor: 'pointer',
|
|
167
|
+
boxShadow: '0 4px 15px rgba(0, 229, 255, 0.4)',
|
|
168
|
+
fontSize: '0.8rem'
|
|
169
|
+
}}
|
|
170
|
+
>
|
|
171
|
+
REFRESH QR
|
|
172
|
+
</button>
|
|
173
|
+
</div>
|
|
174
|
+
)}
|
|
144
175
|
</div>
|
|
145
176
|
|
|
146
|
-
|
|
147
|
-
<
|
|
177
|
+
{isExpired && (
|
|
178
|
+
<p style={{ color: '#ef4444', fontSize: '0.85rem', marginBottom: '20px', fontWeight: '600' }}>
|
|
179
|
+
Session expired. Please refresh.
|
|
180
|
+
</p>
|
|
181
|
+
)}
|
|
148
182
|
|
|
183
|
+
<div style={{ marginBottom: '20px' }}>
|
|
184
|
+
<p style={{ fontSize: '0.85rem', color: '#94a3b8', marginBottom: '8px' }}>
|
|
185
|
+
Are you on mobile?
|
|
186
|
+
</p>
|
|
187
|
+
<p style={{ fontSize: '0.75rem', color: '#64748b' }}>
|
|
188
|
+
Use <strong>One-Click Auth</strong> for instant access.
|
|
189
|
+
</p>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<div style={{ display: 'flex', gap: '12px', marginTop: '10px' }}>
|
|
149
193
|
<button onClick={cancel} style={{
|
|
150
|
-
|
|
151
|
-
width: '100%',
|
|
194
|
+
flex: 2,
|
|
152
195
|
padding: '16px',
|
|
153
196
|
backgroundColor: 'transparent',
|
|
154
197
|
color: '#64748b',
|
|
@@ -159,10 +202,53 @@ export const QidSignInButton: React.FC<QidSignInButtonProps> = ({
|
|
|
159
202
|
transition: 'all 0.2s',
|
|
160
203
|
fontSize: '0.9rem'
|
|
161
204
|
}}>
|
|
162
|
-
Cancel
|
|
205
|
+
Cancel
|
|
206
|
+
</button>
|
|
207
|
+
|
|
208
|
+
{/* Mobile Deep Link Button */}
|
|
209
|
+
<button
|
|
210
|
+
onClick={() => {
|
|
211
|
+
setAppError(false);
|
|
212
|
+
const deepLink = `qidcloud://authorize?sessionId=${session.sessionId}`; // Corrected format per docs
|
|
213
|
+
|
|
214
|
+
// Timeout to check if app opened
|
|
215
|
+
const start = Date.now();
|
|
216
|
+
window.location.href = deepLink;
|
|
217
|
+
|
|
218
|
+
setTimeout(() => {
|
|
219
|
+
if (Date.now() - start < 1500) {
|
|
220
|
+
setAppError(true);
|
|
221
|
+
}
|
|
222
|
+
}, 1000);
|
|
223
|
+
}}
|
|
224
|
+
style={{
|
|
225
|
+
flex: 1,
|
|
226
|
+
padding: '16px',
|
|
227
|
+
backgroundColor: 'rgba(0, 229, 255, 0.1)',
|
|
228
|
+
color: '#00e5ff',
|
|
229
|
+
border: '1px solid rgba(0, 229, 255, 0.3)',
|
|
230
|
+
borderRadius: '16px',
|
|
231
|
+
cursor: 'pointer',
|
|
232
|
+
display: 'flex',
|
|
233
|
+
alignItems: 'center',
|
|
234
|
+
justifyContent: 'center',
|
|
235
|
+
transition: 'all 0.2s'
|
|
236
|
+
}}
|
|
237
|
+
title="Authenticate with QidCloud App"
|
|
238
|
+
>
|
|
239
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
240
|
+
<rect x="5" y="2" width="14" height="20" rx="2" ry="2" />
|
|
241
|
+
<line x1="12" y1="18" x2="12.01" y2="18" />
|
|
242
|
+
</svg>
|
|
163
243
|
</button>
|
|
164
244
|
</div>
|
|
165
245
|
|
|
246
|
+
{appError && (
|
|
247
|
+
<p style={{ color: '#f59e0b', fontSize: '0.75rem', marginTop: '12px', fontWeight: '600' }}>
|
|
248
|
+
QidCloud App not found on device
|
|
249
|
+
</p>
|
|
250
|
+
)}
|
|
251
|
+
|
|
166
252
|
<div style={{ marginTop: '25px', fontSize: '0.75rem', color: '#475569' }}>
|
|
167
253
|
Session ID: {session.sessionId.slice(0, 8)}...
|
|
168
254
|
</div>
|
package/src/hooks/useQidAuth.ts
CHANGED
|
@@ -9,9 +9,12 @@ export interface UseQidAuthReturn {
|
|
|
9
9
|
error: string | null;
|
|
10
10
|
session: QidAuthSession | null;
|
|
11
11
|
initializing: boolean;
|
|
12
|
+
isExpired: boolean;
|
|
13
|
+
timeLeft: number;
|
|
12
14
|
login: () => Promise<void>;
|
|
13
15
|
logout: () => void;
|
|
14
16
|
cancel: () => void;
|
|
17
|
+
setAuthenticated: (user: QidUser, token: string) => void;
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
/**
|
|
@@ -26,69 +29,133 @@ export function useQidAuth(sdk: QidCloud): UseQidAuthReturn {
|
|
|
26
29
|
const [initializing, setInitializing] = useState(false);
|
|
27
30
|
const [error, setError] = useState<string | null>(null);
|
|
28
31
|
|
|
32
|
+
const [timeLeft, setTimeLeft] = useState<number>(0);
|
|
33
|
+
const [isExpired, setIsExpired] = useState(false);
|
|
34
|
+
|
|
29
35
|
// Use ref to track if we should still be listening (to avoid state updates after unmount/cancel)
|
|
30
36
|
const activeSessionId = useRef<string | null>(null);
|
|
37
|
+
const timerRef = useRef<NodeJS.Timeout | null>(null);
|
|
38
|
+
|
|
39
|
+
const clearTimer = useCallback(() => {
|
|
40
|
+
if (timerRef.current) {
|
|
41
|
+
clearInterval(timerRef.current);
|
|
42
|
+
timerRef.current = null;
|
|
43
|
+
}
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
const STORAGE_KEY = 'qid_auth_token';
|
|
31
47
|
|
|
32
|
-
|
|
48
|
+
// Helper to fetch profile
|
|
49
|
+
const fetchProfile = useCallback(async (authToken: string) => {
|
|
50
|
+
setLoading(true);
|
|
51
|
+
try {
|
|
52
|
+
const profile = await sdk.auth.getProfile(authToken);
|
|
53
|
+
setToken(authToken);
|
|
54
|
+
setUser(profile);
|
|
55
|
+
localStorage.setItem(STORAGE_KEY, authToken);
|
|
56
|
+
} catch (err: any) {
|
|
57
|
+
console.error('Failed to restore session:', err);
|
|
58
|
+
setError('Session expired');
|
|
59
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
60
|
+
setToken(null);
|
|
61
|
+
setUser(null);
|
|
62
|
+
} finally {
|
|
63
|
+
setLoading(false);
|
|
64
|
+
setInitializing(false);
|
|
65
|
+
}
|
|
66
|
+
}, [sdk]);
|
|
67
|
+
|
|
68
|
+
// Init: Check local storage
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
const storedToken = localStorage.getItem(STORAGE_KEY);
|
|
71
|
+
if (storedToken) {
|
|
72
|
+
fetchProfile(storedToken);
|
|
73
|
+
}
|
|
74
|
+
}, [fetchProfile]);
|
|
75
|
+
|
|
76
|
+
const logout = useCallback(async () => {
|
|
77
|
+
if (token) {
|
|
78
|
+
await sdk.auth.logout(token);
|
|
79
|
+
} else {
|
|
80
|
+
sdk.auth.disconnect();
|
|
81
|
+
}
|
|
82
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
33
83
|
setUser(null);
|
|
34
84
|
setToken(null);
|
|
35
85
|
setSession(null);
|
|
36
|
-
|
|
37
|
-
|
|
86
|
+
setIsExpired(false);
|
|
87
|
+
clearTimer();
|
|
88
|
+
}, [sdk, token, clearTimer]);
|
|
38
89
|
|
|
39
90
|
const cancel = useCallback(() => {
|
|
40
91
|
activeSessionId.current = null;
|
|
41
92
|
setSession(null);
|
|
42
93
|
setInitializing(false);
|
|
94
|
+
setIsExpired(false);
|
|
95
|
+
clearTimer();
|
|
43
96
|
sdk.auth.disconnect();
|
|
44
|
-
}, [sdk]);
|
|
97
|
+
}, [sdk, clearTimer]);
|
|
45
98
|
|
|
46
99
|
const login = useCallback(async () => {
|
|
47
100
|
setInitializing(true);
|
|
48
101
|
setError(null);
|
|
102
|
+
setIsExpired(false);
|
|
103
|
+
clearTimer();
|
|
104
|
+
|
|
49
105
|
try {
|
|
50
106
|
const newSession = await sdk.auth.createSession();
|
|
51
107
|
setSession(newSession);
|
|
52
108
|
activeSessionId.current = newSession.sessionId;
|
|
53
109
|
|
|
110
|
+
// Start Timer
|
|
111
|
+
const expiresAt = newSession.expiresAt || (Date.now() + 120000); // Fallback 2m
|
|
112
|
+
setTimeLeft(Math.max(0, Math.floor((expiresAt - Date.now()) / 1000)));
|
|
113
|
+
|
|
114
|
+
timerRef.current = setInterval(() => {
|
|
115
|
+
const remaining = Math.max(0, Math.floor((expiresAt - Date.now()) / 1000));
|
|
116
|
+
setTimeLeft(remaining);
|
|
117
|
+
|
|
118
|
+
if (remaining <= 0) {
|
|
119
|
+
setIsExpired(true);
|
|
120
|
+
clearTimer();
|
|
121
|
+
// Don't nullify session immediately, let UI show expiry state
|
|
122
|
+
}
|
|
123
|
+
}, 1000);
|
|
124
|
+
|
|
54
125
|
sdk.auth.listen(
|
|
55
126
|
newSession.sessionId,
|
|
56
127
|
async (receivedToken: string) => {
|
|
57
128
|
// Safety check: is this still the active session?
|
|
58
129
|
if (activeSessionId.current !== newSession.sessionId) return;
|
|
59
130
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
setUser(profile);
|
|
66
|
-
} catch (err: any) {
|
|
67
|
-
setError(err.message || 'Failed to fetch user profile');
|
|
68
|
-
} finally {
|
|
69
|
-
setLoading(false);
|
|
70
|
-
setSession(null);
|
|
71
|
-
}
|
|
131
|
+
clearTimer();
|
|
132
|
+
setSession(null); // Clear QR session
|
|
133
|
+
|
|
134
|
+
// Fetch profile and persist
|
|
135
|
+
await fetchProfile(receivedToken);
|
|
72
136
|
},
|
|
73
137
|
(err: string) => {
|
|
74
138
|
if (activeSessionId.current !== newSession.sessionId) return;
|
|
75
139
|
setError(err);
|
|
76
140
|
setInitializing(false);
|
|
77
141
|
setSession(null);
|
|
142
|
+
clearTimer();
|
|
78
143
|
}
|
|
79
144
|
);
|
|
80
145
|
} catch (err: any) {
|
|
81
146
|
setError(err.message || 'Failed to initiate login handshake');
|
|
82
147
|
setInitializing(false);
|
|
148
|
+
clearTimer();
|
|
83
149
|
}
|
|
84
|
-
}, [sdk]);
|
|
150
|
+
}, [sdk, clearTimer, fetchProfile]);
|
|
85
151
|
|
|
86
152
|
// Cleanup on unmount
|
|
87
153
|
useEffect(() => {
|
|
88
154
|
return () => {
|
|
155
|
+
clearTimer();
|
|
89
156
|
sdk.auth.disconnect();
|
|
90
157
|
};
|
|
91
|
-
}, [sdk]);
|
|
158
|
+
}, [sdk, clearTimer]);
|
|
92
159
|
|
|
93
160
|
return {
|
|
94
161
|
user,
|
|
@@ -97,8 +164,16 @@ export function useQidAuth(sdk: QidCloud): UseQidAuthReturn {
|
|
|
97
164
|
error,
|
|
98
165
|
session,
|
|
99
166
|
initializing,
|
|
167
|
+
isExpired,
|
|
168
|
+
timeLeft,
|
|
100
169
|
login,
|
|
101
170
|
logout,
|
|
102
|
-
cancel
|
|
171
|
+
cancel,
|
|
172
|
+
setAuthenticated: (user: QidUser, token: string) => {
|
|
173
|
+
setUser(user);
|
|
174
|
+
setToken(token);
|
|
175
|
+
localStorage.setItem(STORAGE_KEY, token);
|
|
176
|
+
setInitializing(false);
|
|
177
|
+
}
|
|
103
178
|
};
|
|
104
179
|
}
|
package/src/index.ts
CHANGED
package/src/modules/auth.ts
CHANGED
|
@@ -17,8 +17,13 @@ export class AuthModule {
|
|
|
17
17
|
const resp = await this.sdk.api.post('/api/initiate-generic');
|
|
18
18
|
const { sessionId, nonce } = resp.data;
|
|
19
19
|
|
|
20
|
-
// Construct QR data
|
|
21
|
-
const qrData =
|
|
20
|
+
// Construct QR data (JSON format for mobile app)
|
|
21
|
+
const qrData = JSON.stringify({
|
|
22
|
+
sessionId,
|
|
23
|
+
nonce,
|
|
24
|
+
action: 'login-handshake',
|
|
25
|
+
timestamp: Date.now()
|
|
26
|
+
});
|
|
22
27
|
|
|
23
28
|
return {
|
|
24
29
|
sessionId,
|
|
@@ -53,6 +58,22 @@ export class AuthModule {
|
|
|
53
58
|
});
|
|
54
59
|
}
|
|
55
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Terminate the session on the server
|
|
63
|
+
*/
|
|
64
|
+
async logout(token: string): Promise<void> {
|
|
65
|
+
if (!token) return;
|
|
66
|
+
try {
|
|
67
|
+
await this.sdk.api.post('/api/logout', {}, {
|
|
68
|
+
headers: { 'Authorization': `Bearer ${token}` }
|
|
69
|
+
});
|
|
70
|
+
} catch (e) {
|
|
71
|
+
console.warn('Logout failed or session already expired');
|
|
72
|
+
} finally {
|
|
73
|
+
this.disconnect();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
56
77
|
/**
|
|
57
78
|
* Stop listening and disconnect socket
|
|
58
79
|
*/
|
|
@@ -72,6 +93,14 @@ export class AuthModule {
|
|
|
72
93
|
'Authorization': `Bearer ${token}`
|
|
73
94
|
}
|
|
74
95
|
});
|
|
75
|
-
|
|
96
|
+
const data = resp.data;
|
|
97
|
+
return {
|
|
98
|
+
userId: data.regUserId,
|
|
99
|
+
regUserId: data.regUserId,
|
|
100
|
+
username: data.username,
|
|
101
|
+
email: data.email,
|
|
102
|
+
role: data.role
|
|
103
|
+
};
|
|
76
104
|
}
|
|
77
105
|
}
|
|
106
|
+
|