@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.
Files changed (70) hide show
  1. package/README.md +265 -0
  2. package/dist/chunk-33R4URZV.js +59 -0
  3. package/dist/chunk-33R4URZV.js.map +1 -0
  4. package/dist/chunk-64USRLVP.js +85 -0
  5. package/dist/chunk-64USRLVP.js.map +1 -0
  6. package/dist/chunk-WLOFOVCL.js +210 -0
  7. package/dist/chunk-WLOFOVCL.js.map +1 -0
  8. package/dist/chunk-WNMPTDCR.js +73 -0
  9. package/dist/chunk-WNMPTDCR.js.map +1 -0
  10. package/dist/chunk-Z2ZAL7KX.js +9 -0
  11. package/dist/chunk-Z2ZAL7KX.js.map +1 -0
  12. package/dist/chunk-ZQ4XMJH7.js +1 -0
  13. package/dist/chunk-ZQ4XMJH7.js.map +1 -0
  14. package/dist/htlkg/config.js +7 -0
  15. package/dist/htlkg/config.js.map +1 -0
  16. package/dist/htlkg/index.js +7 -0
  17. package/dist/htlkg/index.js.map +1 -0
  18. package/dist/index.js +64 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/middleware/index.js +168 -0
  21. package/dist/middleware/index.js.map +1 -0
  22. package/dist/utils/hydration.js +21 -0
  23. package/dist/utils/hydration.js.map +1 -0
  24. package/dist/utils/index.js +56 -0
  25. package/dist/utils/index.js.map +1 -0
  26. package/dist/utils/ssr.js +21 -0
  27. package/dist/utils/ssr.js.map +1 -0
  28. package/dist/utils/static.js +19 -0
  29. package/dist/utils/static.js.map +1 -0
  30. package/package.json +53 -0
  31. package/src/__mocks__/astro-middleware.ts +19 -0
  32. package/src/__mocks__/virtual-htlkg-config.ts +14 -0
  33. package/src/auth/LoginForm.vue +482 -0
  34. package/src/auth/LoginPage.astro +70 -0
  35. package/src/components/PageHeader.astro +145 -0
  36. package/src/components/Sidebar.astro +157 -0
  37. package/src/components/Topbar.astro +167 -0
  38. package/src/components/index.ts +9 -0
  39. package/src/htlkg/config.test.ts +165 -0
  40. package/src/htlkg/config.ts +242 -0
  41. package/src/htlkg/index.ts +245 -0
  42. package/src/htlkg/virtual-modules.test.ts +158 -0
  43. package/src/htlkg/virtual-modules.ts +81 -0
  44. package/src/index.ts +37 -0
  45. package/src/layouts/AdminLayout.astro +184 -0
  46. package/src/layouts/AuthLayout.astro +164 -0
  47. package/src/layouts/BrandLayout.astro +309 -0
  48. package/src/layouts/DefaultLayout.astro +25 -0
  49. package/src/layouts/PublicLayout.astro +153 -0
  50. package/src/layouts/index.ts +10 -0
  51. package/src/middleware/auth.ts +53 -0
  52. package/src/middleware/index.ts +31 -0
  53. package/src/middleware/route-guards.test.ts +182 -0
  54. package/src/middleware/route-guards.ts +218 -0
  55. package/src/patterns/admin/DetailPage.astro +195 -0
  56. package/src/patterns/admin/FormPage.astro +203 -0
  57. package/src/patterns/admin/ListPage.astro +178 -0
  58. package/src/patterns/admin/index.ts +9 -0
  59. package/src/patterns/brand/ConfigPage.astro +128 -0
  60. package/src/patterns/brand/PortalPage.astro +161 -0
  61. package/src/patterns/brand/index.ts +8 -0
  62. package/src/patterns/index.ts +8 -0
  63. package/src/utils/hydration.test.ts +154 -0
  64. package/src/utils/hydration.ts +151 -0
  65. package/src/utils/index.ts +9 -0
  66. package/src/utils/ssr.test.ts +235 -0
  67. package/src/utils/ssr.ts +139 -0
  68. package/src/utils/static.test.ts +144 -0
  69. package/src/utils/static.ts +144 -0
  70. 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);