forked from Txio-labs/txio
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathRedirectManager.tsx
More file actions
218 lines (189 loc) · 7.52 KB
/
Copy pathRedirectManager.tsx
File metadata and controls
218 lines (189 loc) · 7.52 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
'use client';
import { useEffect, useState } from 'react';
import { useRouter, usePathname } from 'next/navigation';
import { useAppStore, appStore } from '@/lib/store';
import { apiService } from '@/services/api';
import { FeatureId } from '@/types';
// Read and clear the short-lived OAuth token the backend hands off. The
// intended handoff is a URL fragment (`#token=...`): a cookie can't be used
// here since the backend and frontend are different sites (different
// eTLD+1), so a cookie the backend's redirect response sets is scoped to
// the backend's own origin and never visible to document.cookie here — and
// a fragment, unlike a query param, is never sent to any server (this one
// included) and is stripped from Referer headers.
//
// The `?token=...` query-param branch below is a compatibility fallback:
// older/currently-deployed backend builds hand the token off via the query
// string instead of the fragment. It's read and stripped immediately either
// way, but a query param does still reach this page's own server access
// logs and Referer headers on the way in, so this path should be treated as
// temporary — retire it once the backend is confirmed redeployed with the
// fragment-based handoff.
function consumeOAuthToken(): string | null {
if (typeof window === 'undefined') return null;
const hash = window.location.hash;
const hashMatch = hash.match(/(?:^#|&)token=([^&]+)/);
if (hashMatch) {
const remainingHash = hash.replace(/(?:^#|&)token=[^&]+/, '').replace(/^#&/, '#');
const url = new URL(window.location.href);
url.hash = remainingHash === '#' ? '' : remainingHash;
window.history.replaceState({}, '', url.toString());
return decodeURIComponent(hashMatch[1]);
}
const url = new URL(window.location.href);
const queryToken = url.searchParams.get('token');
if (queryToken) {
url.searchParams.delete('token');
window.history.replaceState({}, '', url.toString());
return queryToken;
}
return null;
}
const workspaceViewModeToTab: Partial<
Record<string, FeatureId>
> = {
docs: 'docs',
ecosystem: 'ecosystem',
features: 'features',
integrations: 'integrations',
infrastructure: 'infrastructure',
partners: 'partners'
};
const workspacePathToTab: Partial<
Record<string, FeatureId>
> = {
'/docs': 'docs',
'/ecosystem': 'ecosystem',
'/features': 'features',
'/integrations': 'integrations',
'/infrastructure': 'infrastructure',
'/partners': 'partners'
};
export function RedirectManager() {
const { viewMode, user } = useAppStore();
const router = useRouter();
const pathname = usePathname();
const [initialized, setInitialized] = useState(false);
// Restore session state BEFORE any redirect logic runs
useEffect(() => {
appStore.initialize().then(() => setInitialized(true));
}, []);
// Sync viewMode from pathname when pathname changes (public routes & workspace)
useEffect(() => {
if (!initialized) return;
const pathToMode: Record<string, string> = {
'/': 'landing',
'/docs': 'docs',
'/ecosystem': 'ecosystem',
'/signin': 'signin',
'/signup': 'signup',
'/features': 'features',
'/otp': 'otp',
'/integrations': 'integrations',
'/infrastructure': 'infrastructure',
'/partners': 'partners',
'/workspace': 'app'
};
const expectedMode = pathToMode[pathname];
if (expectedMode && appStore.getSnapshot().viewMode !== expectedMode) {
appStore.setViewMode(expectedMode as any);
}
// Only react to real URL navigation (pathname), not to viewMode
// changes this effect itself may have caused — otherwise this races
// with the viewMode -> URL effect below and the two fight forever.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pathname, initialized]);
// Clean up URL if there are any query params left over
useEffect(() => {
const token = consumeOAuthToken();
if (!token) return;
// OAuth callback: treat this as the source of truth and
// prevent generic viewMode redirects from taking over.
apiService.setToken(token);
void appStore.initialize();
try {
const payloadSegment = token
.split('.')[1]
?.replace(/-/g, '+')
.replace(/_/g, '/');
const normalizedPayload =
(payloadSegment || '').padEnd(
Math.ceil(
(payloadSegment || '')
.length / 4
) * 4,
'='
);
const payload = JSON.parse(
atob(normalizedPayload)
);
appStore.updateUser({
id: payload.sub,
email: payload.email,
name: payload.email.split('@')[0]
});
appStore.setViewMode('app');
appStore.showToast('Authentication successful!', 'success');
const url = new URL(window.location.href);
url.search = '';
window.history.replaceState({}, '', url.toString());
} catch (e) {
console.error('Failed to parse token', e);
}
}, [initialized]);
// Only redirect after initialization is complete.
useEffect(() => {
if (!initialized) return;
// Read live state rather than the viewMode/user captured in this
// effect's closure: the pathname -> viewMode sync effect above runs
// first within the same commit and may have already corrected
// appStore's viewMode to match pathname. Acting on the stale
// pre-correction closure value here would immediately redirect the
// user away from the page they're actually on, then the pathname
// change bounces back and the two effects fight forever.
const snapshot = appStore.getSnapshot();
const liveViewMode = snapshot.viewMode;
const liveUser = snapshot.user;
// If authenticated, always stay on workspace.
if (liveUser) {
const workspaceTab =
workspacePathToTab[pathname] ||
workspaceViewModeToTab[
liveViewMode
];
if (workspaceTab) {
appStore.openTab(workspaceTab);
if (liveViewMode !== 'app') {
appStore.setViewMode('app');
}
}
const targetPath = '/workspace';
if (pathname !== targetPath) {
router.replace(targetPath);
}
return;
}
// If not authenticated and trying to access workspace, redirect to landing
if (pathname === '/workspace') {
router.replace('/');
return;
}
const modeToPath: Record<string, string> = {
landing: '/',
docs: '/docs',
ecosystem: '/ecosystem',
signin: '/signin',
signup: '/signup',
features: '/features',
otp: '/otp',
integrations: '/integrations',
infrastructure: '/infrastructure',
partners: '/partners'
};
const targetPath = modeToPath[liveViewMode];
if (targetPath && pathname !== targetPath) {
router.replace(targetPath);
}
}, [viewMode, user, router, pathname, initialized]);
return null;
}