@mehdad67/apitogo 0.1.29 → 0.1.31
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/dist/cli/cli.js +1 -1
- package/dist/declarations/lib/authentication/providers/dev-portal.d.ts +3 -0
- package/dist/declarations/lib/authentication/state.d.ts +1 -0
- package/package.json +1 -1
- package/src/lib/authentication/providers/dev-portal.tsx +57 -5
- package/src/lib/authentication/state.ts +33 -1
- package/src/lib/components/Header.tsx +5 -2
- package/src/lib/components/MobileTopNavigation.tsx +4 -2
package/dist/cli/cli.js
CHANGED
|
@@ -20,6 +20,8 @@ export declare class DevPortalAuthenticationProvider extends CoreAuthenticationP
|
|
|
20
20
|
private readonly redirectToAfterSignOut;
|
|
21
21
|
private authMode;
|
|
22
22
|
private backendAvailable;
|
|
23
|
+
private refreshGeneration;
|
|
24
|
+
private refreshInFlight;
|
|
23
25
|
constructor({ apiBaseUrl, redirectToAfterSignIn, redirectToAfterSignUp, redirectToAfterSignOut, }: DevPortalAuthenticationConfig);
|
|
24
26
|
initialize(_context: ZudokuContext): Promise<void>;
|
|
25
27
|
onPageLoad: () => Promise<void>;
|
|
@@ -27,6 +29,7 @@ export declare class DevPortalAuthenticationProvider extends CoreAuthenticationP
|
|
|
27
29
|
private setPreviewState;
|
|
28
30
|
private ensureLiveBackend;
|
|
29
31
|
refreshUserProfile(): Promise<boolean>;
|
|
32
|
+
private refreshUserProfileInternal;
|
|
30
33
|
signIn(_: AuthActionContext, { redirectTo }?: AuthActionOptions): Promise<void>;
|
|
31
34
|
signUp(_: AuthActionContext, { redirectTo }?: AuthActionOptions): Promise<void>;
|
|
32
35
|
signOut(_: AuthActionContext): Promise<void>;
|
|
@@ -42,6 +42,7 @@ export declare const useAuthState: import("zustand").UseBoundStore<Omit<import("
|
|
|
42
42
|
getOptions: () => Partial<import("zustand/middleware").PersistOptions<AuthState, unknown, unknown>>;
|
|
43
43
|
};
|
|
44
44
|
}>;
|
|
45
|
+
export declare const waitForAuthStateHydration: () => Promise<void>;
|
|
45
46
|
export interface UserProfile {
|
|
46
47
|
sub: string;
|
|
47
48
|
email: string | undefined;
|
package/package.json
CHANGED
|
@@ -7,7 +7,11 @@ import type {
|
|
|
7
7
|
AuthenticationProviderInitializer,
|
|
8
8
|
} from "../authentication.js";
|
|
9
9
|
import { CoreAuthenticationPlugin } from "../AuthenticationPlugin.js";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
type UserProfile,
|
|
12
|
+
useAuthState,
|
|
13
|
+
waitForAuthStateHydration,
|
|
14
|
+
} from "../state.js";
|
|
11
15
|
import type {
|
|
12
16
|
DevPortalAuthMode,
|
|
13
17
|
EndUserMeResponse,
|
|
@@ -43,6 +47,8 @@ export class DevPortalAuthenticationProvider
|
|
|
43
47
|
private readonly redirectToAfterSignOut: string;
|
|
44
48
|
private authMode: DevPortalAuthMode = "preview";
|
|
45
49
|
private backendAvailable = false;
|
|
50
|
+
private refreshGeneration = 0;
|
|
51
|
+
private refreshInFlight: Promise<boolean> | null = null;
|
|
46
52
|
|
|
47
53
|
constructor({
|
|
48
54
|
apiBaseUrl,
|
|
@@ -58,6 +64,12 @@ export class DevPortalAuthenticationProvider
|
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
async initialize(_context: ZudokuContext): Promise<void> {
|
|
67
|
+
// Node SSR cannot reach local HTTPS backends with self-signed certs.
|
|
68
|
+
if (typeof window === "undefined") {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
await waitForAuthStateHydration();
|
|
61
73
|
await this.syncBackendAvailability();
|
|
62
74
|
if (!this.backendAvailable) {
|
|
63
75
|
this.setPreviewState();
|
|
@@ -68,6 +80,12 @@ export class DevPortalAuthenticationProvider
|
|
|
68
80
|
}
|
|
69
81
|
|
|
70
82
|
onPageLoad = async () => {
|
|
83
|
+
if (typeof window === "undefined") {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await waitForAuthStateHydration();
|
|
88
|
+
await this.syncBackendAvailability();
|
|
71
89
|
if (!this.backendAvailable) {
|
|
72
90
|
this.setPreviewState();
|
|
73
91
|
return;
|
|
@@ -113,6 +131,18 @@ export class DevPortalAuthenticationProvider
|
|
|
113
131
|
}
|
|
114
132
|
|
|
115
133
|
async refreshUserProfile(): Promise<boolean> {
|
|
134
|
+
if (this.refreshInFlight) {
|
|
135
|
+
return this.refreshInFlight;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
this.refreshInFlight = this.refreshUserProfileInternal().finally(() => {
|
|
139
|
+
this.refreshInFlight = null;
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return this.refreshInFlight;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private async refreshUserProfileInternal(): Promise<boolean> {
|
|
116
146
|
if (!this.apiBaseUrl) {
|
|
117
147
|
this.setPreviewState();
|
|
118
148
|
return false;
|
|
@@ -123,20 +153,34 @@ export class DevPortalAuthenticationProvider
|
|
|
123
153
|
return false;
|
|
124
154
|
}
|
|
125
155
|
|
|
156
|
+
await waitForAuthStateHydration();
|
|
126
157
|
await this.syncBackendAvailability();
|
|
127
158
|
if (!this.backendAvailable) {
|
|
128
159
|
this.setPreviewState();
|
|
129
160
|
return false;
|
|
130
161
|
}
|
|
131
162
|
|
|
163
|
+
const generation = ++this.refreshGeneration;
|
|
164
|
+
useAuthState.setState({ isPending: true });
|
|
165
|
+
|
|
132
166
|
try {
|
|
133
167
|
const response = await fetch(`${this.apiBaseUrl}/api/v1/auth/me`, {
|
|
134
168
|
method: "GET",
|
|
135
169
|
headers: { Accept: "application/json" },
|
|
136
170
|
credentials: "include",
|
|
171
|
+
redirect: "manual",
|
|
137
172
|
});
|
|
138
173
|
|
|
139
|
-
if (
|
|
174
|
+
if (generation !== this.refreshGeneration) {
|
|
175
|
+
return useAuthState.getState().isAuthenticated;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Backends may still respond with 302 to the IdP; treat as unauthenticated.
|
|
179
|
+
if (
|
|
180
|
+
response.type === "opaqueredirect" ||
|
|
181
|
+
response.status === 302 ||
|
|
182
|
+
response.status === 401
|
|
183
|
+
) {
|
|
140
184
|
useAuthState.setState({
|
|
141
185
|
isAuthenticated: false,
|
|
142
186
|
isPending: false,
|
|
@@ -158,22 +202,30 @@ export class DevPortalAuthenticationProvider
|
|
|
158
202
|
}
|
|
159
203
|
|
|
160
204
|
const me = (await response.json()) as EndUserMeResponse;
|
|
205
|
+
if (generation !== this.refreshGeneration) {
|
|
206
|
+
return useAuthState.getState().isAuthenticated;
|
|
207
|
+
}
|
|
208
|
+
|
|
161
209
|
const profile: UserProfile = mapEndUserMeToProfile(me);
|
|
162
210
|
|
|
163
|
-
useAuthState.
|
|
211
|
+
useAuthState.setState({
|
|
212
|
+
isAuthenticated: true,
|
|
213
|
+
isPending: false,
|
|
164
214
|
profile,
|
|
165
215
|
providerData: {
|
|
166
216
|
type: "dev-portal",
|
|
167
217
|
apiBaseUrl: this.apiBaseUrl,
|
|
168
218
|
authMode: "live",
|
|
169
219
|
},
|
|
170
|
-
});
|
|
171
|
-
useAuthState.setState({
|
|
172
220
|
isBackendAvailable: true,
|
|
173
221
|
authMode: "live",
|
|
174
222
|
});
|
|
175
223
|
return true;
|
|
176
224
|
} catch {
|
|
225
|
+
if (generation !== this.refreshGeneration) {
|
|
226
|
+
return useAuthState.getState().isAuthenticated;
|
|
227
|
+
}
|
|
228
|
+
|
|
177
229
|
this.setPreviewState();
|
|
178
230
|
return false;
|
|
179
231
|
}
|
|
@@ -77,12 +77,25 @@ export const authState = create<AuthState>()(
|
|
|
77
77
|
}),
|
|
78
78
|
{
|
|
79
79
|
merge: (persistedState, currentState) => {
|
|
80
|
+
const persisted =
|
|
81
|
+
typeof persistedState === "object" && persistedState !== null
|
|
82
|
+
? (persistedState as Partial<AuthState>)
|
|
83
|
+
: {};
|
|
84
|
+
|
|
85
|
+
// Cookie/session auth is refreshed from the backend on load. Do not
|
|
86
|
+
// restore identity fields from localStorage or OIDC return looks logged out.
|
|
80
87
|
return {
|
|
81
88
|
...currentState,
|
|
82
89
|
isPending: false,
|
|
83
|
-
|
|
90
|
+
isBackendAvailable:
|
|
91
|
+
persisted.isBackendAvailable ?? currentState.isBackendAvailable,
|
|
92
|
+
authMode: persisted.authMode ?? currentState.authMode,
|
|
84
93
|
};
|
|
85
94
|
},
|
|
95
|
+
partialize: (state) => ({
|
|
96
|
+
isBackendAvailable: state.isBackendAvailable,
|
|
97
|
+
authMode: state.authMode,
|
|
98
|
+
}),
|
|
86
99
|
name: "auth-state",
|
|
87
100
|
storage: createJSONStorage(() => localStorage),
|
|
88
101
|
},
|
|
@@ -93,6 +106,25 @@ syncZustandState(authState);
|
|
|
93
106
|
|
|
94
107
|
export const useAuthState = authState;
|
|
95
108
|
|
|
109
|
+
/** Wait until persisted auth metadata has rehydrated from localStorage. */
|
|
110
|
+
export const waitForAuthStateHydration = (): Promise<void> => {
|
|
111
|
+
if (typeof window === "undefined") {
|
|
112
|
+
return Promise.resolve();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const { persist } = useAuthState;
|
|
116
|
+
if (persist.hasHydrated()) {
|
|
117
|
+
return Promise.resolve();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return new Promise((resolve) => {
|
|
121
|
+
const unsubscribe = persist.onFinishHydration(() => {
|
|
122
|
+
unsubscribe();
|
|
123
|
+
resolve();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
|
|
96
128
|
export interface UserProfile {
|
|
97
129
|
sub: string;
|
|
98
130
|
email: string | undefined;
|
|
@@ -73,13 +73,16 @@ const ProfileMenu = () => {
|
|
|
73
73
|
const context = useZudoku();
|
|
74
74
|
const profileItems = context.getProfileMenuItems();
|
|
75
75
|
const auth = useAuth();
|
|
76
|
-
const { isAuthEnabled, isAuthenticated, profile, isBackendAvailable } =
|
|
76
|
+
const { isAuthEnabled, isAuthenticated, isPending, profile, isBackendAvailable } =
|
|
77
|
+
auth;
|
|
77
78
|
|
|
78
79
|
if (!isAuthEnabled || !isBackendAvailable) return null;
|
|
79
80
|
|
|
80
81
|
return (
|
|
81
82
|
<ClientOnly fallback={<Skeleton className="rounded-sm h-5 w-24 mr-4" />}>
|
|
82
|
-
{
|
|
83
|
+
{isPending ? (
|
|
84
|
+
<Skeleton className="rounded-sm h-9 w-24" />
|
|
85
|
+
) : !isAuthenticated ? (
|
|
83
86
|
<Button size="lg" variant="ghost" onClick={() => auth.login()}>
|
|
84
87
|
Login
|
|
85
88
|
</Button>
|
|
@@ -130,7 +130,7 @@ export const MobileTopNavigation = () => {
|
|
|
130
130
|
getProfileMenuItems,
|
|
131
131
|
} = context;
|
|
132
132
|
const headerNavigation = header?.navigation ?? [];
|
|
133
|
-
const { isAuthenticated, profile, isAuthEnabled, isBackendAvailable } =
|
|
133
|
+
const { isAuthenticated, profile, isAuthEnabled, isBackendAvailable, isPending } =
|
|
134
134
|
authState;
|
|
135
135
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
136
136
|
|
|
@@ -237,7 +237,9 @@ export const MobileTopNavigation = () => {
|
|
|
237
237
|
<ClientOnly
|
|
238
238
|
fallback={<Skeleton className="rounded-sm h-8 w-16" />}
|
|
239
239
|
>
|
|
240
|
-
{
|
|
240
|
+
{isPending ? (
|
|
241
|
+
<Skeleton className="rounded-sm h-8 w-16" />
|
|
242
|
+
) : isAuthenticated ? (
|
|
241
243
|
<Button asChild variant="outline">
|
|
242
244
|
<Link
|
|
243
245
|
to="/signout"
|