@htlkg/astro 0.0.1
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/README.md +265 -0
- package/dist/chunk-33R4URZV.js +59 -0
- package/dist/chunk-33R4URZV.js.map +1 -0
- package/dist/chunk-64USRLVP.js +85 -0
- package/dist/chunk-64USRLVP.js.map +1 -0
- package/dist/chunk-WLOFOVCL.js +210 -0
- package/dist/chunk-WLOFOVCL.js.map +1 -0
- package/dist/chunk-WNMPTDCR.js +73 -0
- package/dist/chunk-WNMPTDCR.js.map +1 -0
- package/dist/chunk-Z2ZAL7KX.js +9 -0
- package/dist/chunk-Z2ZAL7KX.js.map +1 -0
- package/dist/chunk-ZQ4XMJH7.js +1 -0
- package/dist/chunk-ZQ4XMJH7.js.map +1 -0
- package/dist/htlkg/config.js +7 -0
- package/dist/htlkg/config.js.map +1 -0
- package/dist/htlkg/index.js +7 -0
- package/dist/htlkg/index.js.map +1 -0
- package/dist/index.js +64 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/index.js +168 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/utils/hydration.js +21 -0
- package/dist/utils/hydration.js.map +1 -0
- package/dist/utils/index.js +56 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/ssr.js +21 -0
- package/dist/utils/ssr.js.map +1 -0
- package/dist/utils/static.js +19 -0
- package/dist/utils/static.js.map +1 -0
- package/package.json +53 -0
- package/src/__mocks__/astro-middleware.ts +19 -0
- package/src/__mocks__/virtual-htlkg-config.ts +14 -0
- package/src/auth/LoginForm.vue +482 -0
- package/src/auth/LoginPage.astro +70 -0
- package/src/components/PageHeader.astro +145 -0
- package/src/components/Sidebar.astro +157 -0
- package/src/components/Topbar.astro +167 -0
- package/src/components/index.ts +9 -0
- package/src/htlkg/config.test.ts +165 -0
- package/src/htlkg/config.ts +242 -0
- package/src/htlkg/index.ts +245 -0
- package/src/htlkg/virtual-modules.test.ts +158 -0
- package/src/htlkg/virtual-modules.ts +81 -0
- package/src/index.ts +37 -0
- package/src/layouts/AdminLayout.astro +184 -0
- package/src/layouts/AuthLayout.astro +164 -0
- package/src/layouts/BrandLayout.astro +309 -0
- package/src/layouts/DefaultLayout.astro +25 -0
- package/src/layouts/PublicLayout.astro +153 -0
- package/src/layouts/index.ts +10 -0
- package/src/middleware/auth.ts +53 -0
- package/src/middleware/index.ts +31 -0
- package/src/middleware/route-guards.test.ts +182 -0
- package/src/middleware/route-guards.ts +218 -0
- package/src/patterns/admin/DetailPage.astro +195 -0
- package/src/patterns/admin/FormPage.astro +203 -0
- package/src/patterns/admin/ListPage.astro +178 -0
- package/src/patterns/admin/index.ts +9 -0
- package/src/patterns/brand/ConfigPage.astro +128 -0
- package/src/patterns/brand/PortalPage.astro +161 -0
- package/src/patterns/brand/index.ts +8 -0
- package/src/patterns/index.ts +8 -0
- package/src/utils/hydration.test.ts +154 -0
- package/src/utils/hydration.ts +151 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/ssr.test.ts +235 -0
- package/src/utils/ssr.ts +139 -0
- package/src/utils/static.test.ts +144 -0
- package/src/utils/static.ts +144 -0
- package/src/vue-app-setup.ts +88 -0
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex min-h-screen flex-col justify-center px-6 py-12 lg:px-8 bg-gradient-to-br from-indigo-500 via-purple-500 to-pink-500">
|
|
3
|
+
<!-- Notification for errors/success messages -->
|
|
4
|
+
<Notification
|
|
5
|
+
v-model:show="showNotification"
|
|
6
|
+
:type="notificationType"
|
|
7
|
+
:title="notificationTitle"
|
|
8
|
+
:message="notificationMessage"
|
|
9
|
+
:fixed="true"
|
|
10
|
+
/>
|
|
11
|
+
|
|
12
|
+
<!-- New Password Modal -->
|
|
13
|
+
<Modal
|
|
14
|
+
v-model:open="showNewPasswordModal"
|
|
15
|
+
modal-name="newPasswordModal"
|
|
16
|
+
title="Set New Password"
|
|
17
|
+
:actions="[{ action: 'setPassword', name: 'Set Password' }]"
|
|
18
|
+
@action="handleNewPasswordAction"
|
|
19
|
+
>
|
|
20
|
+
<p class="mb-4 text-sm text-gray-600">
|
|
21
|
+
You need to set a new password before continuing.
|
|
22
|
+
</p>
|
|
23
|
+
<uiInput
|
|
24
|
+
name="newPassword"
|
|
25
|
+
label="New Password"
|
|
26
|
+
type="password"
|
|
27
|
+
:loading="isLoading"
|
|
28
|
+
:value="newPassword"
|
|
29
|
+
:error="newPasswordError"
|
|
30
|
+
:color="newPasswordError ? 'red' : undefined"
|
|
31
|
+
requiredText="Required"
|
|
32
|
+
@inputChanged="(input) => { newPassword = input.value; newPasswordError = ''; }"
|
|
33
|
+
/>
|
|
34
|
+
<uiInput
|
|
35
|
+
name="confirmPassword"
|
|
36
|
+
label="Confirm Password"
|
|
37
|
+
type="password"
|
|
38
|
+
:loading="isLoading"
|
|
39
|
+
:value="confirmPassword"
|
|
40
|
+
:error="confirmPasswordError"
|
|
41
|
+
:color="confirmPasswordError ? 'red' : undefined"
|
|
42
|
+
requiredText="Required"
|
|
43
|
+
@inputChanged="(input) => { confirmPassword = input.value; confirmPasswordError = ''; }"
|
|
44
|
+
/>
|
|
45
|
+
</Modal>
|
|
46
|
+
|
|
47
|
+
<!-- Password Reset Modal -->
|
|
48
|
+
<Modal
|
|
49
|
+
v-model:open="showResetModal"
|
|
50
|
+
modal-name="resetPasswordModal"
|
|
51
|
+
title="Reset Your Password"
|
|
52
|
+
:actions="[{ action: 'reset', name: 'Send Reset Link' }]"
|
|
53
|
+
@action="handleResetModalAction"
|
|
54
|
+
>
|
|
55
|
+
<p class="mb-4 text-sm text-gray-600">
|
|
56
|
+
Enter your email address. If there's an account associated with this email,
|
|
57
|
+
we'll send you instructions to reset your password.
|
|
58
|
+
</p>
|
|
59
|
+
<uiInput
|
|
60
|
+
name="resetEmail"
|
|
61
|
+
label="Email Address"
|
|
62
|
+
type="email"
|
|
63
|
+
:loading="isLoading"
|
|
64
|
+
:value="resetEmail"
|
|
65
|
+
@inputChanged="(input) => resetEmail = input.value"
|
|
66
|
+
/>
|
|
67
|
+
</Modal>
|
|
68
|
+
|
|
69
|
+
<!-- MFA Code Modal -->
|
|
70
|
+
<Modal
|
|
71
|
+
v-model:open="showMfaModal"
|
|
72
|
+
modal-name="mfaModal"
|
|
73
|
+
title="Two-Factor Authentication"
|
|
74
|
+
:actions="[{ action: 'verify', name: 'Verify Code' }]"
|
|
75
|
+
@action="handleMfaAction"
|
|
76
|
+
>
|
|
77
|
+
<p class="mb-4 text-sm text-gray-600">
|
|
78
|
+
Enter the verification code from your authenticator app.
|
|
79
|
+
</p>
|
|
80
|
+
<uiInput
|
|
81
|
+
name="mfaCode"
|
|
82
|
+
label="Verification Code"
|
|
83
|
+
type="text"
|
|
84
|
+
:loading="isLoading"
|
|
85
|
+
:value="mfaCode"
|
|
86
|
+
:error="mfaError"
|
|
87
|
+
:color="mfaError ? 'red' : undefined"
|
|
88
|
+
placeholder="000000"
|
|
89
|
+
requiredText="Required"
|
|
90
|
+
@inputChanged="(input) => { mfaCode = input.value; mfaError = ''; }"
|
|
91
|
+
/>
|
|
92
|
+
</Modal>
|
|
93
|
+
|
|
94
|
+
<!-- Login Form Container -->
|
|
95
|
+
<div class="bg-white rounded-2xl shadow-2xl p-8 sm:mx-auto sm:w-full sm:max-w-md">
|
|
96
|
+
<!-- Logo and Header -->
|
|
97
|
+
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
|
|
98
|
+
<img
|
|
99
|
+
v-if="logo"
|
|
100
|
+
class="mx-auto h-16"
|
|
101
|
+
:src="logo"
|
|
102
|
+
alt="Logo"
|
|
103
|
+
/>
|
|
104
|
+
<h2 v-if="title" class="mt-6 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
|
|
105
|
+
{{ title }}
|
|
106
|
+
</h2>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<!-- Login Form -->
|
|
110
|
+
<div class="mt-10">
|
|
111
|
+
<form class="space-y-6" @submit.prevent="handleLogin">
|
|
112
|
+
<uiInput
|
|
113
|
+
name="username"
|
|
114
|
+
:loading="isLoading"
|
|
115
|
+
label="Email Address"
|
|
116
|
+
type="email"
|
|
117
|
+
placeholder="Enter your email address"
|
|
118
|
+
:value="username"
|
|
119
|
+
:error="emailError"
|
|
120
|
+
:color="emailError ? 'red' : undefined"
|
|
121
|
+
requiredText="Required"
|
|
122
|
+
@inputChanged="handleEmailChange"
|
|
123
|
+
/>
|
|
124
|
+
|
|
125
|
+
<uiInput
|
|
126
|
+
name="password"
|
|
127
|
+
:loading="isLoading"
|
|
128
|
+
label="Password"
|
|
129
|
+
type="password"
|
|
130
|
+
placeholder="Enter your password"
|
|
131
|
+
:value="password"
|
|
132
|
+
:error="passwordError"
|
|
133
|
+
:color="passwordError ? 'red' : undefined"
|
|
134
|
+
requiredText="Required"
|
|
135
|
+
@inputChanged="handlePasswordChange"
|
|
136
|
+
/>
|
|
137
|
+
|
|
138
|
+
<div class="flex flex-col sm:flex-row justify-between items-center gap-4">
|
|
139
|
+
<uiButton
|
|
140
|
+
:loading="isLoading"
|
|
141
|
+
:disabled="!username || !password || isLoading"
|
|
142
|
+
@click="handleLogin"
|
|
143
|
+
>
|
|
144
|
+
{{ isLoading ? 'Signing in...' : 'Sign In' }}
|
|
145
|
+
</uiButton>
|
|
146
|
+
|
|
147
|
+
<p class="text-sm text-gray-500">
|
|
148
|
+
Forgot your password?
|
|
149
|
+
<span
|
|
150
|
+
@click="showResetModal = true"
|
|
151
|
+
class="text-indigo-600 hover:text-indigo-500 underline cursor-pointer font-semibold"
|
|
152
|
+
>
|
|
153
|
+
Reset it
|
|
154
|
+
</span>
|
|
155
|
+
</p>
|
|
156
|
+
</div>
|
|
157
|
+
</form>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</template>
|
|
162
|
+
|
|
163
|
+
<script setup lang="ts">
|
|
164
|
+
import { ref, onMounted } from "vue";
|
|
165
|
+
import { signIn, confirmSignIn, resetPassword, getCurrentUser, fetchAuthSession } from "aws-amplify/auth";
|
|
166
|
+
import { uiInput, uiButton } from "@hotelinking/ui";
|
|
167
|
+
import { Modal, Notification } from "@htlkg/components";
|
|
168
|
+
|
|
169
|
+
interface Props {
|
|
170
|
+
redirectUrl?: string;
|
|
171
|
+
logo?: string;
|
|
172
|
+
title?: string;
|
|
173
|
+
initialError?: string | null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
177
|
+
redirectUrl: "/",
|
|
178
|
+
logo: "https://images.hotelinking.com/ui/WiFiBot.svg",
|
|
179
|
+
title: "Sign In",
|
|
180
|
+
initialError: null,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Form state
|
|
184
|
+
const username = ref("");
|
|
185
|
+
const password = ref("");
|
|
186
|
+
const isLoading = ref(false);
|
|
187
|
+
const emailError = ref("");
|
|
188
|
+
const passwordError = ref("");
|
|
189
|
+
|
|
190
|
+
// Notification state
|
|
191
|
+
const showNotification = ref(false);
|
|
192
|
+
const notificationType = ref<"success" | "danger" | "warning" | "info">("danger");
|
|
193
|
+
const notificationTitle = ref("");
|
|
194
|
+
const notificationMessage = ref("");
|
|
195
|
+
|
|
196
|
+
// New password modal state
|
|
197
|
+
const showNewPasswordModal = ref(false);
|
|
198
|
+
const newPassword = ref("");
|
|
199
|
+
const confirmPassword = ref("");
|
|
200
|
+
const newPasswordError = ref("");
|
|
201
|
+
const confirmPasswordError = ref("");
|
|
202
|
+
|
|
203
|
+
// Reset password modal state
|
|
204
|
+
const showResetModal = ref(false);
|
|
205
|
+
const resetEmail = ref("");
|
|
206
|
+
|
|
207
|
+
// MFA modal state
|
|
208
|
+
const showMfaModal = ref(false);
|
|
209
|
+
const mfaCode = ref("");
|
|
210
|
+
const mfaError = ref("");
|
|
211
|
+
|
|
212
|
+
// Store sign-in result for MFA flow
|
|
213
|
+
let pendingSignInResult: any = null;
|
|
214
|
+
|
|
215
|
+
onMounted(() => {
|
|
216
|
+
if (props.initialError) {
|
|
217
|
+
showErrorNotification("Authentication Error", props.initialError);
|
|
218
|
+
}
|
|
219
|
+
checkAuthStatus();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
async function checkAuthStatus() {
|
|
223
|
+
try {
|
|
224
|
+
await getCurrentUser();
|
|
225
|
+
const session = await fetchAuthSession();
|
|
226
|
+
if (session.tokens?.accessToken) {
|
|
227
|
+
window.location.href = props.redirectUrl;
|
|
228
|
+
}
|
|
229
|
+
} catch {
|
|
230
|
+
// User not authenticated, stay on login page
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Notification helpers
|
|
235
|
+
const showErrorNotification = (title: string, message: string) => {
|
|
236
|
+
notificationType.value = "danger";
|
|
237
|
+
notificationTitle.value = title;
|
|
238
|
+
notificationMessage.value = message;
|
|
239
|
+
showNotification.value = true;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const showSuccessNotification = (title: string, message: string) => {
|
|
243
|
+
notificationType.value = "success";
|
|
244
|
+
notificationTitle.value = title;
|
|
245
|
+
notificationMessage.value = message;
|
|
246
|
+
showNotification.value = true;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// Input handlers
|
|
250
|
+
const handleEmailChange = (input: { value?: string; inputValue?: string }) => {
|
|
251
|
+
username.value = input.value || input.inputValue || "";
|
|
252
|
+
emailError.value = "";
|
|
253
|
+
showNotification.value = false;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const handlePasswordChange = (input: { value?: string; inputValue?: string }) => {
|
|
257
|
+
password.value = input.value || input.inputValue || "";
|
|
258
|
+
passwordError.value = "";
|
|
259
|
+
showNotification.value = false;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// Login handler
|
|
263
|
+
async function handleLogin() {
|
|
264
|
+
emailError.value = "";
|
|
265
|
+
passwordError.value = "";
|
|
266
|
+
|
|
267
|
+
if (!username.value) {
|
|
268
|
+
emailError.value = "Email is required";
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (!password.value) {
|
|
272
|
+
passwordError.value = "Password is required";
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
277
|
+
if (!emailRegex.test(username.value)) {
|
|
278
|
+
emailError.value = "Please enter a valid email address";
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
isLoading.value = true;
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
const result = await signIn({
|
|
286
|
+
username: username.value,
|
|
287
|
+
password: password.value,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
if (result.isSignedIn) {
|
|
291
|
+
const session = await fetchAuthSession();
|
|
292
|
+
if (session.tokens?.accessToken) {
|
|
293
|
+
window.location.href = props.redirectUrl;
|
|
294
|
+
}
|
|
295
|
+
} else if (result.nextStep) {
|
|
296
|
+
handleNextStep(result);
|
|
297
|
+
}
|
|
298
|
+
} catch (err: any) {
|
|
299
|
+
handleAuthError(err);
|
|
300
|
+
} finally {
|
|
301
|
+
isLoading.value = false;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Handle authentication next steps
|
|
306
|
+
function handleNextStep(result: any) {
|
|
307
|
+
pendingSignInResult = result;
|
|
308
|
+
|
|
309
|
+
switch (result.nextStep.signInStep) {
|
|
310
|
+
case "CONFIRM_SIGN_IN_WITH_NEW_PASSWORD_REQUIRED":
|
|
311
|
+
showNewPasswordModal.value = true;
|
|
312
|
+
break;
|
|
313
|
+
case "CONFIRM_SIGN_IN_WITH_TOTP_CODE":
|
|
314
|
+
case "CONFIRM_SIGN_IN_WITH_SMS_CODE":
|
|
315
|
+
showMfaModal.value = true;
|
|
316
|
+
break;
|
|
317
|
+
case "CONFIRM_SIGN_IN_WITH_CUSTOM_CHALLENGE":
|
|
318
|
+
showErrorNotification(
|
|
319
|
+
"Additional Verification",
|
|
320
|
+
"Custom challenge required. Please contact support."
|
|
321
|
+
);
|
|
322
|
+
break;
|
|
323
|
+
default:
|
|
324
|
+
showErrorNotification(
|
|
325
|
+
"Additional Step Required",
|
|
326
|
+
`${result.nextStep.signInStep}`
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// New password modal handler
|
|
332
|
+
async function handleNewPasswordAction(event: { action: string }) {
|
|
333
|
+
if (event.action === "close") {
|
|
334
|
+
showNewPasswordModal.value = false;
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (event.action === "setPassword") {
|
|
339
|
+
newPasswordError.value = "";
|
|
340
|
+
confirmPasswordError.value = "";
|
|
341
|
+
|
|
342
|
+
if (!newPassword.value) {
|
|
343
|
+
newPasswordError.value = "Password is required";
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (newPassword.value.length < 8) {
|
|
347
|
+
newPasswordError.value = "Password must be at least 8 characters";
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (newPassword.value !== confirmPassword.value) {
|
|
351
|
+
confirmPasswordError.value = "Passwords do not match";
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
isLoading.value = true;
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
const result = await confirmSignIn({
|
|
359
|
+
challengeResponse: newPassword.value,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
if (result.isSignedIn) {
|
|
363
|
+
showNewPasswordModal.value = false;
|
|
364
|
+
showSuccessNotification("Password Updated", "Your password has been set successfully.");
|
|
365
|
+
const session = await fetchAuthSession();
|
|
366
|
+
if (session.tokens?.accessToken) {
|
|
367
|
+
setTimeout(() => {
|
|
368
|
+
window.location.href = props.redirectUrl;
|
|
369
|
+
}, 1000);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
} catch (err: any) {
|
|
373
|
+
handleAuthError(err);
|
|
374
|
+
} finally {
|
|
375
|
+
isLoading.value = false;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Reset password modal handler
|
|
381
|
+
async function handleResetModalAction(event: { action: string }) {
|
|
382
|
+
if (event.action === "close") {
|
|
383
|
+
showResetModal.value = false;
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (event.action === "reset") {
|
|
388
|
+
if (!resetEmail.value) {
|
|
389
|
+
showErrorNotification("Email Required", "Please enter your email address.");
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
isLoading.value = true;
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
await resetPassword({ username: resetEmail.value });
|
|
397
|
+
showResetModal.value = false;
|
|
398
|
+
showSuccessNotification(
|
|
399
|
+
"Reset Email Sent",
|
|
400
|
+
"Check your email for password reset instructions."
|
|
401
|
+
);
|
|
402
|
+
} catch (err: any) {
|
|
403
|
+
handleAuthError(err);
|
|
404
|
+
} finally {
|
|
405
|
+
isLoading.value = false;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// MFA modal handler
|
|
411
|
+
async function handleMfaAction(event: { action: string }) {
|
|
412
|
+
if (event.action === "close") {
|
|
413
|
+
showMfaModal.value = false;
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (event.action === "verify") {
|
|
418
|
+
mfaError.value = "";
|
|
419
|
+
|
|
420
|
+
if (!mfaCode.value) {
|
|
421
|
+
mfaError.value = "Verification code is required";
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
isLoading.value = true;
|
|
426
|
+
|
|
427
|
+
try {
|
|
428
|
+
const result = await confirmSignIn({
|
|
429
|
+
challengeResponse: mfaCode.value,
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
if (result.isSignedIn) {
|
|
433
|
+
showMfaModal.value = false;
|
|
434
|
+
showSuccessNotification("Verified", "Authentication successful.");
|
|
435
|
+
const session = await fetchAuthSession();
|
|
436
|
+
if (session.tokens?.accessToken) {
|
|
437
|
+
setTimeout(() => {
|
|
438
|
+
window.location.href = props.redirectUrl;
|
|
439
|
+
}, 1000);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
} catch (err: any) {
|
|
443
|
+
mfaError.value = "Invalid verification code";
|
|
444
|
+
handleAuthError(err);
|
|
445
|
+
} finally {
|
|
446
|
+
isLoading.value = false;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Error handler
|
|
452
|
+
function handleAuthError(err: any) {
|
|
453
|
+
if (err.name === "NotAuthorizedException") {
|
|
454
|
+
showErrorNotification(
|
|
455
|
+
"Authentication Failed",
|
|
456
|
+
"Invalid email or password. Please check your credentials and try again."
|
|
457
|
+
);
|
|
458
|
+
} else if (err.name === "UserNotConfirmedException") {
|
|
459
|
+
showErrorNotification(
|
|
460
|
+
"Account Not Confirmed",
|
|
461
|
+
"Please confirm your email address before signing in."
|
|
462
|
+
);
|
|
463
|
+
} else if (err.name === "PasswordResetRequiredException") {
|
|
464
|
+
showErrorNotification(
|
|
465
|
+
"Password Reset Required",
|
|
466
|
+
"Please reset your password to continue."
|
|
467
|
+
);
|
|
468
|
+
} else if (err.name === "UserNotFoundException") {
|
|
469
|
+
showErrorNotification(
|
|
470
|
+
"User Not Found",
|
|
471
|
+
"No account found with this email address."
|
|
472
|
+
);
|
|
473
|
+
} else if (err.name === "TooManyRequestsException") {
|
|
474
|
+
showErrorNotification(
|
|
475
|
+
"Too Many Attempts",
|
|
476
|
+
"Too many login attempts. Please wait a moment and try again."
|
|
477
|
+
);
|
|
478
|
+
} else {
|
|
479
|
+
showErrorNotification("Error", err.message || "An unexpected error occurred.");
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
</script>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Default Login Page for htlkg integration
|
|
4
|
+
*
|
|
5
|
+
* This page is automatically injected by the htlkg integration when
|
|
6
|
+
* loginPage is enabled in the configuration.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - AWS Amplify authentication with Cognito
|
|
10
|
+
* - New password / reset password flow
|
|
11
|
+
* - MFA support
|
|
12
|
+
* - Error notifications using @hotelinking/ui
|
|
13
|
+
* - No custom CSS - uses @hotelinking/ui components
|
|
14
|
+
* - Redirects if user is already authenticated
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import DefaultLayout from "../layouts/DefaultLayout.astro";
|
|
18
|
+
import LoginForm from "./LoginForm.vue";
|
|
19
|
+
|
|
20
|
+
export const prerender = false;
|
|
21
|
+
|
|
22
|
+
// Get configuration from virtual module
|
|
23
|
+
const { loginPageConfig } = await import("virtual:htlkg-config");
|
|
24
|
+
|
|
25
|
+
const {
|
|
26
|
+
logo = "https://images.hotelinking.com/ui/WiFiBot.svg",
|
|
27
|
+
title = "Sign In",
|
|
28
|
+
redirectUrl = "/admin",
|
|
29
|
+
} = loginPageConfig || {};
|
|
30
|
+
|
|
31
|
+
// Check if user is already authenticated (populated by auth middleware)
|
|
32
|
+
const user = Astro.locals.user;
|
|
33
|
+
|
|
34
|
+
if (user) {
|
|
35
|
+
// User is already logged in, redirect to configured URL or query param
|
|
36
|
+
return Astro.redirect(Astro.url.searchParams.get("redirect") || redirectUrl);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Get query parameters for error handling
|
|
40
|
+
const { searchParams } = Astro.url;
|
|
41
|
+
const error = searchParams.get("error");
|
|
42
|
+
|
|
43
|
+
// Error messages
|
|
44
|
+
const errorMessages: Record<string, string> = {
|
|
45
|
+
not_authenticated: "Please sign in to continue",
|
|
46
|
+
admin_required: "Admin access required",
|
|
47
|
+
access_denied: "Access denied",
|
|
48
|
+
invalid_brand: "Invalid brand",
|
|
49
|
+
server_error: "An error occurred. Please try again.",
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const errorMessage = error ? errorMessages[error] || "An error occurred" : null;
|
|
53
|
+
const finalRedirectUrl = searchParams.get("redirect") || redirectUrl;
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
<DefaultLayout title={title}>
|
|
57
|
+
<div
|
|
58
|
+
class="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8"
|
|
59
|
+
>
|
|
60
|
+
<div class="max-w-md w-full space-y-8">
|
|
61
|
+
<LoginForm
|
|
62
|
+
client:only="vue"
|
|
63
|
+
redirectUrl={finalRedirectUrl}
|
|
64
|
+
logo={logo}
|
|
65
|
+
title={title}
|
|
66
|
+
initialError={errorMessage}
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</DefaultLayout>
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Page Header Component
|
|
4
|
+
*
|
|
5
|
+
* Reusable page header with title, description, and action buttons.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```astro
|
|
9
|
+
* <PageHeader
|
|
10
|
+
* title="Dashboard"
|
|
11
|
+
* description="Overview of your account"
|
|
12
|
+
* >
|
|
13
|
+
* <button slot="actions">Add New</button>
|
|
14
|
+
* </PageHeader>
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
interface BreadcrumbItem {
|
|
19
|
+
label: string;
|
|
20
|
+
href?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface Props {
|
|
24
|
+
title: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
breadcrumbs?: BreadcrumbItem[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { title, description, breadcrumbs = [] } = Astro.props;
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
<div class="page-header">
|
|
33
|
+
{
|
|
34
|
+
breadcrumbs.length > 0 && (
|
|
35
|
+
<nav class="breadcrumbs">
|
|
36
|
+
{breadcrumbs.map((crumb, index) => (
|
|
37
|
+
<>
|
|
38
|
+
{crumb.href ? (
|
|
39
|
+
<a href={crumb.href} class="breadcrumb-link">
|
|
40
|
+
{crumb.label}
|
|
41
|
+
</a>
|
|
42
|
+
) : (
|
|
43
|
+
<span class="breadcrumb-current">{crumb.label}</span>
|
|
44
|
+
)}
|
|
45
|
+
{index < breadcrumbs.length - 1 && (
|
|
46
|
+
<span class="breadcrumb-separator">/</span>
|
|
47
|
+
)}
|
|
48
|
+
</>
|
|
49
|
+
))}
|
|
50
|
+
</nav>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
<div class="header-content">
|
|
55
|
+
<div class="header-text">
|
|
56
|
+
<h1 class="header-title">{title}</h1>
|
|
57
|
+
{description && <p class="header-description">{description}</p>}
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div class="header-actions">
|
|
61
|
+
<slot name="actions" />
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<style>
|
|
67
|
+
.page-header {
|
|
68
|
+
margin-bottom: 2rem;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.breadcrumbs {
|
|
72
|
+
display: flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
gap: 0.5rem;
|
|
75
|
+
margin-bottom: 1rem;
|
|
76
|
+
font-size: 0.875rem;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.breadcrumb-link {
|
|
80
|
+
color: #6b7280;
|
|
81
|
+
text-decoration: none;
|
|
82
|
+
transition: color 0.2s;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.breadcrumb-link:hover {
|
|
86
|
+
color: #111827;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.breadcrumb-current {
|
|
90
|
+
color: #111827;
|
|
91
|
+
font-weight: 500;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.breadcrumb-separator {
|
|
95
|
+
color: #d1d5db;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.header-content {
|
|
99
|
+
display: flex;
|
|
100
|
+
justify-content: space-between;
|
|
101
|
+
align-items: flex-start;
|
|
102
|
+
gap: 2rem;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.header-text {
|
|
106
|
+
flex: 1;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.header-title {
|
|
110
|
+
font-size: 1.875rem;
|
|
111
|
+
font-weight: 700;
|
|
112
|
+
color: #111827;
|
|
113
|
+
margin: 0;
|
|
114
|
+
line-height: 1.2;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.header-description {
|
|
118
|
+
margin-top: 0.5rem;
|
|
119
|
+
color: #6b7280;
|
|
120
|
+
font-size: 1rem;
|
|
121
|
+
line-height: 1.5;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.header-actions {
|
|
125
|
+
display: flex;
|
|
126
|
+
gap: 0.75rem;
|
|
127
|
+
flex-shrink: 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* Responsive */
|
|
131
|
+
@media (max-width: 768px) {
|
|
132
|
+
.header-content {
|
|
133
|
+
flex-direction: column;
|
|
134
|
+
align-items: stretch;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.header-title {
|
|
138
|
+
font-size: 1.5rem;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.header-actions {
|
|
142
|
+
width: 100%;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
</style>
|