Skip to content

Commit de948c4

Browse files
committed
chore(samples): add DebugPage and TokenInfo component for session data and token debugging
1 parent 5718048 commit de948c4

File tree

6 files changed

+320
-42
lines changed

6 files changed

+320
-42
lines changed

pnpm-lock.yaml

Lines changed: 36 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

samples/teamspace-react/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414
"@asgardeo/react-router": "workspace:^",
1515
"@radix-ui/react-label": "^2.1.7",
1616
"@radix-ui/react-slot": "^1.2.3",
17+
"@types/dompurify": "^3.2.0",
1718
"class-variance-authority": "^0.7.1",
1819
"clsx": "^2.1.1",
20+
"dompurify": "^3.2.7",
1921
"lucide-react": "^0.294.0",
2022
"react": "^19.1.0",
2123
"react-dom": "^19.1.0",

samples/teamspace-react/src/App.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import OrganizationsPage from './pages/Organizations';
88
import CreateOrganizationPage from './pages/CreateOrganizationPage';
99
import SignInPage from './pages/SignInPage';
1010
import LandingPage from './pages/LandingPage';
11+
import DebugPage from './pages/DebugPage';
1112
import LandingLayout from './layouts/LandingLayout';
1213
import DashboardLayout from './layouts/DashboardLayout';
1314
import AuthenticatedLayout from './layouts/AuthenticatedLayout';
@@ -167,6 +168,16 @@ function App() {
167168
</ProtectedRoute>
168169
}
169170
/>
171+
<Route
172+
path="/debug"
173+
element={
174+
<ProtectedRoute redirectTo="/signin">
175+
<DashboardLayout>
176+
<DebugPage />
177+
</DashboardLayout>
178+
</ProtectedRoute>
179+
}
180+
/>
170181
</Routes>
171182
</Router>
172183
</AppContext.Provider>
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import React, {useState, useEffect} from 'react';
2+
import {X} from 'lucide-react';
3+
import DOMPurify from 'dompurify';
4+
5+
interface TokenInfoProps {
6+
isOpen: boolean;
7+
onClose: () => void;
8+
isPage?: boolean;
9+
}
10+
11+
const TokenInfo: React.FC<TokenInfoProps> = ({isOpen, onClose, isPage = false}) => {
12+
const [sessionData, setSessionData] = useState<any>(null);
13+
const [decodedToken, setDecodedToken] = useState<any>(null);
14+
15+
useEffect(() => {
16+
if (!isOpen) return;
17+
18+
const findSessionData = () => {
19+
// Find session storage key that matches the pattern
20+
const sessionKey = Object.keys(sessionStorage).find(key => key.startsWith('session_data-instance_'));
21+
22+
if (sessionKey) {
23+
const data = sessionStorage.getItem(sessionKey);
24+
if (data) {
25+
try {
26+
const parsedData = JSON.parse(data);
27+
setSessionData(parsedData);
28+
29+
// Decode the ID token if present
30+
if (parsedData.id_token) {
31+
const decoded = decodeJWT(parsedData.id_token);
32+
setDecodedToken(decoded);
33+
}
34+
} catch (error) {
35+
console.error('Error parsing session data:', error);
36+
}
37+
}
38+
}
39+
};
40+
41+
findSessionData();
42+
43+
// Set up an interval to check for changes (optional)
44+
const interval = setInterval(findSessionData, 5000);
45+
46+
return () => clearInterval(interval);
47+
}, [isOpen]);
48+
49+
const decodeJWT = (token: string) => {
50+
try {
51+
const base64Url = token.split('.')[1];
52+
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
53+
const jsonPayload = decodeURIComponent(
54+
atob(base64)
55+
.split('')
56+
.map(function (c) {
57+
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
58+
})
59+
.join(''),
60+
);
61+
62+
return JSON.parse(jsonPayload);
63+
} catch (error) {
64+
console.error('Error decoding JWT:', error);
65+
return null;
66+
}
67+
};
68+
69+
const formatDate = (timestamp: number) => {
70+
return new Date(timestamp * 1000).toLocaleString();
71+
};
72+
73+
const highlightJSON = (obj: any) => {
74+
const jsonString = JSON.stringify(obj, null, 2);
75+
const lines = jsonString.split('\n');
76+
77+
const highlightedHTML = lines
78+
.map(line => {
79+
// Escape HTML first
80+
let escapedLine = line.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
81+
82+
// Apply syntax highlighting
83+
escapedLine = escapedLine
84+
// Property names (keys)
85+
.replace(/(\s*)"([^"]+)"(\s*:)/g, '$1<span class="text-blue-600 font-medium">"$2"</span>$3')
86+
// String values
87+
.replace(/:\s*"([^"]*)"/g, ': <span class="text-green-600">"$1"</span>')
88+
// Numbers
89+
.replace(/:\s*(-?\d+(?:\.\d+)?)/g, ': <span class="text-purple-600">$1</span>')
90+
// Booleans
91+
.replace(/:\s*(true|false)/g, ': <span class="text-orange-600 font-semibold">$1</span>')
92+
// Null
93+
.replace(/:\s*(null)/g, ': <span class="text-red-600 font-semibold">$1</span>')
94+
// Punctuation
95+
.replace(/([{}[\],])/g, '<span class="text-gray-600">$1</span>');
96+
97+
return escapedLine;
98+
})
99+
.join('\n');
100+
101+
// Sanitize the HTML with DOMPurify
102+
return DOMPurify.sanitize(highlightedHTML, {
103+
ALLOWED_TAGS: ['span'],
104+
ALLOWED_ATTR: ['class'],
105+
KEEP_CONTENT: true,
106+
});
107+
};
108+
109+
const renderSessionData = () => {
110+
if (!sessionData) return <p className="text-gray-500">No session data found</p>;
111+
112+
// Format the session data with readable timestamps
113+
const formattedData = {...sessionData};
114+
if (formattedData.created_at) {
115+
formattedData.created_at_readable = formatDate(formattedData.created_at / 1000);
116+
}
117+
118+
const highlightedJSON = highlightJSON(formattedData);
119+
120+
return (
121+
<div className="overflow-auto">
122+
<pre
123+
className="text-sm font-mono whitespace-pre-wrap break-words"
124+
dangerouslySetInnerHTML={{__html: highlightedJSON}}
125+
/>
126+
</div>
127+
);
128+
};
129+
130+
const renderDecodedToken = () => {
131+
if (!decodedToken) return <p className="text-gray-500">No decoded token available</p>;
132+
133+
// Format the decoded token with readable timestamps
134+
const formattedToken = {...decodedToken};
135+
['exp', 'iat', 'nbf'].forEach(key => {
136+
if (formattedToken[key]) {
137+
formattedToken[`${key}_readable`] = formatDate(formattedToken[key]);
138+
}
139+
});
140+
141+
const highlightedJSON = highlightJSON(formattedToken);
142+
143+
return (
144+
<div className="overflow-auto">
145+
<pre
146+
className="text-sm font-mono whitespace-pre-wrap break-words"
147+
dangerouslySetInnerHTML={{__html: highlightedJSON}}
148+
/>
149+
</div>
150+
);
151+
};
152+
153+
if (!isOpen) return null;
154+
155+
const content = (
156+
<>
157+
{!isPage && (
158+
<div className="flex items-center justify-between p-6 border-b border-gray-200">
159+
<h1 className="text-2xl font-bold text-gray-900">Token Information</h1>
160+
<button onClick={onClose} className="p-2 hover:bg-gray-100 rounded-lg transition-colors">
161+
<X className="h-5 w-5 text-gray-500" />
162+
</button>
163+
</div>
164+
)}
165+
166+
<div className={isPage ? 'p-0' : 'p-6 overflow-auto max-h-[calc(90vh-120px)]'}>
167+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
168+
<div className="bg-gray-50 rounded-lg p-4">
169+
<h2 className="text-lg font-semibold text-gray-800 mb-4">Session Data</h2>
170+
{renderSessionData()}
171+
</div>
172+
173+
<div className="bg-gray-50 rounded-lg p-4">
174+
<h2 className="text-lg font-semibold text-gray-800 mb-4">Decoded ID Token</h2>
175+
{renderDecodedToken()}
176+
</div>
177+
</div>
178+
</div>
179+
</>
180+
);
181+
182+
if (isPage) {
183+
return content;
184+
}
185+
186+
return (
187+
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
188+
<div className="bg-white rounded-lg shadow-xl max-w-6xl w-full max-h-[90vh] overflow-hidden">{content}</div>
189+
</div>
190+
);
191+
};
192+
193+
export default TokenInfo;

0 commit comments

Comments
 (0)