@qidcloud/sdk 1.2.1 → 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.
@@ -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 = 'Login with QidCloud'
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
- <div style={{ display: 'flex', flexDirection: 'column', gap: '15px' }}>
147
- <div style={{ height: '4px', width: '60px', backgroundColor: '#00e5ff', margin: '0 auto', borderRadius: '2px', opacity: 0.5 }} />
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
- display: 'block',
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 Handshake
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>
@@ -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
- const logout = useCallback(() => {
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
- sdk.auth.disconnect();
37
- }, [sdk]);
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
- setLoading(true);
61
- setInitializing(false);
62
- try {
63
- const profile = await sdk.auth.getProfile(receivedToken);
64
- setToken(receivedToken);
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
@@ -54,4 +54,5 @@ export class QidCloud {
54
54
  export default QidCloud;
55
55
  export * from './types';
56
56
  export { QidSignInButton } from './components/QidSignInButton';
57
+ export { QidCloudLogin } from './components/QidCloudLogin';
57
58
  export { useQidAuth } from './hooks/useQidAuth';
@@ -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 = `qid: handshake:${sessionId}:${nonce} `;
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
- return resp.data;
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
+