@jskit-ai/auth-web 0.1.8 → 0.1.10
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/package.descriptor.mjs +11 -7
- package/package.json +5 -5
- package/src/client/composables/loginView/constants.js +42 -0
- package/src/client/composables/loginView/identityHelpers.js +23 -0
- package/src/client/composables/loginView/oauthCallbackUrl.js +90 -0
- package/src/client/composables/loginView/registerCompletion.js +18 -0
- package/src/client/composables/loginView/rememberedAccountStorage.js +95 -0
- package/src/client/composables/loginView/useLoginViewActions.js +489 -0
- package/src/client/composables/loginView/useLoginViewState.js +262 -0
- package/src/client/composables/loginView/useLoginViewValidation.js +124 -0
- package/src/client/composables/loginView/validationHelpers.js +65 -0
- package/src/client/runtime/authGuardRuntime.js +83 -15
- package/src/client/runtime/useLoginView.js +69 -3
- package/src/client/views/DefaultLoginView.vue +215 -134
- package/src/server/constants/authActionIds.js +1 -0
- package/src/server/controllers/AuthController.js +6 -0
- package/src/server/routes/authRoutes.js +35 -11
- package/src/server/services/AuthWebService.js +7 -0
- package/test/authGuardRuntime.test.js +44 -0
- package/test/clientSurface.test.js +9 -4
- package/test/providerRuntime.test.js +15 -0
- package/test/registerFlow.test.js +40 -0
- package/src/client/composables/useDefaultLoginView.js +0 -935
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { computed, ref } from "vue";
|
|
2
|
+
import {
|
|
3
|
+
resolveSurfaceIdFromPlacementPathname,
|
|
4
|
+
resolveSurfaceRootPathFromPlacementContext
|
|
5
|
+
} from "@jskit-ai/shell-web/client/placement";
|
|
6
|
+
import {
|
|
7
|
+
normalizeAuthReturnToPath,
|
|
8
|
+
resolveAllowedReturnToOriginsFromPlacementContext
|
|
9
|
+
} from "../../lib/returnToPath.js";
|
|
10
|
+
import {
|
|
11
|
+
LOGIN_MODE,
|
|
12
|
+
REGISTER_MODE,
|
|
13
|
+
FORGOT_MODE,
|
|
14
|
+
OTP_MODE,
|
|
15
|
+
EMAIL_CONFIRMATION_MODE,
|
|
16
|
+
AUTH_TITLE_BY_MODE,
|
|
17
|
+
AUTH_SUBTITLE_BY_MODE,
|
|
18
|
+
SUBMIT_LABEL_BY_MODE
|
|
19
|
+
} from "./constants.js";
|
|
20
|
+
import { normalizeEmailAddress, maskEmail } from "./identityHelpers.js";
|
|
21
|
+
import {
|
|
22
|
+
createRememberedAccountHint,
|
|
23
|
+
writeRememberedAccountHint,
|
|
24
|
+
clearRememberedAccountHint
|
|
25
|
+
} from "./rememberedAccountStorage.js";
|
|
26
|
+
|
|
27
|
+
export function useLoginViewState({ placementContext } = {}) {
|
|
28
|
+
const mode = ref(LOGIN_MODE);
|
|
29
|
+
const email = ref("");
|
|
30
|
+
const password = ref("");
|
|
31
|
+
const confirmPassword = ref("");
|
|
32
|
+
const otpCode = ref("");
|
|
33
|
+
const showPassword = ref(false);
|
|
34
|
+
const showConfirmPassword = ref(false);
|
|
35
|
+
const emailTouched = ref(false);
|
|
36
|
+
const passwordTouched = ref(false);
|
|
37
|
+
const confirmPasswordTouched = ref(false);
|
|
38
|
+
const otpCodeTouched = ref(false);
|
|
39
|
+
const submitAttempted = ref(false);
|
|
40
|
+
const rememberAccountOnDevice = ref(true);
|
|
41
|
+
const rememberedAccount = ref(null);
|
|
42
|
+
const useRememberedAccount = ref(false);
|
|
43
|
+
const oauthProviders = ref([]);
|
|
44
|
+
const oauthDefaultProvider = ref("");
|
|
45
|
+
const loading = ref(false);
|
|
46
|
+
const otpRequestPending = ref(false);
|
|
47
|
+
const registerConfirmationResendPending = ref(false);
|
|
48
|
+
const errorMessage = ref("");
|
|
49
|
+
const infoMessage = ref("");
|
|
50
|
+
const pendingEmailConfirmationMessage = ref("");
|
|
51
|
+
const pendingEmailConfirmationAddress = ref("");
|
|
52
|
+
|
|
53
|
+
const isLogin = computed(() => mode.value === LOGIN_MODE);
|
|
54
|
+
const isRegister = computed(() => mode.value === REGISTER_MODE);
|
|
55
|
+
const isForgot = computed(() => mode.value === FORGOT_MODE);
|
|
56
|
+
const isOtp = computed(() => mode.value === OTP_MODE);
|
|
57
|
+
const isEmailConfirmationPending = computed(() => mode.value === EMAIL_CONFIRMATION_MODE);
|
|
58
|
+
const showRememberedAccount = computed(
|
|
59
|
+
() => (isLogin.value || isOtp.value) && useRememberedAccount.value && Boolean(rememberedAccount.value)
|
|
60
|
+
);
|
|
61
|
+
const rememberedAccountDisplayName = computed(() => String(rememberedAccount.value?.displayName || "your account"));
|
|
62
|
+
const rememberedAccountMaskedEmail = computed(() => String(rememberedAccount.value?.maskedEmail || ""));
|
|
63
|
+
const rememberedAccountSwitchLabel = "Use another account";
|
|
64
|
+
const authTitle = computed(() => AUTH_TITLE_BY_MODE[mode.value] || AUTH_TITLE_BY_MODE[LOGIN_MODE]);
|
|
65
|
+
const authSubtitle = computed(() => {
|
|
66
|
+
if (isEmailConfirmationPending.value) {
|
|
67
|
+
const maskedEmail = maskEmail(pendingEmailConfirmationAddress.value);
|
|
68
|
+
if (maskedEmail) {
|
|
69
|
+
return `Open the confirmation link sent to ${maskedEmail} to finish signing in.`;
|
|
70
|
+
}
|
|
71
|
+
return "Open the confirmation link from your inbox to finish signing in.";
|
|
72
|
+
}
|
|
73
|
+
return AUTH_SUBTITLE_BY_MODE[mode.value] || AUTH_SUBTITLE_BY_MODE[LOGIN_MODE];
|
|
74
|
+
});
|
|
75
|
+
const submitLabel = computed(() => SUBMIT_LABEL_BY_MODE[mode.value] || SUBMIT_LABEL_BY_MODE[LOGIN_MODE]);
|
|
76
|
+
const allowedReturnToOrigins = computed(() =>
|
|
77
|
+
resolveAllowedReturnToOriginsFromPlacementContext(placementContext?.value)
|
|
78
|
+
);
|
|
79
|
+
const requestedReturnTo = ref(
|
|
80
|
+
normalizeAuthReturnToPath(
|
|
81
|
+
typeof window === "object" ? new URLSearchParams(window.location.search || "").get("returnTo") : "/",
|
|
82
|
+
"/",
|
|
83
|
+
{
|
|
84
|
+
allowedOrigins: allowedReturnToOrigins.value
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
);
|
|
88
|
+
const mainScreenPath = computed(() => {
|
|
89
|
+
const normalizedReturnTo = normalizeAuthReturnToPath(requestedReturnTo.value, "/", {
|
|
90
|
+
allowedOrigins: allowedReturnToOrigins.value
|
|
91
|
+
});
|
|
92
|
+
const surfaceId = resolveSurfaceIdFromPlacementPathname(placementContext?.value, normalizedReturnTo);
|
|
93
|
+
const rootPath = resolveSurfaceRootPathFromPlacementContext(placementContext?.value, surfaceId || "");
|
|
94
|
+
return normalizeAuthReturnToPath(rootPath, "/", {
|
|
95
|
+
allowedOrigins: allowedReturnToOrigins.value
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
const emailConfirmationMessage = computed(
|
|
99
|
+
() =>
|
|
100
|
+
String(pendingEmailConfirmationMessage.value || "").trim() ||
|
|
101
|
+
"Please confirm your email address. After confirmation, you can sign in."
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
function clearTransientMessages() {
|
|
105
|
+
errorMessage.value = "";
|
|
106
|
+
infoMessage.value = "";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function resetTransientValidationState() {
|
|
110
|
+
submitAttempted.value = false;
|
|
111
|
+
emailTouched.value = false;
|
|
112
|
+
passwordTouched.value = false;
|
|
113
|
+
confirmPasswordTouched.value = false;
|
|
114
|
+
otpCodeTouched.value = false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function clearRememberedAccountState() {
|
|
118
|
+
rememberedAccount.value = null;
|
|
119
|
+
useRememberedAccount.value = false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function resetCredentialFields() {
|
|
123
|
+
password.value = "";
|
|
124
|
+
confirmPassword.value = "";
|
|
125
|
+
otpCode.value = "";
|
|
126
|
+
showPassword.value = false;
|
|
127
|
+
showConfirmPassword.value = false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function resolveNormalizedEmail() {
|
|
131
|
+
return normalizeEmailAddress(email.value);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function applyRememberedAccountHint(hint) {
|
|
135
|
+
if (!hint) {
|
|
136
|
+
clearRememberedAccountState();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
rememberedAccount.value = hint;
|
|
141
|
+
useRememberedAccount.value = true;
|
|
142
|
+
rememberAccountOnDevice.value = true;
|
|
143
|
+
email.value = String(hint.email || "").trim();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function applyRememberedAccountPreference({ email: accountEmail, displayName, shouldRemember } = {}) {
|
|
147
|
+
const rememberedHint = createRememberedAccountHint({
|
|
148
|
+
email: accountEmail,
|
|
149
|
+
displayName,
|
|
150
|
+
lastUsedAt: new Date().toISOString()
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (shouldRemember && rememberedHint) {
|
|
154
|
+
writeRememberedAccountHint(rememberedHint);
|
|
155
|
+
applyRememberedAccountHint(rememberedHint);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
clearRememberedAccountHint();
|
|
160
|
+
clearRememberedAccountState();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function switchAccount() {
|
|
164
|
+
clearRememberedAccountHint();
|
|
165
|
+
clearRememberedAccountState();
|
|
166
|
+
rememberAccountOnDevice.value = false;
|
|
167
|
+
mode.value = LOGIN_MODE;
|
|
168
|
+
email.value = "";
|
|
169
|
+
resetCredentialFields();
|
|
170
|
+
clearTransientMessages();
|
|
171
|
+
resetTransientValidationState();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function switchMode(nextMode) {
|
|
175
|
+
if (nextMode === mode.value) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
mode.value = nextMode;
|
|
180
|
+
resetCredentialFields();
|
|
181
|
+
registerConfirmationResendPending.value = false;
|
|
182
|
+
if (nextMode !== EMAIL_CONFIRMATION_MODE) {
|
|
183
|
+
pendingEmailConfirmationAddress.value = "";
|
|
184
|
+
pendingEmailConfirmationMessage.value = "";
|
|
185
|
+
}
|
|
186
|
+
clearTransientMessages();
|
|
187
|
+
resetTransientValidationState();
|
|
188
|
+
|
|
189
|
+
if (nextMode !== LOGIN_MODE && nextMode !== OTP_MODE) {
|
|
190
|
+
useRememberedAccount.value = false;
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (rememberedAccount.value) {
|
|
195
|
+
useRememberedAccount.value = true;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function enterEmailConfirmationPendingState({ emailAddress = "", message = "" } = {}) {
|
|
200
|
+
switchMode(EMAIL_CONFIRMATION_MODE);
|
|
201
|
+
pendingEmailConfirmationAddress.value = normalizeEmailAddress(emailAddress);
|
|
202
|
+
pendingEmailConfirmationMessage.value = String(message || "").trim();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function goToMainScreen() {
|
|
206
|
+
if (typeof window !== "object" || !window.location || typeof window.location.assign !== "function") {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
window.location.assign(mainScreenPath.value);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
mode,
|
|
214
|
+
email,
|
|
215
|
+
password,
|
|
216
|
+
confirmPassword,
|
|
217
|
+
otpCode,
|
|
218
|
+
showPassword,
|
|
219
|
+
showConfirmPassword,
|
|
220
|
+
emailTouched,
|
|
221
|
+
passwordTouched,
|
|
222
|
+
confirmPasswordTouched,
|
|
223
|
+
otpCodeTouched,
|
|
224
|
+
submitAttempted,
|
|
225
|
+
rememberAccountOnDevice,
|
|
226
|
+
rememberedAccount,
|
|
227
|
+
useRememberedAccount,
|
|
228
|
+
oauthProviders,
|
|
229
|
+
oauthDefaultProvider,
|
|
230
|
+
loading,
|
|
231
|
+
otpRequestPending,
|
|
232
|
+
registerConfirmationResendPending,
|
|
233
|
+
errorMessage,
|
|
234
|
+
infoMessage,
|
|
235
|
+
pendingEmailConfirmationMessage,
|
|
236
|
+
pendingEmailConfirmationAddress,
|
|
237
|
+
isLogin,
|
|
238
|
+
isRegister,
|
|
239
|
+
isForgot,
|
|
240
|
+
isOtp,
|
|
241
|
+
isEmailConfirmationPending,
|
|
242
|
+
showRememberedAccount,
|
|
243
|
+
rememberedAccountDisplayName,
|
|
244
|
+
rememberedAccountMaskedEmail,
|
|
245
|
+
rememberedAccountSwitchLabel,
|
|
246
|
+
authTitle,
|
|
247
|
+
authSubtitle,
|
|
248
|
+
submitLabel,
|
|
249
|
+
allowedReturnToOrigins,
|
|
250
|
+
requestedReturnTo,
|
|
251
|
+
mainScreenPath,
|
|
252
|
+
emailConfirmationMessage,
|
|
253
|
+
resolveNormalizedEmail,
|
|
254
|
+
applyRememberedAccountHint,
|
|
255
|
+
applyRememberedAccountPreference,
|
|
256
|
+
clearTransientMessages,
|
|
257
|
+
switchMode,
|
|
258
|
+
switchAccount,
|
|
259
|
+
enterEmailConfirmationPendingState,
|
|
260
|
+
goToMainScreen
|
|
261
|
+
};
|
|
262
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { computed } from "vue";
|
|
2
|
+
import { authRegisterCommand } from "@jskit-ai/auth-core/shared/commands/authRegisterCommand";
|
|
3
|
+
import { authLoginPasswordCommand } from "@jskit-ai/auth-core/shared/commands/authLoginPasswordCommand";
|
|
4
|
+
import { authLoginOtpRequestCommand } from "@jskit-ai/auth-core/shared/commands/authLoginOtpRequestCommand";
|
|
5
|
+
import { authLoginOtpVerifyCommand } from "@jskit-ai/auth-core/shared/commands/authLoginOtpVerifyCommand";
|
|
6
|
+
import { authPasswordResetRequestCommand } from "@jskit-ai/auth-core/shared/commands/authPasswordResetRequestCommand";
|
|
7
|
+
import {
|
|
8
|
+
validateCommandSection,
|
|
9
|
+
resolveFieldValidationMessage
|
|
10
|
+
} from "./validationHelpers.js";
|
|
11
|
+
|
|
12
|
+
export function useLoginViewValidation({ state } = {}) {
|
|
13
|
+
function resolveFieldErrorMessages({
|
|
14
|
+
shouldValidate,
|
|
15
|
+
commandResource,
|
|
16
|
+
section = "bodyValidator",
|
|
17
|
+
payload,
|
|
18
|
+
fieldName
|
|
19
|
+
} = {}) {
|
|
20
|
+
if (!shouldValidate) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const parsed = validateCommandSection(commandResource, section, payload);
|
|
25
|
+
const message = resolveFieldValidationMessage(parsed, fieldName);
|
|
26
|
+
return message ? [message] : [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const emailErrorMessages = computed(() => {
|
|
30
|
+
const shouldValidate = state.submitAttempted.value || state.emailTouched.value;
|
|
31
|
+
const normalizedEmail = state.resolveNormalizedEmail();
|
|
32
|
+
const command = state.isRegister.value
|
|
33
|
+
? authRegisterCommand
|
|
34
|
+
: state.isForgot.value
|
|
35
|
+
? authPasswordResetRequestCommand
|
|
36
|
+
: state.isOtp.value
|
|
37
|
+
? authLoginOtpRequestCommand
|
|
38
|
+
: authLoginPasswordCommand;
|
|
39
|
+
const payload = state.isRegister.value || state.isLogin.value
|
|
40
|
+
? {
|
|
41
|
+
email: normalizedEmail,
|
|
42
|
+
password: String(state.password.value || "")
|
|
43
|
+
}
|
|
44
|
+
: {
|
|
45
|
+
email: normalizedEmail
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return resolveFieldErrorMessages({
|
|
49
|
+
shouldValidate,
|
|
50
|
+
commandResource: command,
|
|
51
|
+
payload,
|
|
52
|
+
fieldName: "email"
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const passwordErrorMessages = computed(() => {
|
|
57
|
+
const shouldValidate = state.submitAttempted.value || state.passwordTouched.value;
|
|
58
|
+
if (!shouldValidate || state.isForgot.value || state.isOtp.value) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const normalizedEmail = state.resolveNormalizedEmail();
|
|
63
|
+
const command = state.isRegister.value ? authRegisterCommand : authLoginPasswordCommand;
|
|
64
|
+
return resolveFieldErrorMessages({
|
|
65
|
+
shouldValidate: true,
|
|
66
|
+
commandResource: command,
|
|
67
|
+
payload: {
|
|
68
|
+
email: normalizedEmail,
|
|
69
|
+
password: String(state.password.value || "")
|
|
70
|
+
},
|
|
71
|
+
fieldName: "password"
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const confirmPasswordErrorMessages = computed(() => {
|
|
76
|
+
const shouldValidate = state.submitAttempted.value || state.confirmPasswordTouched.value;
|
|
77
|
+
if (!shouldValidate || !state.isRegister.value) {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
if (String(state.confirmPassword.value || "").trim() !== String(state.password.value || "").trim()) {
|
|
81
|
+
return ["Passwords do not match."];
|
|
82
|
+
}
|
|
83
|
+
return [];
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const otpCodeErrorMessages = computed(() => {
|
|
87
|
+
const shouldValidate = state.submitAttempted.value || state.otpCodeTouched.value;
|
|
88
|
+
return resolveFieldErrorMessages({
|
|
89
|
+
shouldValidate: shouldValidate && state.isOtp.value,
|
|
90
|
+
commandResource: authLoginOtpVerifyCommand,
|
|
91
|
+
payload: {
|
|
92
|
+
token: String(state.otpCode.value || "").trim()
|
|
93
|
+
},
|
|
94
|
+
fieldName: "token"
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const canSubmit = computed(() => {
|
|
99
|
+
if (state.isEmailConfirmationPending.value) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
if (state.loading.value) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
if (emailErrorMessages.value.length > 0) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
if (state.isRegister.value || state.isLogin.value) {
|
|
109
|
+
return passwordErrorMessages.value.length < 1 && confirmPasswordErrorMessages.value.length < 1;
|
|
110
|
+
}
|
|
111
|
+
if (state.isOtp.value) {
|
|
112
|
+
return otpCodeErrorMessages.value.length < 1;
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
emailErrorMessages,
|
|
119
|
+
passwordErrorMessages,
|
|
120
|
+
confirmPasswordErrorMessages,
|
|
121
|
+
otpCodeErrorMessages,
|
|
122
|
+
canSubmit
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { validateOperationSection } from "@jskit-ai/http-runtime/shared/validators/operationValidation";
|
|
2
|
+
|
|
3
|
+
function validateCommandSection(commandResource, section, payload) {
|
|
4
|
+
if (!commandResource || !commandResource.operation) {
|
|
5
|
+
return {
|
|
6
|
+
ok: true,
|
|
7
|
+
fieldErrors: {},
|
|
8
|
+
globalErrors: []
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return validateOperationSection({
|
|
13
|
+
operation: commandResource.operation,
|
|
14
|
+
section,
|
|
15
|
+
value: payload
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function resolveValidationMessage(validationResult, fallbackMessage = "Validation failed.") {
|
|
20
|
+
if (!validationResult || validationResult.ok) {
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const fieldErrors = validationResult.fieldErrors && typeof validationResult.fieldErrors === "object"
|
|
25
|
+
? validationResult.fieldErrors
|
|
26
|
+
: {};
|
|
27
|
+
const firstFieldError = Object.values(fieldErrors).find((entry) => String(entry || "").trim().length > 0);
|
|
28
|
+
if (firstFieldError) {
|
|
29
|
+
return String(firstFieldError);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const globalErrors = Array.isArray(validationResult.globalErrors) ? validationResult.globalErrors : [];
|
|
33
|
+
const firstGlobalError = globalErrors.find((entry) => String(entry || "").trim().length > 0);
|
|
34
|
+
if (firstGlobalError) {
|
|
35
|
+
return String(firstGlobalError);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return String(fallbackMessage || "Validation failed.");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveFieldValidationMessage(validationResult, fieldName = "") {
|
|
42
|
+
if (!validationResult || validationResult.ok) {
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const fieldErrors = validationResult.fieldErrors && typeof validationResult.fieldErrors === "object"
|
|
47
|
+
? validationResult.fieldErrors
|
|
48
|
+
: {};
|
|
49
|
+
return String(fieldErrors[fieldName] || "").trim();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function ensureCommandSectionValid(commandResource, section, payload, fallbackMessage) {
|
|
53
|
+
const validation = validateCommandSection(commandResource, section, payload);
|
|
54
|
+
if (validation.ok) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
throw new Error(resolveValidationMessage(validation, fallbackMessage));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export {
|
|
61
|
+
validateCommandSection,
|
|
62
|
+
resolveValidationMessage,
|
|
63
|
+
resolveFieldValidationMessage,
|
|
64
|
+
ensureCommandSectionValid
|
|
65
|
+
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { isTransientQueryError } from "@jskit-ai/kernel/shared/support";
|
|
2
2
|
import { AUTH_PATHS } from "@jskit-ai/auth-core/shared/authPaths";
|
|
3
3
|
import { isExternalLinkTarget } from "@jskit-ai/kernel/shared/support/linkPath";
|
|
4
|
+
import { normalizePathname as normalizeSurfacePathname } from "@jskit-ai/kernel/shared/surface/paths";
|
|
5
|
+
import { createListenerSubscription } from "@jskit-ai/kernel/shared/support/listenerSet";
|
|
4
6
|
|
|
5
7
|
const GLOBAL_GUARD_EVALUATOR_KEY = "__JSKIT_WEB_SHELL_GUARD_EVALUATOR__";
|
|
6
8
|
const AUTH_POLICY_AUTHENTICATED = "authenticated";
|
|
@@ -9,6 +11,25 @@ const DEFAULT_LOGIN_ROUTE = "/auth/login";
|
|
|
9
11
|
const DEFAULT_REFRESH_ON_FOREGROUND = false;
|
|
10
12
|
const DEFAULT_REFRESH_ON_RECONNECT = false;
|
|
11
13
|
const DEFAULT_REALTIME_REFRESH_EVENTS = Object.freeze(["users.bootstrap.changed", "auth.session.changed"]);
|
|
14
|
+
const OAUTH_QUERY_PARAM_RETURN_TO = "returnTo";
|
|
15
|
+
const OAUTH_CALLBACK_PARAM_KEYS = Object.freeze([
|
|
16
|
+
"code",
|
|
17
|
+
"access_token",
|
|
18
|
+
"refresh_token",
|
|
19
|
+
"provider_token",
|
|
20
|
+
"expires_in",
|
|
21
|
+
"expires_at",
|
|
22
|
+
"token_type",
|
|
23
|
+
"state",
|
|
24
|
+
"sb",
|
|
25
|
+
"type",
|
|
26
|
+
"error",
|
|
27
|
+
"error_code",
|
|
28
|
+
"error_description",
|
|
29
|
+
"errorCode",
|
|
30
|
+
"errorDescription",
|
|
31
|
+
"token_hash"
|
|
32
|
+
]);
|
|
12
33
|
const KEEP_PREVIOUS_AUTH_STATE = Symbol("keepPreviousAuthState");
|
|
13
34
|
const DEFAULT_AUTH_STATE = Object.freeze({
|
|
14
35
|
authenticated: false,
|
|
@@ -26,13 +47,11 @@ function asGlobalObject() {
|
|
|
26
47
|
|
|
27
48
|
function normalizePathname(pathname, fallback = "/") {
|
|
28
49
|
const raw = String(pathname || "").trim();
|
|
29
|
-
if (!raw || !raw.startsWith("/")) {
|
|
50
|
+
if (!raw || !raw.startsWith("/") || raw.startsWith("//")) {
|
|
30
51
|
return fallback;
|
|
31
52
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
return raw;
|
|
53
|
+
|
|
54
|
+
return normalizeSurfacePathname(raw);
|
|
36
55
|
}
|
|
37
56
|
|
|
38
57
|
function normalizeLoginRoute(loginRoute, fallback = DEFAULT_LOGIN_ROUTE) {
|
|
@@ -247,6 +266,60 @@ function normalizeRuntimePath(value, fallback) {
|
|
|
247
266
|
return raw || fallback;
|
|
248
267
|
}
|
|
249
268
|
|
|
269
|
+
function hasOAuthCallbackParams({ search = "", hash = "" } = {}) {
|
|
270
|
+
const searchParams = new URLSearchParams(String(search || ""));
|
|
271
|
+
const hashParams = new URLSearchParams(String(hash || "").replace(/^#/, ""));
|
|
272
|
+
return OAUTH_CALLBACK_PARAM_KEYS.some((key) => searchParams.has(key) || hashParams.has(key));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function stripOAuthCallbackParamsFromSearch(search = "") {
|
|
276
|
+
const params = new URLSearchParams(String(search || ""));
|
|
277
|
+
OAUTH_CALLBACK_PARAM_KEYS.forEach((key) => {
|
|
278
|
+
params.delete(key);
|
|
279
|
+
});
|
|
280
|
+
params.delete(OAUTH_QUERY_PARAM_RETURN_TO);
|
|
281
|
+
return params;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function redirectOAuthCallbackToLoginRoute(loginRoute) {
|
|
285
|
+
if (
|
|
286
|
+
typeof window !== "object" ||
|
|
287
|
+
!window ||
|
|
288
|
+
!window.location ||
|
|
289
|
+
typeof window.location.replace !== "function"
|
|
290
|
+
) {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const normalizedLoginRoute = normalizeLoginRoute(loginRoute, DEFAULT_LOGIN_ROUTE);
|
|
295
|
+
if (isExternalLinkTarget(normalizedLoginRoute)) {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const currentPathname = normalizePathname(window.location.pathname || "", "/");
|
|
300
|
+
const loginPathname = normalizePathname(normalizedLoginRoute, DEFAULT_LOGIN_ROUTE);
|
|
301
|
+
if (currentPathname === loginPathname) {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const currentSearch = String(window.location.search || "");
|
|
306
|
+
const currentHash = String(window.location.hash || "");
|
|
307
|
+
if (!hasOAuthCallbackParams({ search: currentSearch, hash: currentHash })) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const returnToParams = stripOAuthCallbackParamsFromSearch(currentSearch);
|
|
312
|
+
const returnToQuery = returnToParams.toString();
|
|
313
|
+
const returnTo = `${currentPathname}${returnToQuery ? `?${returnToQuery}` : ""}`;
|
|
314
|
+
|
|
315
|
+
const nextParams = new URLSearchParams(currentSearch);
|
|
316
|
+
nextParams.set(OAUTH_QUERY_PARAM_RETURN_TO, returnTo);
|
|
317
|
+
const nextQuery = nextParams.toString();
|
|
318
|
+
const nextPath = `${loginPathname}${nextQuery ? `?${nextQuery}` : ""}${currentHash}`;
|
|
319
|
+
window.location.replace(nextPath);
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
|
|
250
323
|
function asEventTarget(value) {
|
|
251
324
|
if (!value || typeof value !== "object") {
|
|
252
325
|
return null;
|
|
@@ -335,6 +408,7 @@ function createAuthGuardRuntime({
|
|
|
335
408
|
let activeRefreshPromise = null;
|
|
336
409
|
let listenersInstalled = false;
|
|
337
410
|
const listeners = new Set();
|
|
411
|
+
const subscribe = createListenerSubscription(listeners);
|
|
338
412
|
|
|
339
413
|
function notifyListeners() {
|
|
340
414
|
for (const listener of listeners) {
|
|
@@ -350,16 +424,6 @@ function createAuthGuardRuntime({
|
|
|
350
424
|
return authState;
|
|
351
425
|
}
|
|
352
426
|
|
|
353
|
-
function subscribe(listener) {
|
|
354
|
-
if (typeof listener !== "function") {
|
|
355
|
-
return () => {};
|
|
356
|
-
}
|
|
357
|
-
listeners.add(listener);
|
|
358
|
-
return () => {
|
|
359
|
-
listeners.delete(listener);
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
|
-
|
|
363
427
|
async function refresh({ sessionPath: nextSessionPath } = {}) {
|
|
364
428
|
currentSessionPath = normalizeRuntimePath(nextSessionPath, currentSessionPath);
|
|
365
429
|
if (activeRefreshPromise) {
|
|
@@ -392,6 +456,10 @@ function createAuthGuardRuntime({
|
|
|
392
456
|
async function initialize({ sessionPath: nextSessionPath, loginRoute: nextLoginRoute } = {}) {
|
|
393
457
|
currentSessionPath = normalizeRuntimePath(nextSessionPath, currentSessionPath);
|
|
394
458
|
currentLoginRoute = normalizeLoginRoute(nextLoginRoute, currentLoginRoute);
|
|
459
|
+
if (redirectOAuthCallbackToLoginRoute(currentLoginRoute)) {
|
|
460
|
+
return authState;
|
|
461
|
+
}
|
|
462
|
+
|
|
395
463
|
installGuardEvaluator({
|
|
396
464
|
loginRoute: currentLoginRoute,
|
|
397
465
|
getAuthState: () => authState
|
|
@@ -1,7 +1,73 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { onMounted } from "vue";
|
|
2
|
+
import { useQueryClient } from "@tanstack/vue-query";
|
|
3
|
+
import { useShellWebErrorRuntime } from "@jskit-ai/shell-web/client/error";
|
|
4
|
+
import { useWebPlacementContext } from "@jskit-ai/shell-web/client/placement";
|
|
5
|
+
import { useLoginViewState } from "../composables/loginView/useLoginViewState.js";
|
|
6
|
+
import { useLoginViewValidation } from "../composables/loginView/useLoginViewValidation.js";
|
|
7
|
+
import { useLoginViewActions } from "../composables/loginView/useLoginViewActions.js";
|
|
2
8
|
|
|
3
9
|
function useLoginView() {
|
|
4
|
-
|
|
10
|
+
const { context: placementContext } = useWebPlacementContext();
|
|
11
|
+
const queryClient = useQueryClient();
|
|
12
|
+
const errorRuntime = useShellWebErrorRuntime();
|
|
13
|
+
|
|
14
|
+
const state = useLoginViewState({ placementContext });
|
|
15
|
+
const validation = useLoginViewValidation({ state });
|
|
16
|
+
const actions = useLoginViewActions({
|
|
17
|
+
state,
|
|
18
|
+
validation,
|
|
19
|
+
queryClient,
|
|
20
|
+
errorRuntime
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
onMounted(actions.initializeOnMounted);
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
authTitle: state.authTitle,
|
|
27
|
+
authSubtitle: state.authSubtitle,
|
|
28
|
+
isForgot: state.isForgot,
|
|
29
|
+
isOtp: state.isOtp,
|
|
30
|
+
isLogin: state.isLogin,
|
|
31
|
+
isRegister: state.isRegister,
|
|
32
|
+
isEmailConfirmationPending: state.isEmailConfirmationPending,
|
|
33
|
+
emailConfirmationMessage: state.emailConfirmationMessage,
|
|
34
|
+
showRememberedAccount: state.showRememberedAccount,
|
|
35
|
+
switchMode: state.switchMode,
|
|
36
|
+
goToMainScreen: state.goToMainScreen,
|
|
37
|
+
submitAuth: actions.submitAuth,
|
|
38
|
+
rememberedAccountDisplayName: state.rememberedAccountDisplayName,
|
|
39
|
+
rememberedAccountMaskedEmail: state.rememberedAccountMaskedEmail,
|
|
40
|
+
rememberedAccountSwitchLabel: state.rememberedAccountSwitchLabel,
|
|
41
|
+
switchAccount: state.switchAccount,
|
|
42
|
+
email: state.email,
|
|
43
|
+
emailErrorMessages: validation.emailErrorMessages,
|
|
44
|
+
emailTouched: state.emailTouched,
|
|
45
|
+
password: state.password,
|
|
46
|
+
showPassword: state.showPassword,
|
|
47
|
+
passwordErrorMessages: validation.passwordErrorMessages,
|
|
48
|
+
passwordTouched: state.passwordTouched,
|
|
49
|
+
confirmPassword: state.confirmPassword,
|
|
50
|
+
showConfirmPassword: state.showConfirmPassword,
|
|
51
|
+
confirmPasswordErrorMessages: validation.confirmPasswordErrorMessages,
|
|
52
|
+
confirmPasswordTouched: state.confirmPasswordTouched,
|
|
53
|
+
otpCode: state.otpCode,
|
|
54
|
+
otpCodeErrorMessages: validation.otpCodeErrorMessages,
|
|
55
|
+
otpCodeTouched: state.otpCodeTouched,
|
|
56
|
+
rememberAccountOnDevice: state.rememberAccountOnDevice,
|
|
57
|
+
otpRequestPending: state.otpRequestPending,
|
|
58
|
+
registerConfirmationResendPending: state.registerConfirmationResendPending,
|
|
59
|
+
requestOtpCode: actions.requestOtpCode,
|
|
60
|
+
resendRegisterConfirmationEmail: actions.resendRegisterConfirmationEmail,
|
|
61
|
+
oauthProviders: state.oauthProviders,
|
|
62
|
+
loading: state.loading,
|
|
63
|
+
oauthProviderIcon: actions.oauthProviderIcon,
|
|
64
|
+
startOAuthSignIn: actions.startOAuthSignIn,
|
|
65
|
+
oauthProviderButtonLabel: actions.oauthProviderButtonLabel,
|
|
66
|
+
errorMessage: state.errorMessage,
|
|
67
|
+
infoMessage: state.infoMessage,
|
|
68
|
+
canSubmit: validation.canSubmit,
|
|
69
|
+
submitLabel: state.submitLabel
|
|
70
|
+
};
|
|
5
71
|
}
|
|
6
72
|
|
|
7
|
-
export { useLoginView
|
|
73
|
+
export { useLoginView };
|