@mehdad67/apitogo 0.1.30 → 0.1.32
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/hook.d.ts +3 -21
- package/dist/declarations/lib/authentication/providers/dev-portal.d.ts +3 -0
- package/dist/declarations/lib/authentication/state.d.ts +9 -26
- package/package.json +1 -1
- package/src/lib/authentication/providers/dev-portal.tsx +40 -4
- package/src/lib/authentication/state.ts +123 -60
- package/src/lib/components/Header.tsx +5 -2
- package/src/lib/components/MobileTopNavigation.tsx +4 -2
package/dist/cli/cli.js
CHANGED
|
@@ -4,28 +4,10 @@ export declare const useRefreshUserProfile: ({ refetchOnWindowFocus, }?: {
|
|
|
4
4
|
refetchOnWindowFocus?: boolean | "always";
|
|
5
5
|
}) => import("@tanstack/react-query").UseQueryResult<boolean | undefined, Error>;
|
|
6
6
|
export declare const useVerifiedEmail: () => {
|
|
7
|
-
email:
|
|
8
|
-
isVerified:
|
|
7
|
+
email: any;
|
|
8
|
+
isVerified: any;
|
|
9
9
|
supportsEmailVerification: boolean;
|
|
10
10
|
refresh: () => undefined;
|
|
11
11
|
requestEmailVerification: (options?: AuthActionOptions) => Promise<void>;
|
|
12
12
|
};
|
|
13
|
-
export declare const useAuth: () =>
|
|
14
|
-
isBackendAvailable: boolean;
|
|
15
|
-
authMode: import("./state.js").AuthMode;
|
|
16
|
-
login: (options?: AuthActionOptions) => Promise<void>;
|
|
17
|
-
logout: () => Promise<void>;
|
|
18
|
-
signup: (options?: AuthActionOptions) => Promise<void>;
|
|
19
|
-
requestEmailVerification: (options?: AuthActionOptions) => Promise<void>;
|
|
20
|
-
isAuthenticated: boolean;
|
|
21
|
-
isPending: boolean;
|
|
22
|
-
profile: import("./state.js").UserProfile | null;
|
|
23
|
-
providerData: import("./state.js").ProviderData | null;
|
|
24
|
-
setAuthenticationPending: () => void;
|
|
25
|
-
setLoggedOut: () => void;
|
|
26
|
-
setLoggedIn: (args: {
|
|
27
|
-
profile: import("./state.js").UserProfile;
|
|
28
|
-
providerData: import("./state.js").ProviderData;
|
|
29
|
-
}) => void;
|
|
30
|
-
isAuthEnabled: boolean;
|
|
31
|
-
};
|
|
13
|
+
export declare const useAuth: () => any;
|
|
@@ -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>;
|
|
@@ -16,32 +16,14 @@ export interface AuthState {
|
|
|
16
16
|
providerData: ProviderData;
|
|
17
17
|
}) => void;
|
|
18
18
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
onHydrate: (fn: (state: AuthState) => void) => () => void;
|
|
28
|
-
onFinishHydration: (fn: (state: AuthState) => void) => () => void;
|
|
29
|
-
getOptions: () => Partial<import("zustand/middleware").PersistOptions<AuthState, unknown, unknown>>;
|
|
30
|
-
};
|
|
31
|
-
}>;
|
|
32
|
-
export declare const useAuthState: import("zustand").UseBoundStore<Omit<import("zustand").StoreApi<AuthState>, "setState" | "persist"> & {
|
|
33
|
-
setState(partial: AuthState | Partial<AuthState> | ((state: AuthState) => AuthState | Partial<AuthState>), replace?: false | undefined): unknown;
|
|
34
|
-
setState(state: AuthState | ((state: AuthState) => AuthState), replace: true): unknown;
|
|
35
|
-
persist: {
|
|
36
|
-
setOptions: (options: Partial<import("zustand/middleware").PersistOptions<AuthState, unknown, unknown>>) => void;
|
|
37
|
-
clearStorage: () => void;
|
|
38
|
-
rehydrate: () => Promise<void> | void;
|
|
39
|
-
hasHydrated: () => boolean;
|
|
40
|
-
onHydrate: (fn: (state: AuthState) => void) => () => void;
|
|
41
|
-
onFinishHydration: (fn: (state: AuthState) => void) => () => void;
|
|
42
|
-
getOptions: () => Partial<import("zustand/middleware").PersistOptions<AuthState, unknown, unknown>>;
|
|
43
|
-
};
|
|
44
|
-
}>;
|
|
19
|
+
type AuthStateStore = ReturnType<typeof createAuthStateStore>;
|
|
20
|
+
declare global {
|
|
21
|
+
var __APITOGO_AUTH_STATE: AuthStateStore | undefined;
|
|
22
|
+
}
|
|
23
|
+
declare function createAuthStateStore(): AuthStateStore;
|
|
24
|
+
export declare const authState: any;
|
|
25
|
+
export declare const useAuthState: any;
|
|
26
|
+
export declare const waitForAuthStateHydration: () => Promise<void>;
|
|
45
27
|
export interface UserProfile {
|
|
46
28
|
sub: string;
|
|
47
29
|
email: string | undefined;
|
|
@@ -50,3 +32,4 @@ export interface UserProfile {
|
|
|
50
32
|
pictureUrl: string | undefined;
|
|
51
33
|
[key: string]: string | boolean | undefined;
|
|
52
34
|
}
|
|
35
|
+
export {};
|
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,
|
|
@@ -63,6 +69,7 @@ export class DevPortalAuthenticationProvider
|
|
|
63
69
|
return;
|
|
64
70
|
}
|
|
65
71
|
|
|
72
|
+
await waitForAuthStateHydration();
|
|
66
73
|
await this.syncBackendAvailability();
|
|
67
74
|
if (!this.backendAvailable) {
|
|
68
75
|
this.setPreviewState();
|
|
@@ -77,6 +84,7 @@ export class DevPortalAuthenticationProvider
|
|
|
77
84
|
return;
|
|
78
85
|
}
|
|
79
86
|
|
|
87
|
+
await waitForAuthStateHydration();
|
|
80
88
|
await this.syncBackendAvailability();
|
|
81
89
|
if (!this.backendAvailable) {
|
|
82
90
|
this.setPreviewState();
|
|
@@ -123,6 +131,18 @@ export class DevPortalAuthenticationProvider
|
|
|
123
131
|
}
|
|
124
132
|
|
|
125
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> {
|
|
126
146
|
if (!this.apiBaseUrl) {
|
|
127
147
|
this.setPreviewState();
|
|
128
148
|
return false;
|
|
@@ -133,12 +153,16 @@ export class DevPortalAuthenticationProvider
|
|
|
133
153
|
return false;
|
|
134
154
|
}
|
|
135
155
|
|
|
156
|
+
await waitForAuthStateHydration();
|
|
136
157
|
await this.syncBackendAvailability();
|
|
137
158
|
if (!this.backendAvailable) {
|
|
138
159
|
this.setPreviewState();
|
|
139
160
|
return false;
|
|
140
161
|
}
|
|
141
162
|
|
|
163
|
+
const generation = ++this.refreshGeneration;
|
|
164
|
+
useAuthState.setState({ isPending: true });
|
|
165
|
+
|
|
142
166
|
try {
|
|
143
167
|
const response = await fetch(`${this.apiBaseUrl}/api/v1/auth/me`, {
|
|
144
168
|
method: "GET",
|
|
@@ -147,6 +171,10 @@ export class DevPortalAuthenticationProvider
|
|
|
147
171
|
redirect: "manual",
|
|
148
172
|
});
|
|
149
173
|
|
|
174
|
+
if (generation !== this.refreshGeneration) {
|
|
175
|
+
return useAuthState.getState().isAuthenticated;
|
|
176
|
+
}
|
|
177
|
+
|
|
150
178
|
// Backends may still respond with 302 to the IdP; treat as unauthenticated.
|
|
151
179
|
if (
|
|
152
180
|
response.type === "opaqueredirect" ||
|
|
@@ -174,22 +202,30 @@ export class DevPortalAuthenticationProvider
|
|
|
174
202
|
}
|
|
175
203
|
|
|
176
204
|
const me = (await response.json()) as EndUserMeResponse;
|
|
205
|
+
if (generation !== this.refreshGeneration) {
|
|
206
|
+
return useAuthState.getState().isAuthenticated;
|
|
207
|
+
}
|
|
208
|
+
|
|
177
209
|
const profile: UserProfile = mapEndUserMeToProfile(me);
|
|
178
210
|
|
|
179
|
-
useAuthState.
|
|
211
|
+
useAuthState.setState({
|
|
212
|
+
isAuthenticated: true,
|
|
213
|
+
isPending: false,
|
|
180
214
|
profile,
|
|
181
215
|
providerData: {
|
|
182
216
|
type: "dev-portal",
|
|
183
217
|
apiBaseUrl: this.apiBaseUrl,
|
|
184
218
|
authMode: "live",
|
|
185
219
|
},
|
|
186
|
-
});
|
|
187
|
-
useAuthState.setState({
|
|
188
220
|
isBackendAvailable: true,
|
|
189
221
|
authMode: "live",
|
|
190
222
|
});
|
|
191
223
|
return true;
|
|
192
224
|
} catch {
|
|
225
|
+
if (generation !== this.refreshGeneration) {
|
|
226
|
+
return useAuthState.getState().isAuthenticated;
|
|
227
|
+
}
|
|
228
|
+
|
|
193
229
|
this.setPreviewState();
|
|
194
230
|
return false;
|
|
195
231
|
}
|
|
@@ -40,72 +40,135 @@ export interface AuthState {
|
|
|
40
40
|
}) => void;
|
|
41
41
|
}
|
|
42
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
|
-
isPending: false,
|
|
90
|
-
isBackendAvailable:
|
|
91
|
-
persisted.isBackendAvailable ?? currentState.isBackendAvailable,
|
|
92
|
-
authMode: persisted.authMode ?? currentState.authMode,
|
|
93
|
-
};
|
|
94
|
-
},
|
|
95
|
-
partialize: (state) => ({
|
|
96
|
-
isBackendAvailable: state.isBackendAvailable,
|
|
97
|
-
authMode: state.authMode,
|
|
43
|
+
type AuthStateStore = ReturnType<typeof createAuthStateStore>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Vite SSR/dev can load authentication modules more than once (app + plugins),
|
|
47
|
+
* which would otherwise create duplicate Zustand stores. The header would read
|
|
48
|
+
* one store while dev-portal auth writes another.
|
|
49
|
+
*/
|
|
50
|
+
declare global {
|
|
51
|
+
var __APITOGO_AUTH_STATE: AuthStateStore | undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function createAuthStateStore(): AuthStateStore {
|
|
55
|
+
return create<AuthState>()(
|
|
56
|
+
persist(
|
|
57
|
+
(set) => ({
|
|
58
|
+
isAuthenticated: false,
|
|
59
|
+
isPending: true,
|
|
60
|
+
profile: null,
|
|
61
|
+
providerData: null,
|
|
62
|
+
isBackendAvailable: true,
|
|
63
|
+
authMode: "live",
|
|
64
|
+
setAuthenticationPending: () =>
|
|
65
|
+
set(() => ({
|
|
66
|
+
isAuthenticated: false,
|
|
67
|
+
isPending: false,
|
|
68
|
+
profile: null,
|
|
69
|
+
providerData: null,
|
|
70
|
+
isBackendAvailable: true,
|
|
71
|
+
authMode: "live",
|
|
72
|
+
})),
|
|
73
|
+
setLoggedOut: () =>
|
|
74
|
+
set(() => ({
|
|
75
|
+
isAuthenticated: false,
|
|
76
|
+
isPending: false,
|
|
77
|
+
profile: null,
|
|
78
|
+
providerData: null,
|
|
79
|
+
isBackendAvailable: true,
|
|
80
|
+
authMode: "live",
|
|
81
|
+
})),
|
|
82
|
+
setLoggedIn: ({ profile, providerData }) =>
|
|
83
|
+
set(() => ({
|
|
84
|
+
isAuthenticated: true,
|
|
85
|
+
isPending: false,
|
|
86
|
+
profile,
|
|
87
|
+
providerData,
|
|
88
|
+
})),
|
|
98
89
|
}),
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
90
|
+
{
|
|
91
|
+
version: 1,
|
|
92
|
+
migrate: (persistedState, version) => {
|
|
93
|
+
const persisted =
|
|
94
|
+
typeof persistedState === "object" && persistedState !== null
|
|
95
|
+
? (persistedState as Partial<AuthState>)
|
|
96
|
+
: {};
|
|
97
|
+
|
|
98
|
+
// v0 stored full auth identity in localStorage; keep only backend metadata.
|
|
99
|
+
if (version === 0) {
|
|
100
|
+
return {
|
|
101
|
+
isBackendAvailable: persisted.isBackendAvailable,
|
|
102
|
+
authMode: persisted.authMode,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return persisted;
|
|
107
|
+
},
|
|
108
|
+
merge: (persistedState, currentState) => {
|
|
109
|
+
const persisted =
|
|
110
|
+
typeof persistedState === "object" && persistedState !== null
|
|
111
|
+
? (persistedState as Partial<AuthState>)
|
|
112
|
+
: {};
|
|
113
|
+
|
|
114
|
+
// Cookie/session auth is refreshed from the backend on load. Do not
|
|
115
|
+
// restore identity fields from localStorage or OIDC return looks logged out.
|
|
116
|
+
return {
|
|
117
|
+
...currentState,
|
|
118
|
+
isPending: false,
|
|
119
|
+
isAuthenticated: currentState.isAuthenticated,
|
|
120
|
+
profile: currentState.profile,
|
|
121
|
+
providerData: currentState.providerData,
|
|
122
|
+
isBackendAvailable:
|
|
123
|
+
persisted.isBackendAvailable ?? currentState.isBackendAvailable,
|
|
124
|
+
authMode: persisted.authMode ?? currentState.authMode,
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
partialize: (state) => ({
|
|
128
|
+
isBackendAvailable: state.isBackendAvailable,
|
|
129
|
+
authMode: state.authMode,
|
|
130
|
+
}),
|
|
131
|
+
name: "auth-state",
|
|
132
|
+
storage: createJSONStorage(() => localStorage),
|
|
133
|
+
},
|
|
134
|
+
),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
globalThis.__APITOGO_AUTH_STATE ??= createAuthStateStore();
|
|
139
|
+
|
|
140
|
+
export const authState = globalThis.__APITOGO_AUTH_STATE;
|
|
104
141
|
|
|
105
142
|
syncZustandState(authState);
|
|
106
143
|
|
|
107
144
|
export const useAuthState = authState;
|
|
108
145
|
|
|
146
|
+
/** Wait until persisted auth metadata has rehydrated from localStorage. */
|
|
147
|
+
export const waitForAuthStateHydration = (): Promise<void> => {
|
|
148
|
+
if (typeof window === "undefined") {
|
|
149
|
+
return Promise.resolve();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const { persist } = useAuthState;
|
|
153
|
+
|
|
154
|
+
return new Promise((resolve) => {
|
|
155
|
+
const finish = () => {
|
|
156
|
+
// Let hydration merge flush before /auth/me refresh runs.
|
|
157
|
+
queueMicrotask(resolve);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
if (persist.hasHydrated()) {
|
|
161
|
+
finish();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const unsubscribe = persist.onFinishHydration(() => {
|
|
166
|
+
unsubscribe();
|
|
167
|
+
finish();
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
};
|
|
171
|
+
|
|
109
172
|
export interface UserProfile {
|
|
110
173
|
sub: string;
|
|
111
174
|
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"
|