@jskit-ai/auth-web 0.1.9 → 0.1.11

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.
@@ -0,0 +1,489 @@
1
+ import { mdiGoogle } from "@mdi/js";
2
+ import { authRegisterCommand } from "@jskit-ai/auth-core/shared/commands/authRegisterCommand";
3
+ import { authRegisterConfirmationResendCommand } from "@jskit-ai/auth-core/shared/commands/authRegisterConfirmationResendCommand";
4
+ import { authLoginPasswordCommand } from "@jskit-ai/auth-core/shared/commands/authLoginPasswordCommand";
5
+ import { authLoginOtpRequestCommand } from "@jskit-ai/auth-core/shared/commands/authLoginOtpRequestCommand";
6
+ import { authLoginOtpVerifyCommand } from "@jskit-ai/auth-core/shared/commands/authLoginOtpVerifyCommand";
7
+ import { authLoginOAuthStartCommand } from "@jskit-ai/auth-core/shared/commands/authLoginOAuthStartCommand";
8
+ import { authLoginOAuthCompleteCommand } from "@jskit-ai/auth-core/shared/commands/authLoginOAuthCompleteCommand";
9
+ import { authPasswordResetRequestCommand } from "@jskit-ai/auth-core/shared/commands/authPasswordResetRequestCommand";
10
+ import { AUTH_PATHS, buildAuthOauthStartPath } from "@jskit-ai/auth-core/shared/authPaths";
11
+ import { authHttpRequest } from "../../runtime/authHttpClient.js";
12
+ import { normalizeAuthReturnToPath } from "../../lib/returnToPath.js";
13
+ import { normalizeEmailAddress } from "./identityHelpers.js";
14
+ import { readRememberedAccountHint } from "./rememberedAccountStorage.js";
15
+ import {
16
+ stripOAuthParamsFromLocation,
17
+ readOAuthCallbackParamsFromLocation
18
+ } from "./oauthCallbackUrl.js";
19
+ import { ensureCommandSectionValid } from "./validationHelpers.js";
20
+ import { resolveRegisterCompletionState } from "./registerCompletion.js";
21
+
22
+ const SESSION_QUERY_KEY = Object.freeze(["auth-web", "session"]);
23
+
24
+ export function useLoginViewActions({
25
+ state,
26
+ validation,
27
+ queryClient,
28
+ errorRuntime
29
+ } = {}) {
30
+ function reportAuthFeedback({
31
+ message,
32
+ severity = "error",
33
+ channel = "banner",
34
+ dedupeKey = ""
35
+ } = {}) {
36
+ const normalizedMessage = String(message || "").trim();
37
+ if (!normalizedMessage) {
38
+ return;
39
+ }
40
+
41
+ errorRuntime.report({
42
+ source: "auth-web.default-login-view",
43
+ message: normalizedMessage,
44
+ severity,
45
+ channel,
46
+ dedupeKey: dedupeKey || `auth-web.default-login-view:${severity}:${normalizedMessage}`,
47
+ dedupeWindowMs: 3000
48
+ });
49
+ }
50
+
51
+ function setErrorMessage(message, dedupeKey = "") {
52
+ const normalizedMessage = String(message || "").trim();
53
+ state.errorMessage.value = normalizedMessage;
54
+ if (!normalizedMessage) {
55
+ return;
56
+ }
57
+
58
+ reportAuthFeedback({
59
+ message: normalizedMessage,
60
+ severity: "error",
61
+ channel: "banner",
62
+ dedupeKey
63
+ });
64
+ }
65
+
66
+ function setInfoMessage(message, dedupeKey = "") {
67
+ const normalizedMessage = String(message || "").trim();
68
+ state.infoMessage.value = normalizedMessage;
69
+ if (!normalizedMessage) {
70
+ return;
71
+ }
72
+
73
+ reportAuthFeedback({
74
+ message: normalizedMessage,
75
+ severity: "info",
76
+ channel: "snackbar",
77
+ dedupeKey
78
+ });
79
+ }
80
+
81
+ function clearTransientMessages() {
82
+ state.clearTransientMessages();
83
+ }
84
+
85
+ async function request(path, options = {}) {
86
+ return authHttpRequest(path, {
87
+ method: options.method || "GET",
88
+ ...(options.body ? { body: options.body } : {})
89
+ });
90
+ }
91
+
92
+ function applySessionPayload(payload) {
93
+ state.oauthProviders.value = Array.isArray(payload?.oauthProviders)
94
+ ? payload.oauthProviders
95
+ .map((provider) => {
96
+ if (!provider || typeof provider !== "object") {
97
+ return null;
98
+ }
99
+ const id = String(provider.id || "")
100
+ .trim()
101
+ .toLowerCase();
102
+ if (!id) {
103
+ return null;
104
+ }
105
+ return {
106
+ id,
107
+ label: String(provider.label || id).trim() || id
108
+ };
109
+ })
110
+ .filter(Boolean)
111
+ : [];
112
+
113
+ state.oauthDefaultProvider.value = String(payload?.oauthDefaultProvider || "")
114
+ .trim()
115
+ .toLowerCase();
116
+ }
117
+
118
+ function resolveDefaultOAuthProvider() {
119
+ const explicit = state.oauthDefaultProvider.value;
120
+ if (explicit) {
121
+ return explicit;
122
+ }
123
+ return String(state.oauthProviders.value[0]?.id || "")
124
+ .trim()
125
+ .toLowerCase();
126
+ }
127
+
128
+ async function refreshSession() {
129
+ const session = await queryClient.fetchQuery({
130
+ queryKey: SESSION_QUERY_KEY,
131
+ queryFn: () => request(AUTH_PATHS.SESSION),
132
+ staleTime: 0
133
+ });
134
+ applySessionPayload(session);
135
+ return session;
136
+ }
137
+
138
+ async function completeLogin() {
139
+ const session = await refreshSession();
140
+ if (!session?.authenticated) {
141
+ throw new Error("Login succeeded but the session is not active yet. Please retry.");
142
+ }
143
+ if (typeof window === "object" && window.location) {
144
+ window.location.replace(state.requestedReturnTo.value);
145
+ }
146
+ }
147
+
148
+ async function submitRegister({ normalizedEmail, shouldRememberAccount }) {
149
+ const registerPayload = {
150
+ email: normalizedEmail,
151
+ password: String(state.password.value || "")
152
+ };
153
+ ensureCommandSectionValid(authRegisterCommand, "bodyValidator", registerPayload, "Unable to register.");
154
+
155
+ const registerResult = await request(AUTH_PATHS.REGISTER, {
156
+ method: "POST",
157
+ body: registerPayload
158
+ });
159
+ state.applyRememberedAccountPreference({
160
+ email: normalizedEmail,
161
+ displayName: registerResult?.username || normalizedEmail,
162
+ shouldRemember: shouldRememberAccount
163
+ });
164
+
165
+ const registerCompletion = resolveRegisterCompletionState(registerResult);
166
+ if (!registerCompletion.shouldCompleteLogin) {
167
+ state.enterEmailConfirmationPendingState({
168
+ emailAddress: normalizedEmail,
169
+ message: registerCompletion.message
170
+ });
171
+ return;
172
+ }
173
+
174
+ await completeLogin();
175
+ }
176
+
177
+ async function submitForgot({ normalizedEmail }) {
178
+ const forgotPayload = { email: normalizedEmail };
179
+ ensureCommandSectionValid(
180
+ authPasswordResetRequestCommand,
181
+ "bodyValidator",
182
+ forgotPayload,
183
+ "Unable to request password reset."
184
+ );
185
+
186
+ await request(AUTH_PATHS.PASSWORD_FORGOT, {
187
+ method: "POST",
188
+ body: forgotPayload
189
+ });
190
+ setInfoMessage("Password reset instructions sent.", "auth-web.default-login-view:password-reset-sent");
191
+ }
192
+
193
+ async function submitOtp({ normalizedEmail, shouldRememberAccount }) {
194
+ const otpPayload = {
195
+ email: normalizedEmail,
196
+ token: String(state.otpCode.value || "").trim(),
197
+ type: "email"
198
+ };
199
+ ensureCommandSectionValid(authLoginOtpVerifyCommand, "bodyValidator", otpPayload, "Unable to verify one-time code.");
200
+
201
+ const otpResult = await request(AUTH_PATHS.LOGIN_OTP_VERIFY, {
202
+ method: "POST",
203
+ body: otpPayload
204
+ });
205
+ state.applyRememberedAccountPreference({
206
+ email: normalizedEmail,
207
+ displayName: otpResult?.username || normalizedEmail,
208
+ shouldRemember: shouldRememberAccount
209
+ });
210
+ await completeLogin();
211
+ }
212
+
213
+ async function submitLogin({ normalizedEmail, shouldRememberAccount }) {
214
+ const loginPayload = {
215
+ email: normalizedEmail,
216
+ password: String(state.password.value || "")
217
+ };
218
+ ensureCommandSectionValid(authLoginPasswordCommand, "bodyValidator", loginPayload, "Unable to sign in.");
219
+
220
+ const loginResult = await request(AUTH_PATHS.LOGIN, {
221
+ method: "POST",
222
+ body: loginPayload
223
+ });
224
+ state.applyRememberedAccountPreference({
225
+ email: normalizedEmail,
226
+ displayName: loginResult?.username || normalizedEmail,
227
+ shouldRemember: shouldRememberAccount
228
+ });
229
+ await completeLogin();
230
+ }
231
+
232
+ function buildOAuthCompletePayload({ callbackParams, provider, hasSessionPair }) {
233
+ const payload = {};
234
+ if (provider) {
235
+ payload.provider = provider;
236
+ }
237
+ if (callbackParams.code) {
238
+ payload.code = callbackParams.code;
239
+ }
240
+ if (hasSessionPair) {
241
+ payload.accessToken = callbackParams.accessToken;
242
+ payload.refreshToken = callbackParams.refreshToken;
243
+ }
244
+ return payload;
245
+ }
246
+
247
+ async function handleOAuthCallbackIfPresent() {
248
+ const callbackParams = readOAuthCallbackParamsFromLocation();
249
+ if (!callbackParams) {
250
+ return false;
251
+ }
252
+
253
+ state.requestedReturnTo.value = normalizeAuthReturnToPath(callbackParams.returnTo, state.requestedReturnTo.value, {
254
+ allowedOrigins: state.allowedReturnToOrigins.value
255
+ });
256
+
257
+ const provider = String(callbackParams.provider || resolveDefaultOAuthProvider() || "")
258
+ .trim()
259
+ .toLowerCase();
260
+ const oauthError = callbackParams.errorCode;
261
+ const oauthErrorDescription = callbackParams.errorDescription;
262
+ const hasSessionPair = callbackParams.hasSessionPair === true;
263
+
264
+ if (!provider && !hasSessionPair) {
265
+ setErrorMessage("OAuth provider is missing from callback.", "auth-web.default-login-view:oauth-missing-provider");
266
+ stripOAuthParamsFromLocation();
267
+ return true;
268
+ }
269
+
270
+ if (oauthError) {
271
+ setErrorMessage(oauthErrorDescription || oauthError, "auth-web.default-login-view:oauth-callback-error");
272
+ stripOAuthParamsFromLocation();
273
+ return true;
274
+ }
275
+
276
+ state.loading.value = true;
277
+ clearTransientMessages();
278
+
279
+ try {
280
+ const payload = buildOAuthCompletePayload({
281
+ callbackParams,
282
+ provider,
283
+ hasSessionPair
284
+ });
285
+ ensureCommandSectionValid(
286
+ authLoginOAuthCompleteCommand,
287
+ "bodyValidator",
288
+ payload,
289
+ "Invalid OAuth callback payload."
290
+ );
291
+
292
+ const oauthResult = await request(AUTH_PATHS.OAUTH_COMPLETE, {
293
+ method: "POST",
294
+ body: payload
295
+ });
296
+ state.applyRememberedAccountPreference({
297
+ email: oauthResult?.email || state.email.value,
298
+ displayName: oauthResult?.username || oauthResult?.email || state.email.value,
299
+ shouldRemember: state.rememberAccountOnDevice.value !== false
300
+ });
301
+
302
+ stripOAuthParamsFromLocation();
303
+ await completeLogin();
304
+ } catch (error) {
305
+ setErrorMessage(String(error?.message || "Unable to complete OAuth sign-in."));
306
+ stripOAuthParamsFromLocation();
307
+ } finally {
308
+ state.loading.value = false;
309
+ }
310
+
311
+ return true;
312
+ }
313
+
314
+ async function submitAuth() {
315
+ state.submitAttempted.value = true;
316
+ clearTransientMessages();
317
+ if (!validation.canSubmit.value) {
318
+ return;
319
+ }
320
+
321
+ state.loading.value = true;
322
+
323
+ try {
324
+ const normalizedEmail = state.resolveNormalizedEmail();
325
+ const shouldRememberAccount = state.rememberAccountOnDevice.value !== false;
326
+
327
+ if (state.isRegister.value) {
328
+ await submitRegister({ normalizedEmail, shouldRememberAccount });
329
+ return;
330
+ }
331
+
332
+ if (state.isForgot.value) {
333
+ await submitForgot({ normalizedEmail });
334
+ return;
335
+ }
336
+
337
+ if (state.isOtp.value) {
338
+ await submitOtp({ normalizedEmail, shouldRememberAccount });
339
+ return;
340
+ }
341
+
342
+ await submitLogin({ normalizedEmail, shouldRememberAccount });
343
+ } catch (error) {
344
+ setErrorMessage(String(error?.message || "Authentication failed."));
345
+ } finally {
346
+ state.loading.value = false;
347
+ }
348
+ }
349
+
350
+ async function requestOtpCode() {
351
+ state.otpRequestPending.value = true;
352
+ clearTransientMessages();
353
+ try {
354
+ const normalizedEmail = state.resolveNormalizedEmail();
355
+ const otpRequestPayload = {
356
+ email: normalizedEmail,
357
+ returnTo: state.requestedReturnTo.value
358
+ };
359
+ ensureCommandSectionValid(
360
+ authLoginOtpRequestCommand,
361
+ "bodyValidator",
362
+ otpRequestPayload,
363
+ "Unable to request one-time code."
364
+ );
365
+ await request(AUTH_PATHS.LOGIN_OTP_REQUEST, {
366
+ method: "POST",
367
+ body: otpRequestPayload
368
+ });
369
+ setInfoMessage("One-time code sent. Check your inbox.", "auth-web.default-login-view:otp-code-sent");
370
+ } catch (error) {
371
+ setErrorMessage(String(error?.message || "Unable to request one-time code."));
372
+ } finally {
373
+ state.otpRequestPending.value = false;
374
+ }
375
+ }
376
+
377
+ async function resendRegisterConfirmationEmail() {
378
+ const normalizedEmail = normalizeEmailAddress(state.pendingEmailConfirmationAddress.value || state.email.value);
379
+ if (!normalizedEmail) {
380
+ setErrorMessage("Enter an email address before requesting confirmation.");
381
+ return;
382
+ }
383
+
384
+ state.registerConfirmationResendPending.value = true;
385
+ clearTransientMessages();
386
+ try {
387
+ const resendPayload = {
388
+ email: normalizedEmail
389
+ };
390
+ ensureCommandSectionValid(
391
+ authRegisterConfirmationResendCommand,
392
+ "bodyValidator",
393
+ resendPayload,
394
+ "Unable to resend confirmation email."
395
+ );
396
+ const resendResult = await request(AUTH_PATHS.REGISTER_CONFIRMATION_RESEND, {
397
+ method: "POST",
398
+ body: resendPayload
399
+ });
400
+ const info =
401
+ String(resendResult?.message || "").trim() ||
402
+ "If an account exists for that email, a confirmation email has been sent.";
403
+ setInfoMessage(info, "auth-web.default-login-view:register-confirmation-resend");
404
+ } catch (error) {
405
+ setErrorMessage(String(error?.message || "Unable to resend confirmation email."));
406
+ } finally {
407
+ state.registerConfirmationResendPending.value = false;
408
+ }
409
+ }
410
+
411
+ function oauthProviderButtonLabel(provider) {
412
+ const providerLabel = String(provider?.label || provider?.id || "OAuth provider");
413
+ if (state.isRegister.value) {
414
+ return `Register with ${providerLabel}`;
415
+ }
416
+ if (state.showRememberedAccount.value) {
417
+ return `Continue with ${providerLabel} as ${state.rememberedAccountDisplayName.value}`;
418
+ }
419
+ return `Continue with ${providerLabel}`;
420
+ }
421
+
422
+ function oauthProviderIcon(provider) {
423
+ if (String(provider?.id || "").toLowerCase() === "google") {
424
+ return mdiGoogle;
425
+ }
426
+ return undefined;
427
+ }
428
+
429
+ function startOAuthSignIn(providerId) {
430
+ const provider = String(providerId || "").trim().toLowerCase();
431
+ if (!provider || typeof window !== "object" || !window.location) {
432
+ return;
433
+ }
434
+
435
+ const paramsPayload = {
436
+ provider
437
+ };
438
+ const queryPayload = {
439
+ returnTo: state.requestedReturnTo.value
440
+ };
441
+ try {
442
+ ensureCommandSectionValid(
443
+ authLoginOAuthStartCommand,
444
+ "paramsValidator",
445
+ paramsPayload,
446
+ "OAuth provider id is invalid."
447
+ );
448
+ ensureCommandSectionValid(
449
+ authLoginOAuthStartCommand,
450
+ "queryValidator",
451
+ queryPayload,
452
+ "OAuth return path is invalid."
453
+ );
454
+ } catch (error) {
455
+ setErrorMessage(String(error?.message || "Unable to start OAuth sign-in."));
456
+ return;
457
+ }
458
+
459
+ const params = new URLSearchParams(queryPayload);
460
+ const oauthStartPath = buildAuthOauthStartPath(provider);
461
+ window.location.assign(`${oauthStartPath}?${params.toString()}`);
462
+ }
463
+
464
+ async function initializeOnMounted() {
465
+ state.applyRememberedAccountHint(readRememberedAccountHint());
466
+ state.loading.value = true;
467
+ try {
468
+ const session = await refreshSession();
469
+ const callbackHandled = await handleOAuthCallbackIfPresent();
470
+ if (!callbackHandled && session?.authenticated && typeof window === "object" && window.location) {
471
+ window.location.replace(state.requestedReturnTo.value);
472
+ }
473
+ } catch (error) {
474
+ setErrorMessage(String(error?.message || "Unable to initialize sign in."));
475
+ } finally {
476
+ state.loading.value = false;
477
+ }
478
+ }
479
+
480
+ return {
481
+ submitAuth,
482
+ requestOtpCode,
483
+ resendRegisterConfirmationEmail,
484
+ oauthProviderButtonLabel,
485
+ oauthProviderIcon,
486
+ startOAuthSignIn,
487
+ initializeOnMounted
488
+ };
489
+ }