@douvery/auth 0.3.1 → 0.3.3
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/index.d.ts +61 -1
- package/dist/index.js +132 -2
- package/dist/index.js.map +1 -1
- package/dist/qwik/index.d.ts +59 -25
- package/dist/qwik/index.js +154 -59
- package/dist/qwik/index.js.map +1 -1
- package/package.json +4 -3
- package/src/qwik/index.tsx +439 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @douvery/auth/qwik - Qwik adapter
|
|
3
|
+
*
|
|
4
|
+
* Uses QRL for config to avoid Qwik serialization issues with
|
|
5
|
+
* function-based storage adapters (customStorage).
|
|
6
|
+
* The client is created inside useVisibleTask$ and wrapped with
|
|
7
|
+
* noSerialize() since DouveryAuthClient has non-serializable methods.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
createContextId,
|
|
12
|
+
useContextProvider,
|
|
13
|
+
useContext,
|
|
14
|
+
useSignal,
|
|
15
|
+
useTask$,
|
|
16
|
+
useVisibleTask$,
|
|
17
|
+
component$,
|
|
18
|
+
$,
|
|
19
|
+
Slot,
|
|
20
|
+
noSerialize,
|
|
21
|
+
type Signal,
|
|
22
|
+
type NoSerialize,
|
|
23
|
+
type QRL,
|
|
24
|
+
} from "@builder.io/qwik";
|
|
25
|
+
import {
|
|
26
|
+
DouveryAuthClient,
|
|
27
|
+
createDouveryAuth,
|
|
28
|
+
type DouveryAuthConfig,
|
|
29
|
+
type AuthState,
|
|
30
|
+
type User,
|
|
31
|
+
type LoginOptions,
|
|
32
|
+
type LogoutOptions,
|
|
33
|
+
type SelectAccountOptions,
|
|
34
|
+
type RegisterOptions,
|
|
35
|
+
type RecoverAccountOptions,
|
|
36
|
+
type VerifyAccountOptions,
|
|
37
|
+
type UpgradeAccountOptions,
|
|
38
|
+
type SetupPasskeyOptions,
|
|
39
|
+
type SetupAddressOptions,
|
|
40
|
+
type AddAccountOptions,
|
|
41
|
+
type RevokeTokenOptions,
|
|
42
|
+
type AuthUrl,
|
|
43
|
+
} from "@douvery/auth";
|
|
44
|
+
|
|
45
|
+
// ============================================================================
|
|
46
|
+
// Context
|
|
47
|
+
// ============================================================================
|
|
48
|
+
|
|
49
|
+
interface DouveryAuthContextValue {
|
|
50
|
+
state: Signal<AuthState>;
|
|
51
|
+
isInitialized: Signal<boolean>;
|
|
52
|
+
isLoading: Signal<boolean>;
|
|
53
|
+
error: Signal<Error | null>;
|
|
54
|
+
clientRef: Signal<NoSerialize<DouveryAuthClient> | undefined>;
|
|
55
|
+
/** Application-specific user data from SSR (e.g. routeLoader$). */
|
|
56
|
+
appUser: Signal<unknown>;
|
|
57
|
+
appUserAuthenticated: Signal<boolean>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const DouveryAuthContext =
|
|
61
|
+
createContextId<DouveryAuthContextValue>("douvery-auth");
|
|
62
|
+
|
|
63
|
+
// ============================================================================
|
|
64
|
+
// Provider
|
|
65
|
+
// ============================================================================
|
|
66
|
+
|
|
67
|
+
export interface DouveryAuthProviderProps {
|
|
68
|
+
/**
|
|
69
|
+
* QRL that returns the auth configuration.
|
|
70
|
+
* Use $(() => getDouveryAuthConfig()) to wrap your config factory.
|
|
71
|
+
* This avoids Qwik serialization issues with customStorage functions.
|
|
72
|
+
*/
|
|
73
|
+
config$: QRL<() => DouveryAuthConfig>;
|
|
74
|
+
/**
|
|
75
|
+
* Optional application-specific user data loaded from SSR (routeLoader$).
|
|
76
|
+
* This is separate from OAuth user – it holds the full app user object
|
|
77
|
+
* (e.g. UserACC with address, active, sessionId, etc.).
|
|
78
|
+
* Pass the routeLoader$ signal directly.
|
|
79
|
+
*/
|
|
80
|
+
appUser?: Signal<unknown>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const DEFAULT_STATE: AuthState = {
|
|
84
|
+
status: "unauthenticated",
|
|
85
|
+
user: null,
|
|
86
|
+
tokens: null,
|
|
87
|
+
error: null,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const DouveryAuthProvider = component$<DouveryAuthProviderProps>(
|
|
91
|
+
({ config$, appUser: externalAppUser }) => {
|
|
92
|
+
// All signals are serializable - no functions stored directly
|
|
93
|
+
const state = useSignal<AuthState>(DEFAULT_STATE);
|
|
94
|
+
const isInitialized = useSignal(false);
|
|
95
|
+
const isLoading = useSignal(false);
|
|
96
|
+
const error = useSignal<Error | null>(null);
|
|
97
|
+
const clientRef = useSignal<NoSerialize<DouveryAuthClient>>();
|
|
98
|
+
|
|
99
|
+
// App user data: use external signal if provided, otherwise create internal one
|
|
100
|
+
const internalAppUser = useSignal<unknown>(externalAppUser?.value ?? null);
|
|
101
|
+
const appUser = externalAppUser ?? internalAppUser;
|
|
102
|
+
const appUserAuthenticated = useSignal<boolean>(!!appUser.value);
|
|
103
|
+
|
|
104
|
+
// Keep appUserAuthenticated in sync
|
|
105
|
+
useTask$(({ track }) => {
|
|
106
|
+
const u = track(() => appUser.value);
|
|
107
|
+
appUserAuthenticated.value = !!u;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
useContextProvider(DouveryAuthContext, {
|
|
111
|
+
state,
|
|
112
|
+
isInitialized,
|
|
113
|
+
isLoading,
|
|
114
|
+
error,
|
|
115
|
+
clientRef,
|
|
116
|
+
appUser,
|
|
117
|
+
appUserAuthenticated,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Client creation deferred to browser-only task.
|
|
121
|
+
// The QRL is invoked here, returning the full config (with customStorage).
|
|
122
|
+
// noSerialize() wraps the client so Qwik doesn't try to serialize it.
|
|
123
|
+
useVisibleTask$(async () => {
|
|
124
|
+
const config = await config$();
|
|
125
|
+
const client = createDouveryAuth(config);
|
|
126
|
+
clientRef.value = noSerialize(client);
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
await client.initialize();
|
|
130
|
+
isInitialized.value = true;
|
|
131
|
+
state.value = client.getState();
|
|
132
|
+
} catch (err) {
|
|
133
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const unsubscribe = client.subscribe((event) => {
|
|
137
|
+
state.value = client.getState();
|
|
138
|
+
if (
|
|
139
|
+
event.type === "LOGIN_ERROR" ||
|
|
140
|
+
event.type === "LOGOUT_ERROR" ||
|
|
141
|
+
event.type === "TOKEN_REFRESH_ERROR"
|
|
142
|
+
) {
|
|
143
|
+
error.value = event.error;
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return () => unsubscribe();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return <Slot />;
|
|
151
|
+
},
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// ============================================================================
|
|
155
|
+
// Hooks
|
|
156
|
+
// ============================================================================
|
|
157
|
+
|
|
158
|
+
export function useDouveryAuth() {
|
|
159
|
+
return useContext(DouveryAuthContext);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Internal helper: safely access the client from context.
|
|
164
|
+
* Throws if the client hasn't been initialized yet (before useVisibleTask$ runs).
|
|
165
|
+
*/
|
|
166
|
+
function getClient(ctx: DouveryAuthContextValue): DouveryAuthClient {
|
|
167
|
+
const client = ctx.clientRef.value;
|
|
168
|
+
if (!client) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
"DouveryAuth client not initialized. " +
|
|
171
|
+
"Ensure DouveryAuthProvider is mounted and the page has hydrated.",
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
return client;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function useUser(): Signal<User | null> {
|
|
178
|
+
const { state } = useDouveryAuth();
|
|
179
|
+
const user = useSignal<User | null>(state.value.user);
|
|
180
|
+
useTask$(({ track }) => {
|
|
181
|
+
track(() => state.value);
|
|
182
|
+
user.value = state.value.user;
|
|
183
|
+
});
|
|
184
|
+
return user;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function useIsAuthenticated(): Signal<boolean> {
|
|
188
|
+
const { state } = useDouveryAuth();
|
|
189
|
+
const isAuth = useSignal(state.value.status === "authenticated");
|
|
190
|
+
useTask$(({ track }) => {
|
|
191
|
+
track(() => state.value);
|
|
192
|
+
isAuth.value = state.value.status === "authenticated";
|
|
193
|
+
});
|
|
194
|
+
return isAuth;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function useAuthActions() {
|
|
198
|
+
const ctx = useDouveryAuth();
|
|
199
|
+
const { isLoading, error } = ctx;
|
|
200
|
+
|
|
201
|
+
const login = $(async (options?: LoginOptions) => {
|
|
202
|
+
const client = getClient(ctx);
|
|
203
|
+
isLoading.value = true;
|
|
204
|
+
error.value = null;
|
|
205
|
+
try {
|
|
206
|
+
await client.login(options);
|
|
207
|
+
} catch (err) {
|
|
208
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
209
|
+
throw err;
|
|
210
|
+
} finally {
|
|
211
|
+
isLoading.value = false;
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const logout = $(async (options?: LogoutOptions) => {
|
|
216
|
+
const client = getClient(ctx);
|
|
217
|
+
isLoading.value = true;
|
|
218
|
+
error.value = null;
|
|
219
|
+
try {
|
|
220
|
+
await client.logout(options);
|
|
221
|
+
} catch (err) {
|
|
222
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
223
|
+
throw err;
|
|
224
|
+
} finally {
|
|
225
|
+
isLoading.value = false;
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const selectAccount = $((options?: SelectAccountOptions) => {
|
|
230
|
+
getClient(ctx).selectAccount(options);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const addAccount = $((options?: AddAccountOptions) => {
|
|
234
|
+
getClient(ctx).addAccount(options);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const register = $((options?: RegisterOptions) => {
|
|
238
|
+
getClient(ctx).register(options);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const recoverAccount = $((options?: RecoverAccountOptions) => {
|
|
242
|
+
getClient(ctx).recoverAccount(options);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const verifyAccount = $((options?: VerifyAccountOptions) => {
|
|
246
|
+
getClient(ctx).verifyAccount(options);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const upgradeAccount = $((options?: UpgradeAccountOptions) => {
|
|
250
|
+
getClient(ctx).upgradeAccount(options);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const setupPasskey = $((options?: SetupPasskeyOptions) => {
|
|
254
|
+
getClient(ctx).setupPasskey(options);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const setupAddress = $((options?: SetupAddressOptions) => {
|
|
258
|
+
getClient(ctx).setupAddress(options);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const revokeToken = $(async (options?: RevokeTokenOptions) => {
|
|
262
|
+
const client = getClient(ctx);
|
|
263
|
+
isLoading.value = true;
|
|
264
|
+
error.value = null;
|
|
265
|
+
try {
|
|
266
|
+
await client.revokeToken(options);
|
|
267
|
+
} catch (err) {
|
|
268
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
269
|
+
throw err;
|
|
270
|
+
} finally {
|
|
271
|
+
isLoading.value = false;
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
login,
|
|
277
|
+
logout,
|
|
278
|
+
selectAccount,
|
|
279
|
+
addAccount,
|
|
280
|
+
register,
|
|
281
|
+
recoverAccount,
|
|
282
|
+
verifyAccount,
|
|
283
|
+
upgradeAccount,
|
|
284
|
+
setupPasskey,
|
|
285
|
+
setupAddress,
|
|
286
|
+
revokeToken,
|
|
287
|
+
isLoading,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Get URL builders for auth pages (non-redirecting, useful for <a> tags) */
|
|
292
|
+
export function useAuthUrls() {
|
|
293
|
+
const ctx = useDouveryAuth();
|
|
294
|
+
return {
|
|
295
|
+
loginUrl: $(
|
|
296
|
+
(options?: LoginOptions): AuthUrl =>
|
|
297
|
+
getClient(ctx).buildLoginUrl(options),
|
|
298
|
+
),
|
|
299
|
+
logoutUrl: $(
|
|
300
|
+
(options?: LogoutOptions): AuthUrl =>
|
|
301
|
+
getClient(ctx).buildLogoutUrl(options),
|
|
302
|
+
),
|
|
303
|
+
selectAccountUrl: $(
|
|
304
|
+
(options?: SelectAccountOptions): AuthUrl =>
|
|
305
|
+
getClient(ctx).buildSelectAccountUrl(options),
|
|
306
|
+
),
|
|
307
|
+
addAccountUrl: $(
|
|
308
|
+
(options?: AddAccountOptions): AuthUrl =>
|
|
309
|
+
getClient(ctx).buildAddAccountUrl(options),
|
|
310
|
+
),
|
|
311
|
+
registerUrl: $(
|
|
312
|
+
(options?: RegisterOptions): AuthUrl =>
|
|
313
|
+
getClient(ctx).buildRegisterUrl(options),
|
|
314
|
+
),
|
|
315
|
+
recoverAccountUrl: $(
|
|
316
|
+
(options?: RecoverAccountOptions): AuthUrl =>
|
|
317
|
+
getClient(ctx).buildRecoverAccountUrl(options),
|
|
318
|
+
),
|
|
319
|
+
verifyAccountUrl: $(
|
|
320
|
+
(options?: VerifyAccountOptions): AuthUrl =>
|
|
321
|
+
getClient(ctx).buildVerifyAccountUrl(options),
|
|
322
|
+
),
|
|
323
|
+
upgradeAccountUrl: $(
|
|
324
|
+
(options?: UpgradeAccountOptions): AuthUrl =>
|
|
325
|
+
getClient(ctx).buildUpgradeAccountUrl(options),
|
|
326
|
+
),
|
|
327
|
+
setupPasskeyUrl: $(
|
|
328
|
+
(options?: SetupPasskeyOptions): AuthUrl =>
|
|
329
|
+
getClient(ctx).buildSetupPasskeyUrl(options),
|
|
330
|
+
),
|
|
331
|
+
setupAddressUrl: $(
|
|
332
|
+
(options?: SetupAddressOptions): AuthUrl =>
|
|
333
|
+
getClient(ctx).buildSetupAddressUrl(options),
|
|
334
|
+
),
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Get session status helpers */
|
|
339
|
+
export function useSessionStatus() {
|
|
340
|
+
const ctx = useDouveryAuth();
|
|
341
|
+
const { state } = ctx;
|
|
342
|
+
const c = ctx.clientRef.value;
|
|
343
|
+
const isExpired = useSignal(c ? c.isSessionExpired() : false);
|
|
344
|
+
const needsVerification = useSignal(c ? c.needsEmailVerification() : false);
|
|
345
|
+
const isGuest = useSignal(c ? c.isGuestAccount() : false);
|
|
346
|
+
|
|
347
|
+
useTask$(({ track }) => {
|
|
348
|
+
track(() => state.value);
|
|
349
|
+
const client = ctx.clientRef.value;
|
|
350
|
+
if (client) {
|
|
351
|
+
isExpired.value = client.isSessionExpired();
|
|
352
|
+
needsVerification.value = client.needsEmailVerification();
|
|
353
|
+
isGuest.value = client.isGuestAccount();
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
return { isExpired, needsVerification, isGuest };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ============================================================================
|
|
361
|
+
// App User hooks
|
|
362
|
+
// ============================================================================
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Returns the application-specific user data provided via `appUser` prop.
|
|
366
|
+
* Cast to your app's user type: `const user = useAppUser<UserACC>()`.
|
|
367
|
+
* Returns `{ user: Signal<T | null>, isAuthenticated: Signal<boolean> }`.
|
|
368
|
+
*/
|
|
369
|
+
export function useAppUser<T = unknown>() {
|
|
370
|
+
const { appUser, appUserAuthenticated } = useDouveryAuth();
|
|
371
|
+
return {
|
|
372
|
+
user: appUser as Signal<T | null>,
|
|
373
|
+
isAuthenticated: appUserAuthenticated,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Full app user context with refresh capabilities.
|
|
379
|
+
* Use when you need to re-fetch user data from the server.
|
|
380
|
+
*/
|
|
381
|
+
export function useAppUserActions<T = unknown>() {
|
|
382
|
+
const { appUser, appUserAuthenticated } = useDouveryAuth();
|
|
383
|
+
|
|
384
|
+
const updateUser = $((userData: T | null) => {
|
|
385
|
+
(appUser as Signal<T | null>).value = userData;
|
|
386
|
+
appUserAuthenticated.value = !!userData;
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const refreshUser = $(async () => {
|
|
390
|
+
try {
|
|
391
|
+
const response = await fetch("/api/auth/me", {
|
|
392
|
+
method: "GET",
|
|
393
|
+
credentials: "include",
|
|
394
|
+
headers: { "Cache-Control": "no-cache", Pragma: "no-cache" },
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
if (response.ok) {
|
|
398
|
+
const data = await response.json();
|
|
399
|
+
(appUser as Signal<T | null>).value = data.user;
|
|
400
|
+
appUserAuthenticated.value = true;
|
|
401
|
+
} else if (response.status === 401 || response.status === 403) {
|
|
402
|
+
(appUser as Signal<T | null>).value = null;
|
|
403
|
+
appUserAuthenticated.value = false;
|
|
404
|
+
}
|
|
405
|
+
} catch {
|
|
406
|
+
// Network error: keep current state
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
user: appUser as Signal<T | null>,
|
|
412
|
+
isAuthenticated: appUserAuthenticated,
|
|
413
|
+
updateUser,
|
|
414
|
+
refreshUser,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ============================================================================
|
|
419
|
+
// Re-exports
|
|
420
|
+
// ============================================================================
|
|
421
|
+
|
|
422
|
+
export { DouveryAuthClient, createDouveryAuth } from "@douvery/auth";
|
|
423
|
+
export type {
|
|
424
|
+
DouveryAuthConfig,
|
|
425
|
+
AuthState,
|
|
426
|
+
User,
|
|
427
|
+
LoginOptions,
|
|
428
|
+
LogoutOptions,
|
|
429
|
+
SelectAccountOptions,
|
|
430
|
+
RegisterOptions,
|
|
431
|
+
RecoverAccountOptions,
|
|
432
|
+
VerifyAccountOptions,
|
|
433
|
+
UpgradeAccountOptions,
|
|
434
|
+
SetupPasskeyOptions,
|
|
435
|
+
SetupAddressOptions,
|
|
436
|
+
AddAccountOptions,
|
|
437
|
+
RevokeTokenOptions,
|
|
438
|
+
AuthUrl,
|
|
439
|
+
} from "@douvery/auth";
|