Skip to content

Commit 344e1f9

Browse files
author
Darcy Ye
committed
chore: resume least changes
1 parent 1974dac commit 344e1f9

9 files changed

Lines changed: 526 additions & 133 deletions

File tree

src/theme/Layout/auth-status.ts

Lines changed: 229 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,234 @@
1-
import type { SiteConfig } from './types';
2-
3-
export function createAuthStatusChecker(
4-
siteConfig: SiteConfig,
5-
onAuthStatus: (status: boolean) => void
6-
): () => void {
7-
const logtoAdminConsoleUrl = siteConfig.customFields?.logtoAdminConsoleUrl;
8-
9-
if (typeof logtoAdminConsoleUrl !== 'string') {
10-
console.warn('logtoAdminConsoleUrl not configured');
11-
return () => {
12-
// Noop
13-
};
14-
}
15-
16-
// Create iframe for auth status check
17-
const iframe = document.createElement('iframe');
18-
// Set iframe properties using setAttribute and style properties
19-
iframe.setAttribute(
20-
'src',
21-
`${logtoAdminConsoleUrl}/auth-status${siteConfig.customFields?.isDebuggingEnabled ? '?debug=true' : ''}`
22-
);
23-
iframe.setAttribute(
24-
'style',
25-
`
26-
display: ${siteConfig.customFields?.isIframeVisible ? 'block' : 'none'};
27-
width: 1px;
28-
height: 1px;
29-
position: absolute;
30-
top: -9999px;
31-
left: -9999px;
32-
`
33-
.replaceAll(/\s+/g, ' ')
34-
.trim()
35-
);
36-
37-
// Listen for auth status response
38-
const handleMessage = (event: MessageEvent) => {
39-
if (event.origin !== new URL(logtoAdminConsoleUrl).origin) {
40-
return;
41-
}
42-
43-
if (event.data?.type === 'auth-status') {
44-
onAuthStatus(Boolean(event.data.isAuthenticated));
45-
if (siteConfig.customFields?.isDebuggingEnabled) {
46-
console.log('Auth status received:', event.data.isAuthenticated);
1+
/* eslint-disable @silverhand/fp/no-mutation */
2+
import {
3+
iframeLoadDelay,
4+
requestTimeout,
5+
debugIframeDelay,
6+
debugIframeTimeoutDelay,
7+
} from './constants';
8+
import type { DebugLogger } from './debug-logger';
9+
import { AuthMessageType, type AuthStatusRequest, type AuthStatusResponse } from './types';
10+
11+
export type AuthStatusCheckerOptions = {
12+
logtoAdminConsoleUrl?: string;
13+
enableAuthStatusCheck?: boolean;
14+
isDebugMode: boolean;
15+
isIframeVisible: boolean;
16+
debugLogger: DebugLogger;
17+
};
18+
19+
export type AuthStatusResult = {
20+
authStatus?: boolean;
21+
authCheckError?: string;
22+
};
23+
24+
export function createAuthStatusChecker({
25+
logtoAdminConsoleUrl,
26+
enableAuthStatusCheck,
27+
isDebugMode,
28+
isIframeVisible,
29+
debugLogger,
30+
}: AuthStatusCheckerOptions) {
31+
const iframeSrc =
32+
typeof logtoAdminConsoleUrl === 'string'
33+
? `${logtoAdminConsoleUrl}/auth-status${isDebugMode ? '?debug=true' : ''}`
34+
: undefined;
35+
36+
const authStatusCheckerHost =
37+
typeof logtoAdminConsoleUrl === 'string' ? new URL(logtoAdminConsoleUrl).origin : undefined;
38+
39+
/**
40+
* Function to check admin token status via cross-domain iframe communication
41+
*
42+
* This function creates a hidden iframe, sends a message to check the admin token,
43+
* and returns a promise that resolves with the token status.
44+
*
45+
* @returns Promise that resolves to true if user has admin token, false otherwise
46+
* @throws Error if auth status checker is not configured or request fails
47+
*/
48+
const checkAdminTokenStatus = async (): Promise<boolean> => {
49+
return new Promise((resolve, reject) => {
50+
if (!logtoAdminConsoleUrl || !enableAuthStatusCheck || !iframeSrc) {
51+
reject(new Error('Auth status checker not configured'));
52+
return;
4753
}
48-
}
49-
};
5054

51-
window.addEventListener('message', handleMessage);
52-
document.body.append(iframe);
55+
const iframe = document.createElement('iframe');
56+
iframe.src = iframeSrc;
57+
58+
if (isIframeVisible) {
59+
// Temporarily show iframe for debugging
60+
iframe.style.position = 'fixed';
61+
iframe.style.top = '10px';
62+
iframe.style.right = '10px';
63+
iframe.style.width = '1000px';
64+
iframe.style.height = '1000px';
65+
iframe.style.border = '2px solid red';
66+
iframe.style.zIndex = '9999';
67+
iframe.style.backgroundColor = 'white';
68+
} else {
69+
iframe.style.display = 'none';
70+
}
71+
72+
document.body.append(iframe);
73+
74+
const requestId = Math.random().toString(36).slice(7);
75+
// eslint-disable-next-line @silverhand/fp/no-let, prefer-const
76+
let timeoutId: NodeJS.Timeout;
77+
// eslint-disable-next-line @silverhand/fp/no-let
78+
let messageHandlerAdded = false;
79+
80+
const handleMessage = (event: MessageEvent<AuthStatusResponse>) => {
81+
debugLogger.log('handleMessage received:', {
82+
origin: event.origin,
83+
expectedOrigin: authStatusCheckerHost,
84+
data: event.data,
85+
requestId,
86+
dataType: typeof event.data,
87+
dataKeys: Object.keys(event.data),
88+
});
89+
90+
// Validate origin for security
91+
if (event.origin !== authStatusCheckerHost) {
92+
debugLogger.warn('Origin mismatch:', event.origin, 'vs', authStatusCheckerHost);
93+
return;
94+
}
95+
96+
const { data } = event;
97+
98+
// Validate data structure
99+
if (typeof data !== 'object') {
100+
debugLogger.warn('Invalid message data structure:', data);
101+
return;
102+
}
103+
104+
if (data.requestId !== requestId) {
105+
debugLogger.log(
106+
'Request ID mismatch, ignoring message:',
107+
data.requestId,
108+
'vs',
109+
requestId
110+
);
111+
return;
112+
}
113+
114+
debugLogger.log('Processing valid response for request:', requestId);
115+
116+
clearTimeout(timeoutId);
117+
if (messageHandlerAdded) {
118+
window.removeEventListener('message', handleMessage);
119+
messageHandlerAdded = false;
120+
}
121+
122+
// In debug mode (when iframe is visible), don't remove iframe immediately
123+
const isIframeVisible = iframe.style.display !== 'none';
124+
if (isIframeVisible) {
125+
// In debug mode, delay removal to allow inspection
126+
iframe.style.border = '2px solid green'; // Change border color to indicate success
127+
setTimeout(() => {
128+
if (document.body.contains(iframe)) {
129+
iframe.remove();
130+
}
131+
}, debugIframeDelay);
132+
} else if (document.body.contains(iframe)) {
133+
iframe.remove();
134+
}
135+
136+
switch (data.type) {
137+
case AuthMessageType.AdminTokenStatus: {
138+
debugLogger.log('Received admin token status (data):', JSON.stringify(data, null, 2));
139+
resolve(data.isAuthenticated ?? false);
140+
break;
141+
}
142+
case AuthMessageType.AdminTokenError: {
143+
console.error('Received auth error:', data.error);
144+
reject(new Error(data.error || 'Unknown auth error'));
145+
break;
146+
}
147+
}
148+
};
149+
150+
// Add message listener
151+
window.addEventListener('message', handleMessage);
152+
messageHandlerAdded = true;
153+
154+
iframe.addEventListener('load', () => {
155+
debugLogger.log('iframe loaded successfully, preparing to send message');
156+
debugLogger.log('iframe details:', {
157+
src: iframe.src,
158+
contentWindow: Boolean(iframe.contentWindow),
159+
readyState: iframe.contentDocument?.readyState,
160+
});
161+
162+
// Add a delay to ensure iframe content is fully ready and message listeners are set up
163+
setTimeout(() => {
164+
try {
165+
const message: AuthStatusRequest = {
166+
type: AuthMessageType.CheckAdminToken,
167+
requestId,
168+
};
169+
170+
debugLogger.log('Sending message to iframe:', {
171+
message,
172+
targetOrigin: authStatusCheckerHost,
173+
iframeContentWindow: Boolean(iframe.contentWindow),
174+
});
175+
176+
iframe.contentWindow?.postMessage(message, authStatusCheckerHost ?? '');
177+
} catch (error) {
178+
clearTimeout(timeoutId);
179+
if (messageHandlerAdded) {
180+
window.removeEventListener('message', handleMessage);
181+
messageHandlerAdded = false;
182+
}
183+
if (document.body.contains(iframe)) {
184+
iframe.remove();
185+
}
186+
reject(
187+
new Error(
188+
`Failed to send message to iframe: ${error instanceof Error ? error.message : 'Unknown error'}`
189+
)
190+
);
191+
}
192+
}, iframeLoadDelay);
193+
});
194+
195+
// eslint-disable-next-line unicorn/prefer-add-event-listener
196+
iframe.onerror = () => {
197+
clearTimeout(timeoutId);
198+
window.removeEventListener('message', handleMessage);
199+
if (document.body.contains(iframe)) {
200+
iframe.remove();
201+
}
202+
reject(new Error('iframe failed to load'));
203+
};
204+
205+
// Set timeout for the request
206+
timeoutId = setTimeout(() => {
207+
window.removeEventListener('message', handleMessage);
208+
209+
// In debug mode, don't remove iframe immediately on timeout
210+
const isIframeVisible = iframe.style.display !== 'none';
211+
if (isIframeVisible) {
212+
// In debug mode, keep iframe visible for inspection
213+
iframe.style.border = '2px solid orange'; // Change border color to indicate timeout
214+
setTimeout(() => {
215+
if (document.body.contains(iframe)) {
216+
iframe.remove();
217+
}
218+
}, debugIframeTimeoutDelay);
219+
} else {
220+
iframe.remove();
221+
}
222+
223+
reject(new Error('Request timeout'));
224+
}, requestTimeout);
225+
});
226+
};
53227

54-
// Return cleanup function
55-
return () => {
56-
window.removeEventListener('message', handleMessage);
57-
if (iframe.parentNode) {
58-
iframe.remove();
59-
}
228+
return {
229+
checkAdminTokenStatus,
230+
authStatusCheckerHost,
231+
iframeSrc,
60232
};
61233
}
234+
/* eslint-enable @silverhand/fp/no-mutation */

src/theme/Layout/config-fetcher.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
1-
import type { Optional } from '@silverhand/essentials';
2-
31
import { cacheKey, cacheExpiryKey, cacheExpiryTime } from './constants';
4-
import { googleOneTapConfigSchema, type GoogleOneTapConfig } from './types';
5-
6-
export async function fetchGoogleOneTapConfig(
7-
apiBaseUrl: string
8-
): Promise<Optional<GoogleOneTapConfig>> {
2+
import type { DebugLogger } from './debug-logger';
3+
import { googleOneTapConfigSchema, type GoogleOneTapConfig } from './google-one-tap';
4+
5+
export type ConfigFetcherOptions = {
6+
apiBaseUrl: string;
7+
debugLogger: DebugLogger;
8+
};
9+
10+
export async function fetchGoogleOneTapConfig({
11+
apiBaseUrl,
12+
debugLogger,
13+
}: ConfigFetcherOptions): Promise<GoogleOneTapConfig | undefined> {
914
try {
1015
const cachedConfig = localStorage.getItem(cacheKey);
1116
const cachedExpiry = localStorage.getItem(cacheExpiryKey);
1217

13-
if (cachedConfig && cachedExpiry && Number.parseInt(cachedExpiry, 10) > Date.now()) {
18+
if (cachedConfig && cachedExpiry && Number(cachedExpiry) > Date.now()) {
1419
try {
1520
const parsedConfig = googleOneTapConfigSchema.parse(JSON.parse(cachedConfig));
1621
return parsedConfig;
1722
} catch (parseError) {
18-
console.error('Cached config validation failed:', parseError);
23+
debugLogger.error('Cached config validation failed:', parseError);
1924
}
2025
}
2126

@@ -37,7 +42,7 @@ export async function fetchGoogleOneTapConfig(
3742

3843
return validatedConfig;
3944
} catch (error) {
40-
console.error('Error fetching or validating Google One Tap config:', error);
45+
debugLogger.error('Error fetching or validating Google One Tap config:', error);
4146
return undefined;
4247
}
4348
}

src/theme/Layout/constants.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
// Cache keys for Google One Tap config
21
export const cacheKey = '_logto_google_one_tap_config';
32
export const cacheExpiryKey = '_logto_google_one_tap_config_expiry';
43
export const cacheExpiryTime = 1 * 60 * 60 * 1000; // 1 hour
54

6-
// Default API base URLs
75
export const defaultApiBaseProdUrl = 'https://auth.logto.io';
86
export const defaultApiBaseDevUrl = 'https://auth.logto.dev';
97

10-
// Auth status check settings
11-
export const authCheckDelay = 1000; // 1 second delay to not block initial render
8+
export const authStatusPollInterval = 30_000; // 30 seconds
9+
export const iframeLoadDelay = 1000; // Reduced from 5000 to 1000 to not block render
10+
export const requestTimeout = 10_000; // 10 seconds
11+
export const debugIframeDelay = 5000; // 5 seconds for success
12+
export const debugIframeTimeoutDelay = 10_000; // 10 seconds for timeout

src/theme/Layout/debug-logger.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
export type DebugLogger = {
2+
log: (...args: unknown[]) => void;
3+
warn: (...args: unknown[]) => void;
4+
error: (...args: unknown[]) => void;
5+
};
6+
7+
export function createDebugLogger(isDebugMode: boolean): DebugLogger {
8+
return {
9+
log: (...args: unknown[]) => {
10+
if (isDebugMode) {
11+
console.log(...args);
12+
}
13+
},
14+
warn: (...args: unknown[]) => {
15+
if (isDebugMode) {
16+
console.warn(...args);
17+
}
18+
},
19+
error: (...args: unknown[]) => {
20+
if (isDebugMode) {
21+
console.error(...args);
22+
}
23+
},
24+
};
25+
}

src/theme/Layout/global.d.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
declare global {
2+
interface Window {
3+
__logtoAuthStatus?: {
4+
authStatus?: boolean;
5+
authCheckError?: string;
6+
checkAdminTokenStatus: () => Promise<boolean>;
7+
};
8+
}
9+
}
10+
11+
export {};

0 commit comments

Comments
 (0)