@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.
- 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
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<v-chip color="primary" size="small" label>Secure</v-chip>
|
|
13
13
|
</div>
|
|
14
14
|
|
|
15
|
-
<div v-if="!isForgot && !isOtp" class="mode-switch d-flex ga-2 pa-1 mb-5">
|
|
15
|
+
<div v-if="!isForgot && !isOtp && !isEmailConfirmationPending" class="mode-switch d-flex ga-2 pa-1 mb-5">
|
|
16
16
|
<v-btn
|
|
17
17
|
data-testid="auth-mode-sign-in"
|
|
18
18
|
class="text-none"
|
|
@@ -35,140 +35,166 @@
|
|
|
35
35
|
</div>
|
|
36
36
|
|
|
37
37
|
<v-form @submit.prevent="submitAuth" novalidate>
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
<p class="remembered-title">Welcome back, {{ rememberedAccountDisplayName }}</p>
|
|
44
|
-
<p class="remembered-email">{{ rememberedAccountMaskedEmail }}</p>
|
|
38
|
+
<template v-if="isEmailConfirmationPending">
|
|
39
|
+
<div class="email-confirmation-state mb-5">
|
|
40
|
+
<p class="email-confirmation-message mb-0">
|
|
41
|
+
{{ emailConfirmationMessage }}
|
|
42
|
+
</p>
|
|
45
43
|
</div>
|
|
46
|
-
<v-btn variant="text" color="secondary" class="text-none" @click="switchAccount">
|
|
47
|
-
{{ rememberedAccountSwitchLabel }}
|
|
48
|
-
</v-btn>
|
|
49
|
-
</div>
|
|
50
|
-
|
|
51
|
-
<v-text-field
|
|
52
|
-
v-model="email"
|
|
53
|
-
label="Email"
|
|
54
|
-
variant="outlined"
|
|
55
|
-
density="comfortable"
|
|
56
|
-
type="email"
|
|
57
|
-
autocomplete="email"
|
|
58
|
-
:error-messages="emailErrorMessages"
|
|
59
|
-
@blur="emailTouched = true"
|
|
60
|
-
class="mb-3"
|
|
61
|
-
/>
|
|
62
|
-
|
|
63
|
-
<v-text-field
|
|
64
|
-
v-if="!isForgot && !isOtp"
|
|
65
|
-
v-model="password"
|
|
66
|
-
label="Password"
|
|
67
|
-
:type="showPassword ? 'text' : 'password'"
|
|
68
|
-
variant="outlined"
|
|
69
|
-
density="comfortable"
|
|
70
|
-
:autocomplete="isRegister ? 'new-password' : 'current-password'"
|
|
71
|
-
:error-messages="passwordErrorMessages"
|
|
72
|
-
:append-inner-icon="showPassword ? '$eyeOff' : '$eye'"
|
|
73
|
-
@click:append-inner="showPassword = !showPassword"
|
|
74
|
-
@blur="passwordTouched = true"
|
|
75
|
-
class="mb-3"
|
|
76
|
-
/>
|
|
77
|
-
|
|
78
|
-
<v-text-field
|
|
79
|
-
v-if="isRegister"
|
|
80
|
-
v-model="confirmPassword"
|
|
81
|
-
label="Confirm password"
|
|
82
|
-
:type="showConfirmPassword ? 'text' : 'password'"
|
|
83
|
-
variant="outlined"
|
|
84
|
-
density="comfortable"
|
|
85
|
-
autocomplete="new-password"
|
|
86
|
-
:error-messages="confirmPasswordErrorMessages"
|
|
87
|
-
:append-inner-icon="showConfirmPassword ? '$eyeOff' : '$eye'"
|
|
88
|
-
@click:append-inner="showConfirmPassword = !showConfirmPassword"
|
|
89
|
-
@blur="confirmPasswordTouched = true"
|
|
90
|
-
class="mb-3"
|
|
91
|
-
/>
|
|
92
|
-
|
|
93
|
-
<v-text-field
|
|
94
|
-
v-if="isOtp"
|
|
95
|
-
v-model="otpCode"
|
|
96
|
-
label="One-time code"
|
|
97
|
-
variant="outlined"
|
|
98
|
-
density="comfortable"
|
|
99
|
-
autocomplete="one-time-code"
|
|
100
|
-
:error-messages="otpCodeErrorMessages"
|
|
101
|
-
@blur="otpCodeTouched = true"
|
|
102
|
-
class="mb-3"
|
|
103
|
-
/>
|
|
104
|
-
|
|
105
|
-
<div v-if="isLogin" class="aux-links d-flex justify-end mb-4">
|
|
106
|
-
<v-btn variant="text" color="secondary" @click="switchMode('forgot')">Forgot password?</v-btn>
|
|
107
|
-
<v-btn variant="text" color="secondary" @click="switchMode('otp')">Use one-time code</v-btn>
|
|
108
|
-
</div>
|
|
109
44
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
45
|
+
<div class="switch-row d-flex align-center justify-space-between ga-3">
|
|
46
|
+
<v-btn class="text-none" variant="tonal" color="primary" @click="goToMainScreen">
|
|
47
|
+
Go to main screen
|
|
48
|
+
</v-btn>
|
|
49
|
+
<v-btn
|
|
50
|
+
class="text-none"
|
|
51
|
+
variant="outlined"
|
|
52
|
+
color="secondary"
|
|
53
|
+
:loading="registerConfirmationResendPending"
|
|
54
|
+
@click="resendRegisterConfirmationEmail"
|
|
55
|
+
>
|
|
56
|
+
Resend confirmation email
|
|
57
|
+
</v-btn>
|
|
58
|
+
<v-btn variant="text" color="secondary" @click="switchMode('login')">Back to sign in</v-btn>
|
|
59
|
+
</div>
|
|
60
|
+
</template>
|
|
118
61
|
|
|
119
|
-
<
|
|
120
|
-
<
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
color="secondary"
|
|
124
|
-
:loading="otpRequestPending"
|
|
125
|
-
@click="requestOtpCode"
|
|
62
|
+
<template v-else>
|
|
63
|
+
<div
|
|
64
|
+
v-if="showRememberedAccount"
|
|
65
|
+
class="remembered-account d-flex align-center justify-space-between ga-3 mb-4"
|
|
126
66
|
>
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
67
|
+
<div class="remembered-copy flex-grow-1">
|
|
68
|
+
<p class="remembered-title">Welcome back, {{ rememberedAccountDisplayName }}</p>
|
|
69
|
+
<p class="remembered-email">{{ rememberedAccountMaskedEmail }}</p>
|
|
70
|
+
</div>
|
|
71
|
+
<v-btn variant="text" color="secondary" class="text-none" @click="switchAccount">
|
|
72
|
+
{{ rememberedAccountSwitchLabel }}
|
|
73
|
+
</v-btn>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<v-text-field
|
|
77
|
+
v-model="email"
|
|
78
|
+
label="Email"
|
|
79
|
+
variant="outlined"
|
|
80
|
+
density="comfortable"
|
|
81
|
+
type="email"
|
|
82
|
+
autocomplete="email"
|
|
83
|
+
:error-messages="emailErrorMessages"
|
|
84
|
+
@blur="emailTouched = true"
|
|
85
|
+
class="mb-3"
|
|
86
|
+
/>
|
|
87
|
+
|
|
88
|
+
<v-text-field
|
|
89
|
+
v-if="!isForgot && !isOtp"
|
|
90
|
+
v-model="password"
|
|
91
|
+
label="Password"
|
|
92
|
+
:type="showPassword ? 'text' : 'password'"
|
|
93
|
+
variant="outlined"
|
|
94
|
+
density="comfortable"
|
|
95
|
+
:autocomplete="isRegister ? 'new-password' : 'current-password'"
|
|
96
|
+
:error-messages="passwordErrorMessages"
|
|
97
|
+
:append-inner-icon="showPassword ? mdiEyeOff : mdiEye"
|
|
98
|
+
@click:append-inner="togglePasswordVisibility"
|
|
99
|
+
@blur="passwordTouched = true"
|
|
100
|
+
class="mb-3"
|
|
101
|
+
/>
|
|
102
|
+
|
|
103
|
+
<v-text-field
|
|
104
|
+
v-if="isRegister"
|
|
105
|
+
v-model="confirmPassword"
|
|
106
|
+
label="Confirm password"
|
|
107
|
+
:type="showConfirmPassword ? 'text' : 'password'"
|
|
108
|
+
variant="outlined"
|
|
109
|
+
density="comfortable"
|
|
110
|
+
autocomplete="new-password"
|
|
111
|
+
:error-messages="confirmPasswordErrorMessages"
|
|
112
|
+
:append-inner-icon="showConfirmPassword ? mdiEyeOff : mdiEye"
|
|
113
|
+
@click:append-inner="toggleConfirmPasswordVisibility"
|
|
114
|
+
@blur="confirmPasswordTouched = true"
|
|
115
|
+
class="mb-3"
|
|
116
|
+
/>
|
|
117
|
+
|
|
118
|
+
<v-text-field
|
|
119
|
+
v-if="isOtp"
|
|
120
|
+
v-model="otpCode"
|
|
121
|
+
label="One-time code"
|
|
122
|
+
variant="outlined"
|
|
123
|
+
density="comfortable"
|
|
124
|
+
autocomplete="one-time-code"
|
|
125
|
+
:error-messages="otpCodeErrorMessages"
|
|
126
|
+
@blur="otpCodeTouched = true"
|
|
127
|
+
class="mb-3"
|
|
128
|
+
/>
|
|
130
129
|
|
|
131
|
-
|
|
130
|
+
<div v-if="isLogin" class="aux-links d-flex justify-end mb-4">
|
|
131
|
+
<v-btn variant="text" color="secondary" @click="switchMode('forgot')">Forgot password?</v-btn>
|
|
132
|
+
<v-btn variant="text" color="secondary" @click="switchMode('otp')">Use one-time code</v-btn>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<v-checkbox
|
|
136
|
+
v-if="isLogin || isOtp"
|
|
137
|
+
v-model="rememberAccountOnDevice"
|
|
138
|
+
label="Remember this account on this device"
|
|
139
|
+
density="compact"
|
|
140
|
+
hide-details
|
|
141
|
+
class="mb-4"
|
|
142
|
+
/>
|
|
143
|
+
|
|
144
|
+
<div v-if="isOtp" class="aux-links d-flex justify-end mb-4">
|
|
145
|
+
<v-btn
|
|
146
|
+
type="button"
|
|
147
|
+
variant="tonal"
|
|
148
|
+
color="secondary"
|
|
149
|
+
:loading="otpRequestPending"
|
|
150
|
+
@click="requestOtpCode"
|
|
151
|
+
>
|
|
152
|
+
Send one-time code
|
|
153
|
+
</v-btn>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<div v-if="isLogin || isRegister" class="oauth-actions d-grid ga-2 mb-4">
|
|
157
|
+
<v-btn
|
|
158
|
+
v-for="provider in oauthProviders"
|
|
159
|
+
:key="provider.id"
|
|
160
|
+
block
|
|
161
|
+
variant="outlined"
|
|
162
|
+
color="secondary"
|
|
163
|
+
:disabled="loading"
|
|
164
|
+
:prepend-icon="oauthProviderIcon(provider)"
|
|
165
|
+
class="text-none oauth-provider-button"
|
|
166
|
+
@click="startOAuthSignIn(provider.id)"
|
|
167
|
+
>
|
|
168
|
+
{{ oauthProviderButtonLabel(provider) }}
|
|
169
|
+
</v-btn>
|
|
170
|
+
</div>
|
|
132
171
|
<v-btn
|
|
133
|
-
|
|
134
|
-
:key="provider.id"
|
|
172
|
+
data-testid="auth-submit"
|
|
135
173
|
block
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
:
|
|
139
|
-
:
|
|
140
|
-
|
|
141
|
-
@click="startOAuthSignIn(provider.id)"
|
|
174
|
+
color="primary"
|
|
175
|
+
size="large"
|
|
176
|
+
:loading="loading"
|
|
177
|
+
:disabled="!canSubmit"
|
|
178
|
+
type="submit"
|
|
142
179
|
>
|
|
143
|
-
{{
|
|
180
|
+
{{ submitLabel }}
|
|
144
181
|
</v-btn>
|
|
145
|
-
</div>
|
|
146
|
-
<v-btn
|
|
147
|
-
data-testid="auth-submit"
|
|
148
|
-
block
|
|
149
|
-
color="primary"
|
|
150
|
-
size="large"
|
|
151
|
-
:loading="loading"
|
|
152
|
-
:disabled="!canSubmit"
|
|
153
|
-
type="submit"
|
|
154
|
-
>
|
|
155
|
-
{{ submitLabel }}
|
|
156
|
-
</v-btn>
|
|
157
182
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
183
|
+
<div v-if="isRegister" class="switch-row mt-4 d-flex align-center justify-space-between ga-3">
|
|
184
|
+
<span class="text-medium-emphasis">Already have an account?</span>
|
|
185
|
+
<v-btn variant="text" color="secondary" @click="switchMode('login')">Back to sign in</v-btn>
|
|
186
|
+
</div>
|
|
162
187
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
188
|
+
<div v-else-if="isForgot" class="switch-row mt-4 d-flex align-center justify-space-between ga-3">
|
|
189
|
+
<span class="text-medium-emphasis">Remembered your password?</span>
|
|
190
|
+
<v-btn variant="text" color="secondary" @click="switchMode('login')">Back to sign in</v-btn>
|
|
191
|
+
</div>
|
|
167
192
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
193
|
+
<div v-else-if="isOtp" class="switch-row mt-4 d-flex align-center justify-space-between ga-3">
|
|
194
|
+
<span class="text-medium-emphasis">Want to use another method?</span>
|
|
195
|
+
<v-btn variant="text" color="secondary" @click="switchMode('login')">Back to sign in</v-btn>
|
|
196
|
+
</div>
|
|
197
|
+
</template>
|
|
172
198
|
</v-form>
|
|
173
199
|
</v-card-text>
|
|
174
200
|
</v-card>
|
|
@@ -177,7 +203,31 @@
|
|
|
177
203
|
</template>
|
|
178
204
|
|
|
179
205
|
<script setup>
|
|
180
|
-
import {
|
|
206
|
+
import { onMounted } from "vue";
|
|
207
|
+
import { useQueryClient } from "@tanstack/vue-query";
|
|
208
|
+
import { mdiEye, mdiEyeOff } from "@mdi/js";
|
|
209
|
+
import { useShellWebErrorRuntime } from "@jskit-ai/shell-web/client/error";
|
|
210
|
+
import { useWebPlacementContext } from "@jskit-ai/shell-web/client/placement";
|
|
211
|
+
import { useLoginViewState } from "../composables/loginView/useLoginViewState.js";
|
|
212
|
+
import { useLoginViewValidation } from "../composables/loginView/useLoginViewValidation.js";
|
|
213
|
+
import { useLoginViewActions } from "../composables/loginView/useLoginViewActions.js";
|
|
214
|
+
|
|
215
|
+
const {
|
|
216
|
+
context: placementContext
|
|
217
|
+
} = useWebPlacementContext();
|
|
218
|
+
const queryClient = useQueryClient();
|
|
219
|
+
const errorRuntime = useShellWebErrorRuntime();
|
|
220
|
+
|
|
221
|
+
const state = useLoginViewState({ placementContext });
|
|
222
|
+
const validation = useLoginViewValidation({ state });
|
|
223
|
+
const actions = useLoginViewActions({
|
|
224
|
+
state,
|
|
225
|
+
validation,
|
|
226
|
+
queryClient,
|
|
227
|
+
errorRuntime
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
onMounted(actions.initializeOnMounted);
|
|
181
231
|
|
|
182
232
|
const {
|
|
183
233
|
authTitle,
|
|
@@ -186,38 +236,57 @@ const {
|
|
|
186
236
|
isOtp,
|
|
187
237
|
isLogin,
|
|
188
238
|
isRegister,
|
|
239
|
+
isEmailConfirmationPending,
|
|
240
|
+
emailConfirmationMessage,
|
|
189
241
|
showRememberedAccount,
|
|
190
242
|
switchMode,
|
|
191
|
-
|
|
243
|
+
goToMainScreen,
|
|
192
244
|
rememberedAccountDisplayName,
|
|
193
245
|
rememberedAccountMaskedEmail,
|
|
194
246
|
rememberedAccountSwitchLabel,
|
|
195
247
|
switchAccount,
|
|
196
248
|
email,
|
|
197
|
-
emailErrorMessages,
|
|
198
249
|
emailTouched,
|
|
199
250
|
password,
|
|
200
251
|
showPassword,
|
|
201
|
-
passwordErrorMessages,
|
|
202
252
|
passwordTouched,
|
|
203
253
|
confirmPassword,
|
|
204
254
|
showConfirmPassword,
|
|
205
|
-
confirmPasswordErrorMessages,
|
|
206
255
|
confirmPasswordTouched,
|
|
207
256
|
otpCode,
|
|
208
|
-
otpCodeErrorMessages,
|
|
209
257
|
otpCodeTouched,
|
|
210
258
|
rememberAccountOnDevice,
|
|
211
259
|
otpRequestPending,
|
|
212
|
-
|
|
260
|
+
registerConfirmationResendPending,
|
|
213
261
|
oauthProviders,
|
|
214
262
|
loading,
|
|
263
|
+
submitLabel
|
|
264
|
+
} = state;
|
|
265
|
+
|
|
266
|
+
const {
|
|
267
|
+
emailErrorMessages,
|
|
268
|
+
passwordErrorMessages,
|
|
269
|
+
confirmPasswordErrorMessages,
|
|
270
|
+
otpCodeErrorMessages,
|
|
271
|
+
canSubmit
|
|
272
|
+
} = validation;
|
|
273
|
+
|
|
274
|
+
const {
|
|
275
|
+
submitAuth,
|
|
276
|
+
requestOtpCode,
|
|
277
|
+
resendRegisterConfirmationEmail,
|
|
215
278
|
oauthProviderIcon,
|
|
216
279
|
startOAuthSignIn,
|
|
217
|
-
oauthProviderButtonLabel
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
280
|
+
oauthProviderButtonLabel
|
|
281
|
+
} = actions;
|
|
282
|
+
|
|
283
|
+
function togglePasswordVisibility() {
|
|
284
|
+
showPassword.value = !showPassword.value;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function toggleConfirmPasswordVisibility() {
|
|
288
|
+
showConfirmPassword.value = !showConfirmPassword.value;
|
|
289
|
+
}
|
|
221
290
|
</script>
|
|
222
291
|
|
|
223
292
|
<style scoped>
|
|
@@ -251,6 +320,18 @@ const {
|
|
|
251
320
|
background-color: rgba(57, 84, 71, 0.08);
|
|
252
321
|
}
|
|
253
322
|
|
|
323
|
+
.email-confirmation-state {
|
|
324
|
+
border-radius: 12px;
|
|
325
|
+
border: 1px solid rgba(57, 84, 71, 0.2);
|
|
326
|
+
background: rgba(57, 84, 71, 0.07);
|
|
327
|
+
padding: 16px;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.email-confirmation-message {
|
|
331
|
+
color: #1d2c24;
|
|
332
|
+
line-height: 1.5;
|
|
333
|
+
}
|
|
334
|
+
|
|
254
335
|
.remembered-account {
|
|
255
336
|
border-radius: 12px;
|
|
256
337
|
border: 1px solid rgba(57, 84, 71, 0.2);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const AUTH_ACTION_IDS = Object.freeze({
|
|
2
2
|
REGISTER: "auth.register",
|
|
3
|
+
REGISTER_CONFIRMATION_RESEND: "auth.register.confirmation.resend",
|
|
3
4
|
LOGIN_PASSWORD: "auth.login.password",
|
|
4
5
|
LOGIN_OTP_REQUEST: "auth.login.otp.request",
|
|
5
6
|
LOGIN_OTP_VERIFY: "auth.login.otp.verify",
|
|
@@ -35,6 +35,12 @@ class AuthController {
|
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
async resendRegisterConfirmation(request, reply) {
|
|
39
|
+
const payload = request.body || {};
|
|
40
|
+
const result = await this.service.resendRegisterConfirmation(request, payload);
|
|
41
|
+
reply.code(200).send(result);
|
|
42
|
+
}
|
|
43
|
+
|
|
38
44
|
async login(request, reply) {
|
|
39
45
|
const payload = request.body || {};
|
|
40
46
|
const result = await this.service.login(request, payload);
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { withStandardErrorResponses } from "@jskit-ai/http-runtime/shared/validators/errorResponses";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
2
|
+
import {
|
|
3
|
+
authRegisterCommand,
|
|
4
|
+
authRegisterConfirmationResendCommand,
|
|
5
|
+
authLoginPasswordCommand,
|
|
6
|
+
authLoginOtpRequestCommand,
|
|
7
|
+
authLoginOtpVerifyCommand,
|
|
8
|
+
authLoginOAuthStartCommand,
|
|
9
|
+
authLoginOAuthCompleteCommand,
|
|
10
|
+
authPasswordResetRequestCommand,
|
|
11
|
+
authPasswordRecoveryCompleteCommand,
|
|
12
|
+
authPasswordResetCommand,
|
|
13
|
+
authLogoutCommand,
|
|
14
|
+
authSessionReadCommand
|
|
15
|
+
} from "@jskit-ai/auth-core/shared/commands";
|
|
13
16
|
import { AUTH_PATHS } from "@jskit-ai/auth-core/shared/authPaths";
|
|
14
17
|
|
|
15
18
|
function buildRoutes(controller) {
|
|
@@ -41,6 +44,27 @@ function buildRoutes(controller) {
|
|
|
41
44
|
},
|
|
42
45
|
handler: handler("register")
|
|
43
46
|
},
|
|
47
|
+
{
|
|
48
|
+
path: AUTH_PATHS.REGISTER_CONFIRMATION_RESEND,
|
|
49
|
+
method: "POST",
|
|
50
|
+
auth: "public",
|
|
51
|
+
meta: {
|
|
52
|
+
tags: ["auth"],
|
|
53
|
+
summary: "Resend sign-up email confirmation"
|
|
54
|
+
},
|
|
55
|
+
bodyValidator: authRegisterConfirmationResendCommand.operation.bodyValidator,
|
|
56
|
+
responseValidators: withStandardErrorResponses(
|
|
57
|
+
{
|
|
58
|
+
200: authRegisterConfirmationResendCommand.operation.responseValidator
|
|
59
|
+
},
|
|
60
|
+
{ includeValidation400: true }
|
|
61
|
+
),
|
|
62
|
+
rateLimit: {
|
|
63
|
+
max: 5,
|
|
64
|
+
timeWindow: "1 minute"
|
|
65
|
+
},
|
|
66
|
+
handler: handler("resendRegisterConfirmation")
|
|
67
|
+
},
|
|
44
68
|
{
|
|
45
69
|
path: AUTH_PATHS.LOGIN,
|
|
46
70
|
method: "POST",
|
|
@@ -19,6 +19,13 @@ class AuthWebService {
|
|
|
19
19
|
});
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
async resendRegisterConfirmation(request, payload) {
|
|
23
|
+
return request.executeAction({
|
|
24
|
+
actionId: AUTH_ACTION_IDS.REGISTER_CONFIRMATION_RESEND,
|
|
25
|
+
input: payload
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
22
29
|
async login(request, payload) {
|
|
23
30
|
return request.executeAction({
|
|
24
31
|
actionId: AUTH_ACTION_IDS.LOGIN_PASSWORD,
|
|
@@ -105,6 +105,50 @@ test("auth guard runtime keeps previous auth state on transient refresh failure"
|
|
|
105
105
|
assert.equal(placementRuntime.setCalls.length, setCallCountBeforeTransientFailure);
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
+
test("auth guard runtime redirects callback hashes on non-login routes to /auth/login", async () => {
|
|
109
|
+
const originalWindow = globalThis.window;
|
|
110
|
+
let redirectedTo = "";
|
|
111
|
+
globalThis.window = {
|
|
112
|
+
location: {
|
|
113
|
+
pathname: "/home",
|
|
114
|
+
search: "",
|
|
115
|
+
hash: "#access_token=access&refresh_token=refresh",
|
|
116
|
+
replace(target) {
|
|
117
|
+
redirectedTo = String(target || "");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const placementRuntime = createPlacementRuntimeStub();
|
|
124
|
+
let fetchCalls = 0;
|
|
125
|
+
const runtime = createAuthGuardRuntime({
|
|
126
|
+
placementRuntime,
|
|
127
|
+
fetchImplementation: async () => {
|
|
128
|
+
fetchCalls += 1;
|
|
129
|
+
return {
|
|
130
|
+
ok: true,
|
|
131
|
+
async json() {
|
|
132
|
+
return {
|
|
133
|
+
authenticated: false
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const state = await runtime.initialize();
|
|
141
|
+
assert.equal(state.authenticated, false);
|
|
142
|
+
assert.equal(fetchCalls, 0);
|
|
143
|
+
assert.equal(
|
|
144
|
+
redirectedTo,
|
|
145
|
+
"/auth/login?returnTo=%2Fhome#access_token=access&refresh_token=refresh"
|
|
146
|
+
);
|
|
147
|
+
} finally {
|
|
148
|
+
globalThis.window = originalWindow;
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
108
152
|
test("auth guard runtime only updates placement auth context", async () => {
|
|
109
153
|
const placementRuntime = createPlacementRuntimeStub({
|
|
110
154
|
user: {
|
|
@@ -51,13 +51,18 @@ test("auth-web removes legacy client wrapper modules", () => {
|
|
|
51
51
|
}
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
-
test("auth-web runtime/useLoginView
|
|
54
|
+
test("auth-web runtime/useLoginView composes login view state, validation, and actions", () => {
|
|
55
55
|
const runtimeUseLoginViewPath = fileURLToPath(new URL("../src/client/runtime/useLoginView.js", import.meta.url));
|
|
56
56
|
const runtimeUseLoginViewSource = readFileSync(runtimeUseLoginViewPath, "utf8");
|
|
57
57
|
|
|
58
|
-
assert.match(runtimeUseLoginViewSource, /import\s+\{\s*
|
|
59
|
-
assert.match(runtimeUseLoginViewSource, /
|
|
60
|
-
assert.match(runtimeUseLoginViewSource, /
|
|
58
|
+
assert.match(runtimeUseLoginViewSource, /import\s+\{\s*useLoginViewState\s*\}\s+from\s+"..\/composables\/loginView\/useLoginViewState\.js";/);
|
|
59
|
+
assert.match(runtimeUseLoginViewSource, /import\s+\{\s*useLoginViewValidation\s*\}\s+from\s+"..\/composables\/loginView\/useLoginViewValidation\.js";/);
|
|
60
|
+
assert.match(runtimeUseLoginViewSource, /import\s+\{\s*useLoginViewActions\s*\}\s+from\s+"..\/composables\/loginView\/useLoginViewActions\.js";/);
|
|
61
|
+
assert.match(runtimeUseLoginViewSource, /const\s+state\s*=\s*useLoginViewState\(/);
|
|
62
|
+
assert.match(runtimeUseLoginViewSource, /const\s+validation\s*=\s*useLoginViewValidation\(\s*\{\s*state\s*\}\s*\)/);
|
|
63
|
+
assert.match(runtimeUseLoginViewSource, /const\s+actions\s*=\s*useLoginViewActions\(/);
|
|
64
|
+
assert.doesNotMatch(runtimeUseLoginViewSource, /useDefaultLoginView/);
|
|
65
|
+
assert.match(runtimeUseLoginViewSource, /export\s+\{\s*useLoginView\s*\};/);
|
|
61
66
|
});
|
|
62
67
|
|
|
63
68
|
test("auth-web package exports only minimal client runtime/view subpaths", () => {
|
|
@@ -56,6 +56,12 @@ test("auth route provider registers routes and executes login/logout handlers",
|
|
|
56
56
|
app.instance("authService", authService);
|
|
57
57
|
app.instance("actionExecutor", {
|
|
58
58
|
async execute({ actionId }) {
|
|
59
|
+
if (actionId === "auth.register.confirmation.resend") {
|
|
60
|
+
return {
|
|
61
|
+
ok: true,
|
|
62
|
+
message: "If an account exists for that email, a confirmation email has been sent."
|
|
63
|
+
};
|
|
64
|
+
}
|
|
59
65
|
if (actionId === "auth.login.password") {
|
|
60
66
|
return {
|
|
61
67
|
session: { access_token: "a", refresh_token: "r" },
|
|
@@ -88,6 +94,15 @@ test("auth route provider registers routes and executes login/logout handlers",
|
|
|
88
94
|
assert.equal(loginReply.statusCode, 200);
|
|
89
95
|
assert.equal(loginReply.payload.username, "Ada");
|
|
90
96
|
|
|
97
|
+
const resendConfirmationRoute = fastify.routes.find(
|
|
98
|
+
(route) => route.method === "POST" && route.url === "/api/register/confirmation/resend"
|
|
99
|
+
);
|
|
100
|
+
assert.ok(resendConfirmationRoute);
|
|
101
|
+
const resendConfirmationReply = createReplyStub();
|
|
102
|
+
await resendConfirmationRoute.handler({ body: { email: "ada@example.com" } }, resendConfirmationReply);
|
|
103
|
+
assert.equal(resendConfirmationReply.statusCode, 200);
|
|
104
|
+
assert.equal(resendConfirmationReply.payload.ok, true);
|
|
105
|
+
|
|
91
106
|
const logoutRoute = fastify.routes.find((route) => route.method === "POST" && route.url === "/api/logout");
|
|
92
107
|
assert.ok(logoutRoute);
|
|
93
108
|
const logoutReply = createReplyStub();
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { resolveRegisterCompletionState } from "../src/client/composables/loginView/registerCompletion.js";
|
|
4
|
+
|
|
5
|
+
test("resolveRegisterCompletionState requires no immediate session when email confirmation is required", () => {
|
|
6
|
+
const result = resolveRegisterCompletionState({
|
|
7
|
+
ok: true,
|
|
8
|
+
requiresEmailConfirmation: true,
|
|
9
|
+
message: "Custom confirmation message"
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
assert.deepEqual(result, {
|
|
13
|
+
shouldCompleteLogin: false,
|
|
14
|
+
message: "Custom confirmation message"
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("resolveRegisterCompletionState falls back to default confirmation message", () => {
|
|
19
|
+
const result = resolveRegisterCompletionState({
|
|
20
|
+
ok: true,
|
|
21
|
+
requiresEmailConfirmation: true
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
assert.deepEqual(result, {
|
|
25
|
+
shouldCompleteLogin: false,
|
|
26
|
+
message: "Check your email to confirm the account before logging in."
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("resolveRegisterCompletionState completes login when confirmation is not required", () => {
|
|
31
|
+
const result = resolveRegisterCompletionState({
|
|
32
|
+
ok: true,
|
|
33
|
+
requiresEmailConfirmation: false
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
assert.deepEqual(result, {
|
|
37
|
+
shouldCompleteLogin: true,
|
|
38
|
+
message: ""
|
|
39
|
+
});
|
|
40
|
+
});
|