@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,309 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Brand Layout
|
|
4
|
+
*
|
|
5
|
+
* Layout for brand-specific pages with brand context and navigation.
|
|
6
|
+
* Includes brand header, navigation, and content area.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```astro
|
|
10
|
+
* <BrandLayout
|
|
11
|
+
* title="WiFi Portal"
|
|
12
|
+
* brandId={brandId}
|
|
13
|
+
* brand={brand}
|
|
14
|
+
* user={user}
|
|
15
|
+
* >
|
|
16
|
+
* <div>Brand-specific content</div>
|
|
17
|
+
* </BrandLayout>
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { AuthUser, Brand } from "@htlkg/core/types";
|
|
22
|
+
|
|
23
|
+
interface NavItem {
|
|
24
|
+
label: string;
|
|
25
|
+
href: string;
|
|
26
|
+
active?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface Props {
|
|
30
|
+
title: string;
|
|
31
|
+
description?: string;
|
|
32
|
+
brandId: number;
|
|
33
|
+
brand: Brand;
|
|
34
|
+
user: AuthUser;
|
|
35
|
+
navItems?: NavItem[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const { title, description, brandId, brand, user, navItems = [] } = Astro.props;
|
|
39
|
+
|
|
40
|
+
// Format user data
|
|
41
|
+
const userData = {
|
|
42
|
+
username: user.username || "User",
|
|
43
|
+
email: user.email || "user@example.com",
|
|
44
|
+
avatar: `https://ui-avatars.com/api/?name=${encodeURIComponent(user.username || "User")}&background=3B82F6&color=fff`,
|
|
45
|
+
};
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
<!doctype html>
|
|
49
|
+
<html lang="en">
|
|
50
|
+
<head>
|
|
51
|
+
<meta charset="UTF-8" />
|
|
52
|
+
<link
|
|
53
|
+
rel="icon"
|
|
54
|
+
href={brand.logo || "https://images.hotelinking.com/login/favicon.ico"}
|
|
55
|
+
/>
|
|
56
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
57
|
+
<title>{title} | {brand.name}</title>
|
|
58
|
+
{description && <meta name="description" content={description} />}
|
|
59
|
+
</head>
|
|
60
|
+
<body>
|
|
61
|
+
<div id="app" class="brand-layout">
|
|
62
|
+
<!-- Brand Header -->
|
|
63
|
+
<header class="brand-header">
|
|
64
|
+
<div class="brand-header-content">
|
|
65
|
+
<div class="brand-info">
|
|
66
|
+
{
|
|
67
|
+
brand.logo && (
|
|
68
|
+
<img src={brand.logo} alt={brand.name} class="brand-logo" />
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
<div>
|
|
72
|
+
<h1 class="brand-name">{brand.name}</h1>
|
|
73
|
+
<p class="brand-timezone">{brand.timezone}</p>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div class="brand-header-right">
|
|
78
|
+
<div class="brand-status" data-status={brand.status}>
|
|
79
|
+
{brand.status}
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div class="user-menu">
|
|
83
|
+
<img
|
|
84
|
+
src={userData.avatar}
|
|
85
|
+
alt={userData.username}
|
|
86
|
+
class="user-avatar"
|
|
87
|
+
/>
|
|
88
|
+
<div class="user-info">
|
|
89
|
+
<div class="user-name">{userData.username}</div>
|
|
90
|
+
<div class="user-email">{userData.email}</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<!-- Navigation -->
|
|
97
|
+
{
|
|
98
|
+
navItems.length > 0 && (
|
|
99
|
+
<nav class="brand-nav">
|
|
100
|
+
{navItems.map((item) => (
|
|
101
|
+
<a
|
|
102
|
+
href={item.href}
|
|
103
|
+
class:list={["nav-item", { active: item.active }]}
|
|
104
|
+
>
|
|
105
|
+
{item.label}
|
|
106
|
+
</a>
|
|
107
|
+
))}
|
|
108
|
+
</nav>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
</header>
|
|
112
|
+
|
|
113
|
+
<!-- Main Content -->
|
|
114
|
+
<main class="brand-main">
|
|
115
|
+
<div class="page-header">
|
|
116
|
+
<div class="page-header-content">
|
|
117
|
+
<div>
|
|
118
|
+
<h2 class="page-title">{title}</h2>
|
|
119
|
+
{description && <p class="page-description">{description}</p>}
|
|
120
|
+
</div>
|
|
121
|
+
<div class="page-actions">
|
|
122
|
+
<slot name="actions" />
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<div class="page-content">
|
|
128
|
+
<slot />
|
|
129
|
+
</div>
|
|
130
|
+
</main>
|
|
131
|
+
</div>
|
|
132
|
+
</body>
|
|
133
|
+
</html>
|
|
134
|
+
|
|
135
|
+
<style>
|
|
136
|
+
.brand-layout {
|
|
137
|
+
min-height: 100vh;
|
|
138
|
+
background: #f9fafb;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.brand-header {
|
|
142
|
+
background: white;
|
|
143
|
+
border-bottom: 1px solid #e5e7eb;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.brand-header-content {
|
|
147
|
+
padding: 1.5rem;
|
|
148
|
+
display: flex;
|
|
149
|
+
justify-content: space-between;
|
|
150
|
+
align-items: center;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.brand-info {
|
|
154
|
+
display: flex;
|
|
155
|
+
align-items: center;
|
|
156
|
+
gap: 1rem;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.brand-logo {
|
|
160
|
+
width: 3rem;
|
|
161
|
+
height: 3rem;
|
|
162
|
+
border-radius: 0.5rem;
|
|
163
|
+
object-fit: cover;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.brand-name {
|
|
167
|
+
font-size: 1.5rem;
|
|
168
|
+
font-weight: 700;
|
|
169
|
+
color: #111827;
|
|
170
|
+
margin: 0;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.brand-timezone {
|
|
174
|
+
font-size: 0.875rem;
|
|
175
|
+
color: #6b7280;
|
|
176
|
+
margin: 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.brand-header-right {
|
|
180
|
+
display: flex;
|
|
181
|
+
align-items: center;
|
|
182
|
+
gap: 1rem;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.brand-status {
|
|
186
|
+
padding: 0.25rem 0.75rem;
|
|
187
|
+
border-radius: 9999px;
|
|
188
|
+
font-size: 0.875rem;
|
|
189
|
+
font-weight: 500;
|
|
190
|
+
text-transform: capitalize;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.brand-status[data-status="active"] {
|
|
194
|
+
background: #d1fae5;
|
|
195
|
+
color: #065f46;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.brand-status[data-status="inactive"] {
|
|
199
|
+
background: #f3f4f6;
|
|
200
|
+
color: #6b7280;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.brand-status[data-status="maintenance"] {
|
|
204
|
+
background: #fef3c7;
|
|
205
|
+
color: #92400e;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.brand-status[data-status="suspended"] {
|
|
209
|
+
background: #fee2e2;
|
|
210
|
+
color: #991b1b;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.user-menu {
|
|
214
|
+
display: flex;
|
|
215
|
+
align-items: center;
|
|
216
|
+
gap: 0.75rem;
|
|
217
|
+
padding: 0.5rem;
|
|
218
|
+
border-radius: 0.5rem;
|
|
219
|
+
cursor: pointer;
|
|
220
|
+
transition: all 0.2s;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.user-menu:hover {
|
|
224
|
+
background: #f9fafb;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.user-avatar {
|
|
228
|
+
width: 2rem;
|
|
229
|
+
height: 2rem;
|
|
230
|
+
border-radius: 9999px;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.user-info {
|
|
234
|
+
display: flex;
|
|
235
|
+
flex-direction: column;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.user-name {
|
|
239
|
+
font-size: 0.875rem;
|
|
240
|
+
font-weight: 500;
|
|
241
|
+
color: #111827;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.user-email {
|
|
245
|
+
font-size: 0.75rem;
|
|
246
|
+
color: #6b7280;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.brand-nav {
|
|
250
|
+
padding: 0 1.5rem;
|
|
251
|
+
display: flex;
|
|
252
|
+
gap: 2rem;
|
|
253
|
+
border-top: 1px solid #e5e7eb;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.nav-item {
|
|
257
|
+
padding: 1rem 0;
|
|
258
|
+
color: #6b7280;
|
|
259
|
+
text-decoration: none;
|
|
260
|
+
font-weight: 500;
|
|
261
|
+
border-bottom: 2px solid transparent;
|
|
262
|
+
transition: all 0.2s;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.nav-item:hover {
|
|
266
|
+
color: #111827;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.nav-item.active {
|
|
270
|
+
color: #2563eb;
|
|
271
|
+
border-bottom-color: #2563eb;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.brand-main {
|
|
275
|
+
max-width: 80rem;
|
|
276
|
+
margin: 0 auto;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.page-header {
|
|
280
|
+
padding: 2rem 1.5rem 1rem;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.page-header-content {
|
|
284
|
+
display: flex;
|
|
285
|
+
justify-content: space-between;
|
|
286
|
+
align-items: flex-start;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.page-title {
|
|
290
|
+
font-size: 1.875rem;
|
|
291
|
+
font-weight: 700;
|
|
292
|
+
color: #111827;
|
|
293
|
+
margin: 0;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.page-description {
|
|
297
|
+
margin-top: 0.5rem;
|
|
298
|
+
color: #6b7280;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.page-actions {
|
|
302
|
+
display: flex;
|
|
303
|
+
gap: 0.75rem;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.page-content {
|
|
307
|
+
padding: 0 1.5rem 2rem;
|
|
308
|
+
}
|
|
309
|
+
</style>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Minimal layout for the login page
|
|
4
|
+
* Provides basic HTML structure without additional styling
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
title?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { title = "Sign In" } = Astro.props;
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<!doctype html>
|
|
15
|
+
<html lang="en">
|
|
16
|
+
<head>
|
|
17
|
+
<meta charset="UTF-8" />
|
|
18
|
+
<link rel="icon" href="https://images.hotelinking.com/login/favicon.ico" />
|
|
19
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
20
|
+
<title>{title}</title>
|
|
21
|
+
</head>
|
|
22
|
+
<body>
|
|
23
|
+
<slot />
|
|
24
|
+
</body>
|
|
25
|
+
</html>
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Public Layout
|
|
4
|
+
*
|
|
5
|
+
* Simple layout for public-facing pages without authentication.
|
|
6
|
+
* Minimal header and footer with clean content area.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```astro
|
|
10
|
+
* <PublicLayout
|
|
11
|
+
* title="Welcome"
|
|
12
|
+
* description="Public page"
|
|
13
|
+
* >
|
|
14
|
+
* <div>Public content</div>
|
|
15
|
+
* </PublicLayout>
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
interface Props {
|
|
20
|
+
title: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
showHeader?: boolean;
|
|
23
|
+
showFooter?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const {
|
|
27
|
+
title,
|
|
28
|
+
description,
|
|
29
|
+
showHeader = true,
|
|
30
|
+
showFooter = true,
|
|
31
|
+
} = Astro.props;
|
|
32
|
+
|
|
33
|
+
const currentYear = new Date().getFullYear();
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
<!doctype html>
|
|
37
|
+
<html lang="en">
|
|
38
|
+
<head>
|
|
39
|
+
<meta charset="UTF-8" />
|
|
40
|
+
<link rel="icon" href="https://images.hotelinking.com/login/favicon.ico" />
|
|
41
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
42
|
+
<title>{title} | Hotelinking</title>
|
|
43
|
+
{description && <meta name="description" content={description} />}
|
|
44
|
+
</head>
|
|
45
|
+
<body>
|
|
46
|
+
<div id="app" class="public-layout">
|
|
47
|
+
{
|
|
48
|
+
showHeader && (
|
|
49
|
+
<header class="public-header">
|
|
50
|
+
<div class="header-content">
|
|
51
|
+
<div class="logo">
|
|
52
|
+
<img
|
|
53
|
+
src="https://images.hotelinking.com/login/logo.png"
|
|
54
|
+
alt="Hotelinking"
|
|
55
|
+
class="logo-image"
|
|
56
|
+
/>
|
|
57
|
+
</div>
|
|
58
|
+
<nav class="header-nav">
|
|
59
|
+
<slot name="nav" />
|
|
60
|
+
</nav>
|
|
61
|
+
</div>
|
|
62
|
+
</header>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
<main class="public-main">
|
|
67
|
+
<slot />
|
|
68
|
+
</main>
|
|
69
|
+
|
|
70
|
+
{
|
|
71
|
+
showFooter && (
|
|
72
|
+
<footer class="public-footer">
|
|
73
|
+
<div class="footer-content">
|
|
74
|
+
<p class="footer-text">
|
|
75
|
+
© {currentYear} Hotelinking. All rights reserved.
|
|
76
|
+
</p>
|
|
77
|
+
<div class="footer-links">
|
|
78
|
+
<slot name="footer-links" />
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</footer>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
</div>
|
|
85
|
+
</body>
|
|
86
|
+
</html>
|
|
87
|
+
|
|
88
|
+
<style>
|
|
89
|
+
.public-layout {
|
|
90
|
+
min-height: 100vh;
|
|
91
|
+
display: flex;
|
|
92
|
+
flex-direction: column;
|
|
93
|
+
background: #ffffff;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.public-header {
|
|
97
|
+
background: white;
|
|
98
|
+
border-bottom: 1px solid #e5e7eb;
|
|
99
|
+
padding: 1rem 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.header-content {
|
|
103
|
+
max-width: 80rem;
|
|
104
|
+
margin: 0 auto;
|
|
105
|
+
padding: 0 1.5rem;
|
|
106
|
+
display: flex;
|
|
107
|
+
justify-content: space-between;
|
|
108
|
+
align-items: center;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.logo-image {
|
|
112
|
+
height: 2rem;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.header-nav {
|
|
116
|
+
display: flex;
|
|
117
|
+
gap: 2rem;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.public-main {
|
|
121
|
+
flex: 1;
|
|
122
|
+
max-width: 80rem;
|
|
123
|
+
width: 100%;
|
|
124
|
+
margin: 0 auto;
|
|
125
|
+
padding: 2rem 1.5rem;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.public-footer {
|
|
129
|
+
background: #f9fafb;
|
|
130
|
+
border-top: 1px solid #e5e7eb;
|
|
131
|
+
padding: 2rem 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.footer-content {
|
|
135
|
+
max-width: 80rem;
|
|
136
|
+
margin: 0 auto;
|
|
137
|
+
padding: 0 1.5rem;
|
|
138
|
+
display: flex;
|
|
139
|
+
justify-content: space-between;
|
|
140
|
+
align-items: center;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.footer-text {
|
|
144
|
+
color: #6b7280;
|
|
145
|
+
font-size: 0.875rem;
|
|
146
|
+
margin: 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.footer-links {
|
|
150
|
+
display: flex;
|
|
151
|
+
gap: 1.5rem;
|
|
152
|
+
}
|
|
153
|
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @htlkg/pages - Layouts
|
|
3
|
+
*
|
|
4
|
+
* Reusable Astro layouts for different page types.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { default as AdminLayout } from './AdminLayout.astro';
|
|
8
|
+
export { default as BrandLayout } from './BrandLayout.astro';
|
|
9
|
+
export { default as PublicLayout } from './PublicLayout.astro';
|
|
10
|
+
export { default as AuthLayout } from './AuthLayout.astro';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication middleware for htlkg integration
|
|
3
|
+
*
|
|
4
|
+
* This middleware:
|
|
5
|
+
* - Retrieves the authenticated user from AWS Amplify
|
|
6
|
+
* - Injects the user into Astro.locals for use in pages and API routes
|
|
7
|
+
* - Handles authentication errors gracefully
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { MiddlewareHandler } from 'astro';
|
|
11
|
+
import { getUser } from '@htlkg/core/auth';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Auth middleware - retrieves authenticated user and injects into locals
|
|
15
|
+
*
|
|
16
|
+
* This middleware runs on every request and attempts to get the current
|
|
17
|
+
* authenticated user from AWS Amplify. If successful, the user is injected
|
|
18
|
+
* into context.locals.user. If authentication fails, locals.user is set to null.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* // In an Astro page
|
|
22
|
+
* const { user } = Astro.locals;
|
|
23
|
+
* if (user) {
|
|
24
|
+
* console.log('Authenticated user:', user.email);
|
|
25
|
+
* }
|
|
26
|
+
*/
|
|
27
|
+
export const authMiddleware: MiddlewareHandler = async (context, next) => {
|
|
28
|
+
const { locals } = context;
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const user = await getUser(context);
|
|
32
|
+
locals.user = user;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
// Set user to null on any authentication error
|
|
35
|
+
locals.user = null;
|
|
36
|
+
|
|
37
|
+
// Log error without exposing sensitive information
|
|
38
|
+
if (error instanceof Error) {
|
|
39
|
+
// Filter out sensitive data from error messages
|
|
40
|
+
const safeMessage = error.message
|
|
41
|
+
.replace(/token[=:]\s*[^\s,}]+/gi, 'token=***')
|
|
42
|
+
.replace(/key[=:]\s*[^\s,}]+/gi, 'key=***')
|
|
43
|
+
.replace(/secret[=:]\s*[^\s,}]+/gi, 'secret=***')
|
|
44
|
+
.replace(/password[=:]\s*[^\s,}]+/gi, 'password=***');
|
|
45
|
+
|
|
46
|
+
console.error('[htlkg Auth] Authentication failed:', safeMessage);
|
|
47
|
+
} else {
|
|
48
|
+
console.error('[htlkg Auth] Authentication failed: Unknown error');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return next();
|
|
53
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Middleware for @htlkg/astro
|
|
3
|
+
*
|
|
4
|
+
* This module exports:
|
|
5
|
+
* - authMiddleware: Retrieves authenticated user and injects into locals
|
|
6
|
+
* - routeGuard: Enforces access control based on route configuration
|
|
7
|
+
* - onRequest: Composed middleware chain (auth + route guard)
|
|
8
|
+
* - Helper functions: requireAuth, requireAdminAccess, requireBrandAccess
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { sequence } from 'astro:middleware';
|
|
12
|
+
import { authMiddleware } from './auth.js';
|
|
13
|
+
import { routeGuard } from './route-guards.js';
|
|
14
|
+
|
|
15
|
+
// Export individual middleware
|
|
16
|
+
export { authMiddleware } from './auth.js';
|
|
17
|
+
export {
|
|
18
|
+
routeGuard,
|
|
19
|
+
requireAuth,
|
|
20
|
+
requireAdminAccess,
|
|
21
|
+
requireBrandAccess,
|
|
22
|
+
} from './route-guards.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Export composed middleware chain
|
|
26
|
+
* Auth middleware runs first to populate locals.user
|
|
27
|
+
* Route guard middleware runs second to enforce access control
|
|
28
|
+
*
|
|
29
|
+
* This is the default export that gets injected by the htlkg integration
|
|
30
|
+
*/
|
|
31
|
+
export const onRequest = sequence(authMiddleware, routeGuard);
|