@htlkg/astro 0.0.1 → 0.0.3

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 (41) hide show
  1. package/README.md +24 -8
  2. package/dist/chunk-2GML443T.js +273 -0
  3. package/dist/chunk-2GML443T.js.map +1 -0
  4. package/dist/{chunk-Z2ZAL7KX.js → chunk-UBF5F2RG.js} +1 -1
  5. package/dist/{chunk-Z2ZAL7KX.js.map → chunk-UBF5F2RG.js.map} +1 -1
  6. package/dist/chunk-XOY5BM3N.js +151 -0
  7. package/dist/chunk-XOY5BM3N.js.map +1 -0
  8. package/dist/htlkg/config.js +1 -1
  9. package/dist/htlkg/index.js +1 -1
  10. package/dist/index.js +126 -14
  11. package/dist/index.js.map +1 -1
  12. package/dist/middleware/index.js +27 -28
  13. package/dist/middleware/index.js.map +1 -1
  14. package/dist/utils/index.js +31 -12
  15. package/dist/vue-app-setup.js +47 -0
  16. package/dist/vue-app-setup.js.map +1 -0
  17. package/package.json +60 -26
  18. package/src/auth/auth.md +77 -0
  19. package/src/components/Island.astro +56 -0
  20. package/src/components/components.md +79 -0
  21. package/src/factories/createListPage.ts +290 -0
  22. package/src/factories/index.ts +16 -0
  23. package/src/htlkg/config.ts +10 -0
  24. package/src/htlkg/htlkg.md +63 -0
  25. package/src/htlkg/index.ts +49 -157
  26. package/src/index.ts +3 -0
  27. package/src/layouts/AdminLayout.astro +103 -92
  28. package/src/layouts/layouts.md +87 -0
  29. package/src/middleware/auth.ts +42 -0
  30. package/src/middleware/middleware.md +82 -0
  31. package/src/middleware/route-guards.ts +4 -28
  32. package/src/patterns/patterns.md +104 -0
  33. package/src/utils/filters.ts +320 -0
  34. package/src/utils/index.ts +8 -2
  35. package/src/utils/params.ts +260 -0
  36. package/src/utils/utils.md +86 -0
  37. package/src/vue-app-setup.ts +21 -28
  38. package/dist/chunk-WLOFOVCL.js +0 -210
  39. package/dist/chunk-WLOFOVCL.js.map +0 -1
  40. package/dist/chunk-ZQ4XMJH7.js +0 -1
  41. package/dist/chunk-ZQ4XMJH7.js.map +0 -1
@@ -0,0 +1,82 @@
1
+ # Middleware Module
2
+
3
+ Authentication middleware and route guards for Astro.
4
+
5
+ ## Installation
6
+
7
+ ```typescript
8
+ import {
9
+ requireAuth,
10
+ requireAdminAccess,
11
+ requireBrandAccess,
12
+ } from '@htlkg/astro/middleware';
13
+ ```
14
+
15
+ ## Route Guards
16
+
17
+ ### requireAuth
18
+
19
+ Require authenticated user. Redirects to login if not authenticated.
20
+
21
+ ```astro
22
+ ---
23
+ import { requireAuth } from '@htlkg/astro/middleware';
24
+
25
+ export const prerender = false;
26
+
27
+ const { user } = await requireAuth(Astro);
28
+ ---
29
+
30
+ <h1>Welcome, {user.username}</h1>
31
+ ```
32
+
33
+ ### requireAdminAccess
34
+
35
+ Require admin role.
36
+
37
+ ```astro
38
+ ---
39
+ import { requireAdminAccess } from '@htlkg/astro/middleware';
40
+
41
+ export const prerender = false;
42
+
43
+ const { user } = await requireAdminAccess(Astro);
44
+ ---
45
+
46
+ <AdminDashboard user={user} />
47
+ ```
48
+
49
+ ### requireBrandAccess
50
+
51
+ Require access to a specific brand.
52
+
53
+ ```astro
54
+ ---
55
+ import { requireBrandAccess } from '@htlkg/astro/middleware';
56
+
57
+ export const prerender = false;
58
+
59
+ const { brandId } = Astro.params;
60
+ const { user, brand } = await requireBrandAccess(Astro, brandId);
61
+ ---
62
+
63
+ <BrandDashboard brand={brand} user={user} />
64
+ ```
65
+
66
+ ## Custom Login URL
67
+
68
+ ```typescript
69
+ await requireAuth(Astro, '/custom-login');
70
+ await requireAdminAccess(Astro, '/admin/login');
71
+ ```
72
+
73
+ ## Route Guard Config
74
+
75
+ ```typescript
76
+ interface RouteGuardConfig {
77
+ publicRoutes: RoutePattern[];
78
+ protectedRoutes: RoutePattern[];
79
+ loginPage: string;
80
+ brandRoutePattern?: RegExp;
81
+ }
82
+ ```
@@ -36,7 +36,7 @@ function matchesPattern(pathname: string, patterns: RoutePattern[]): boolean {
36
36
  } catch (error) {
37
37
  // Log pattern matching errors but don't block the request
38
38
  console.error(
39
- '[htlkg Route Guard] Error matching pattern:',
39
+ '[htlkg] Error matching route pattern:',
40
40
  error instanceof Error ? error.message : 'Unknown error',
41
41
  );
42
42
  return false;
@@ -78,7 +78,6 @@ export const routeGuard: MiddlewareHandler = async (context, next) => {
78
78
  try {
79
79
  // Public routes - no auth required
80
80
  if (matchesPattern(pathname, publicRoutes)) {
81
- console.log(`[htlkg Route Guard] Public route: ${pathname}`);
82
81
  return next();
83
82
  }
84
83
 
@@ -87,14 +86,8 @@ export const routeGuard: MiddlewareHandler = async (context, next) => {
87
86
  // Admin routes - require admin role
88
87
  if (matchesPattern(pathname, adminRoutes)) {
89
88
  if (!user || !user.isAdmin) {
90
- console.log(
91
- `[htlkg Route Guard] Admin access denied for ${pathname} - User: ${user ? 'authenticated (non-admin)' : 'not authenticated'}`,
92
- );
93
89
  return redirect(`${loginUrl}?error=admin_required`);
94
90
  }
95
- console.log(
96
- `[htlkg Route Guard] Admin access granted for ${pathname}`,
97
- );
98
91
  return next();
99
92
  }
100
93
 
@@ -106,26 +99,18 @@ export const routeGuard: MiddlewareHandler = async (context, next) => {
106
99
  const brandId = Number.parseInt(match[brandRoute.brandIdParam], 10);
107
100
 
108
101
  if (Number.isNaN(brandId)) {
109
- console.warn(
110
- `[htlkg Route Guard] Invalid brandId extracted from ${pathname}`,
111
- );
102
+ console.warn(`[htlkg] Invalid brandId extracted from ${pathname}`);
112
103
  return redirect(`${loginUrl}?error=invalid_brand`);
113
104
  }
114
105
 
115
106
  if (!user || (!user.isAdmin && !user.brandIds.includes(brandId))) {
116
- console.log(
117
- `[htlkg Route Guard] Brand access denied for ${pathname} (brandId: ${brandId}) - User: ${user ? `authenticated (brandIds: ${user.brandIds.join(',')})` : 'not authenticated'}`,
118
- );
119
107
  return redirect(`${loginUrl}?error=access_denied`);
120
108
  }
121
- console.log(
122
- `[htlkg Route Guard] Brand access granted for ${pathname} (brandId: ${brandId})`,
123
- );
124
109
  return next();
125
110
  }
126
111
  } catch (error) {
127
112
  console.error(
128
- `[htlkg Route Guard] Error processing brand route for ${pathname}:`,
113
+ `[htlkg] Error processing brand route for ${pathname}:`,
129
114
  error instanceof Error ? error.message : 'Unknown error',
130
115
  );
131
116
  // Fail-safe: deny access on error
@@ -137,26 +122,17 @@ export const routeGuard: MiddlewareHandler = async (context, next) => {
137
122
  if (matchesPattern(pathname, authenticatedRoutes)) {
138
123
  if (!user) {
139
124
  const returnUrl = encodeURIComponent(pathname + url.search);
140
- console.log(
141
- `[htlkg Route Guard] Authentication required for ${pathname}, redirecting to login`,
142
- );
143
125
  return redirect(`${loginUrl}?redirect=${returnUrl}`);
144
126
  }
145
- console.log(
146
- `[htlkg Route Guard] Authenticated access granted for ${pathname}`,
147
- );
148
127
  return next();
149
128
  }
150
129
 
151
130
  // Default: allow access
152
- console.log(
153
- `[htlkg Route Guard] Default access granted for ${pathname}`,
154
- );
155
131
  return next();
156
132
  } catch (error) {
157
133
  // Catch-all error handler for route guard
158
134
  console.error(
159
- '[htlkg Route Guard] Unexpected error in route guard:',
135
+ '[htlkg] Unexpected error in route guard:',
160
136
  error instanceof Error ? error.message : 'Unknown error',
161
137
  );
162
138
  // Fail-safe: deny access and redirect to login
@@ -0,0 +1,104 @@
1
+ # Patterns Module
2
+
3
+ Reusable page patterns for common admin and brand pages.
4
+
5
+ ## Installation
6
+
7
+ ```typescript
8
+ // Admin patterns
9
+ import { ListPage } from '@htlkg/astro/patterns/admin';
10
+ import { DetailPage } from '@htlkg/astro/patterns/admin';
11
+ import { FormPage } from '@htlkg/astro/patterns/admin';
12
+
13
+ // Brand patterns
14
+ import { ConfigPage } from '@htlkg/astro/patterns/brand';
15
+ import { PortalPage } from '@htlkg/astro/patterns/brand';
16
+ ```
17
+
18
+ ## Admin Patterns
19
+
20
+ ### ListPage
21
+
22
+ List/table view with filters and pagination.
23
+
24
+ ```astro
25
+ ---
26
+ import { ListPage } from '@htlkg/astro/patterns/admin';
27
+ ---
28
+
29
+ <ListPage
30
+ title="Brands"
31
+ items={brands}
32
+ columns={columns}
33
+ filters={filters}
34
+ onRowClick={(brand) => `/admin/brands/${brand.id}`}
35
+ />
36
+ ```
37
+
38
+ ### DetailPage
39
+
40
+ Detail view with tabs and actions.
41
+
42
+ ```astro
43
+ ---
44
+ import { DetailPage } from '@htlkg/astro/patterns/admin';
45
+ ---
46
+
47
+ <DetailPage
48
+ title={brand.name}
49
+ tabs={['Overview', 'Settings', 'Users']}
50
+ actions={['Edit', 'Delete']}
51
+ >
52
+ <BrandOverview slot="Overview" brand={brand} />
53
+ <BrandSettings slot="Settings" brand={brand} />
54
+ <BrandUsers slot="Users" brand={brand} />
55
+ </DetailPage>
56
+ ```
57
+
58
+ ### FormPage
59
+
60
+ Form view with validation.
61
+
62
+ ```astro
63
+ ---
64
+ import { FormPage } from '@htlkg/astro/patterns/admin';
65
+ ---
66
+
67
+ <FormPage
68
+ title="Create Brand"
69
+ schema={brandSchema}
70
+ onSubmit={handleSubmit}
71
+ />
72
+ ```
73
+
74
+ ## Brand Patterns
75
+
76
+ ### ConfigPage
77
+
78
+ Configuration page for brand settings.
79
+
80
+ ```astro
81
+ ---
82
+ import { ConfigPage } from '@htlkg/astro/patterns/brand';
83
+ ---
84
+
85
+ <ConfigPage
86
+ brand={brand}
87
+ product={product}
88
+ config={productConfig}
89
+ />
90
+ ```
91
+
92
+ ### PortalPage
93
+
94
+ Public portal page for brand.
95
+
96
+ ```astro
97
+ ---
98
+ import { PortalPage } from '@htlkg/astro/patterns/brand';
99
+ ---
100
+
101
+ <PortalPage brand={brand}>
102
+ <WifiPortal />
103
+ </PortalPage>
104
+ ```
@@ -0,0 +1,320 @@
1
+ /**
2
+ * Filter Building Utilities
3
+ *
4
+ * Utilities for building GraphQL filters from parsed URL parameters,
5
+ * and for applying client-side filters to data.
6
+ */
7
+
8
+ import type { FilterFieldConfig, ListParams } from "./params";
9
+
10
+ /**
11
+ * GraphQL filter operators
12
+ */
13
+ export type GraphQLOperator = "eq" | "ne" | "le" | "lt" | "ge" | "gt" | "contains" | "notContains" | "beginsWith" | "between";
14
+
15
+ /**
16
+ * GraphQL filter condition
17
+ */
18
+ export interface GraphQLFilterCondition {
19
+ [key: string]: {
20
+ [operator in GraphQLOperator]?: any;
21
+ };
22
+ }
23
+
24
+ /**
25
+ * GraphQL combined filter
26
+ */
27
+ export interface GraphQLFilter {
28
+ and?: GraphQLFilterCondition[];
29
+ or?: GraphQLFilterCondition[];
30
+ not?: GraphQLFilter;
31
+ [key: string]: any;
32
+ }
33
+
34
+ /**
35
+ * Build GraphQL filter from parsed URL parameters
36
+ *
37
+ * Only includes fields marked with `graphql: true` in the field config.
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * const filter = buildGraphQLFilter(
42
+ * { status: 'active', name: 'test' },
43
+ * [
44
+ * { key: 'status', type: 'select', graphql: true },
45
+ * { key: 'name', type: 'text', graphql: true },
46
+ * ]
47
+ * );
48
+ * // Returns: { and: [{ status: { eq: 'active' } }, { name: { contains: 'test' } }] }
49
+ * ```
50
+ */
51
+ export function buildGraphQLFilter(
52
+ filters: Record<string, any>,
53
+ filterableFields: FilterFieldConfig[],
54
+ options: { searchFields?: string[]; search?: string } = {}
55
+ ): GraphQLFilter | undefined {
56
+ const conditions: GraphQLFilterCondition[] = [];
57
+
58
+ // Build field filters
59
+ for (const field of filterableFields) {
60
+ if (!field.graphql) continue;
61
+
62
+ const value = filters[field.key];
63
+ if (value === undefined || value === null || value === "") continue;
64
+
65
+ const condition = buildFieldCondition(field, value);
66
+ if (condition) {
67
+ conditions.push(condition);
68
+ }
69
+ }
70
+
71
+ // Build search filter (applies to multiple fields with OR)
72
+ if (options.search && options.searchFields && options.searchFields.length > 0) {
73
+ const searchConditions = options.searchFields.map((fieldKey) => ({
74
+ [fieldKey]: { contains: options.search },
75
+ }));
76
+
77
+ if (searchConditions.length === 1) {
78
+ conditions.push(searchConditions[0]);
79
+ } else if (searchConditions.length > 1) {
80
+ // Multiple search fields use OR
81
+ conditions.push({ or: searchConditions } as unknown as GraphQLFilterCondition);
82
+ }
83
+ }
84
+
85
+ // Return combined filter
86
+ if (conditions.length === 0) {
87
+ return undefined;
88
+ }
89
+
90
+ if (conditions.length === 1) {
91
+ return conditions[0] as unknown as GraphQLFilter;
92
+ }
93
+
94
+ return { and: conditions };
95
+ }
96
+
97
+ /**
98
+ * Build a single field condition
99
+ */
100
+ function buildFieldCondition(field: FilterFieldConfig, value: any): GraphQLFilterCondition | undefined {
101
+ // Use custom operator if provided
102
+ const operator = field.graphqlOperator;
103
+
104
+ if (operator) {
105
+ // Use the custom operator specified in config
106
+ return { [field.key]: { [operator]: value } };
107
+ }
108
+
109
+ // Default behavior based on field type
110
+ switch (field.type) {
111
+ case "text":
112
+ // Text fields use contains by default (case-sensitive in DynamoDB)
113
+ return { [field.key]: { contains: String(value) } };
114
+
115
+ case "number":
116
+ // Number fields use exact match
117
+ return { [field.key]: { eq: Number(value) } };
118
+
119
+ case "boolean":
120
+ // Boolean fields use exact match
121
+ return { [field.key]: { eq: Boolean(value) } };
122
+
123
+ case "date":
124
+ // Date fields - could be exact or range
125
+ if (value instanceof Date) {
126
+ return { [field.key]: { eq: value.toISOString() } };
127
+ }
128
+ return { [field.key]: { eq: value } };
129
+
130
+ case "select":
131
+ // Select fields use exact match
132
+ return { [field.key]: { eq: value } };
133
+
134
+ default:
135
+ return { [field.key]: { eq: value } };
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Apply client-side filters to data array
141
+ *
142
+ * Only applies filters for fields NOT marked with `graphql: true`.
143
+ *
144
+ * @example
145
+ * ```typescript
146
+ * const filtered = applyClientFilters(
147
+ * items,
148
+ * { brandCount: 5 },
149
+ * [{ key: 'brandCount', type: 'number', graphql: false }]
150
+ * );
151
+ * ```
152
+ */
153
+ export function applyClientFilters<T extends Record<string, any>>(
154
+ items: T[],
155
+ filters: Record<string, any>,
156
+ filterableFields: FilterFieldConfig[],
157
+ options: { search?: string; searchFields?: string[] } = {}
158
+ ): T[] {
159
+ let result = items;
160
+
161
+ // Apply field filters (only non-GraphQL fields)
162
+ for (const field of filterableFields) {
163
+ if (field.graphql) continue; // Skip GraphQL fields
164
+
165
+ const value = filters[field.key];
166
+ if (value === undefined || value === null || value === "") continue;
167
+
168
+ result = result.filter((item) => matchesFieldFilter(item, field, value));
169
+ }
170
+
171
+ // Apply search filter (only if not applied via GraphQL)
172
+ if (options.search && options.searchFields) {
173
+ const searchLower = options.search.toLowerCase();
174
+ result = result.filter((item) =>
175
+ options.searchFields!.some((fieldKey) => {
176
+ const fieldValue = item[fieldKey];
177
+ if (fieldValue === null || fieldValue === undefined) return false;
178
+ return String(fieldValue).toLowerCase().includes(searchLower);
179
+ })
180
+ );
181
+ }
182
+
183
+ return result;
184
+ }
185
+
186
+ /**
187
+ * Check if an item matches a field filter
188
+ */
189
+ function matchesFieldFilter<T extends Record<string, any>>(item: T, field: FilterFieldConfig, filterValue: any): boolean {
190
+ const itemValue = item[field.key];
191
+
192
+ switch (field.type) {
193
+ case "text":
194
+ if (itemValue === null || itemValue === undefined) return false;
195
+ return String(itemValue).toLowerCase().includes(String(filterValue).toLowerCase());
196
+
197
+ case "number":
198
+ return Number(itemValue) === Number(filterValue);
199
+
200
+ case "boolean":
201
+ return Boolean(itemValue) === Boolean(filterValue);
202
+
203
+ case "date":
204
+ if (!itemValue) return false;
205
+ const itemDate = itemValue instanceof Date ? itemValue : new Date(itemValue);
206
+ const filterDate = filterValue instanceof Date ? filterValue : new Date(filterValue);
207
+ // Compare dates (ignoring time)
208
+ return itemDate.toDateString() === filterDate.toDateString();
209
+
210
+ case "select":
211
+ return itemValue === filterValue;
212
+
213
+ default:
214
+ return itemValue === filterValue;
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Sort items by a key
220
+ */
221
+ export function sortItems<T extends Record<string, any>>(items: T[], sortKey: string, sortOrder: "asc" | "desc"): T[] {
222
+ if (!sortKey) return items;
223
+
224
+ const multiplier = sortOrder === "asc" ? 1 : -1;
225
+
226
+ return [...items].sort((a, b) => {
227
+ const aVal = a[sortKey];
228
+ const bVal = b[sortKey];
229
+
230
+ // Handle null/undefined
231
+ if (aVal === null || aVal === undefined) return 1;
232
+ if (bVal === null || bVal === undefined) return -1;
233
+ if (aVal === bVal) return 0;
234
+
235
+ // Handle different types
236
+ if (typeof aVal === "string" && typeof bVal === "string") {
237
+ return aVal.localeCompare(bVal) * multiplier;
238
+ }
239
+
240
+ if (typeof aVal === "number" && typeof bVal === "number") {
241
+ return (aVal - bVal) * multiplier;
242
+ }
243
+
244
+ if (aVal instanceof Date && bVal instanceof Date) {
245
+ return (aVal.getTime() - bVal.getTime()) * multiplier;
246
+ }
247
+
248
+ // Fallback to string comparison
249
+ return String(aVal).localeCompare(String(bVal)) * multiplier;
250
+ });
251
+ }
252
+
253
+ /**
254
+ * Paginate items
255
+ */
256
+ export function paginateItems<T>(
257
+ items: T[],
258
+ page: number,
259
+ pageSize: number
260
+ ): {
261
+ paginatedItems: T[];
262
+ totalItems: number;
263
+ totalPages: number;
264
+ currentPage: number;
265
+ pageSize: number;
266
+ } {
267
+ const totalItems = items.length;
268
+ const totalPages = Math.ceil(totalItems / pageSize);
269
+ const currentPage = Math.min(Math.max(1, page), totalPages || 1);
270
+
271
+ const startIndex = (currentPage - 1) * pageSize;
272
+ const endIndex = startIndex + pageSize;
273
+ const paginatedItems = items.slice(startIndex, endIndex);
274
+
275
+ return {
276
+ paginatedItems,
277
+ totalItems,
278
+ totalPages,
279
+ currentPage,
280
+ pageSize,
281
+ };
282
+ }
283
+
284
+ /**
285
+ * Process list data with filters, sorting, and pagination
286
+ *
287
+ * Convenience function that combines all processing steps.
288
+ */
289
+ export function processListData<T extends Record<string, any>>(
290
+ items: T[],
291
+ params: ListParams,
292
+ filterableFields: FilterFieldConfig[],
293
+ options: { searchFields?: string[] } = {}
294
+ ): {
295
+ items: T[];
296
+ totalItems: number;
297
+ totalPages: number;
298
+ currentPage: number;
299
+ pageSize: number;
300
+ } {
301
+ // Apply client-side filters
302
+ let result = applyClientFilters(items, params.filters, filterableFields, {
303
+ search: params.search,
304
+ searchFields: options.searchFields,
305
+ });
306
+
307
+ // Sort
308
+ result = sortItems(result, params.sortKey, params.sortOrder);
309
+
310
+ // Paginate
311
+ const paginated = paginateItems(result, params.page, params.pageSize);
312
+
313
+ return {
314
+ items: paginated.paginatedItems,
315
+ totalItems: paginated.totalItems,
316
+ totalPages: paginated.totalPages,
317
+ currentPage: paginated.currentPage,
318
+ pageSize: paginated.pageSize,
319
+ };
320
+ }
@@ -1,9 +1,15 @@
1
1
  /**
2
- * @htlkg/pages - Utilities
3
- *
2
+ * @htlkg/astro - Utilities
3
+ *
4
4
  * Helper functions for Astro pages.
5
5
  */
6
6
 
7
7
  export * from './ssr';
8
8
  export * from './static';
9
9
  export * from './hydration';
10
+
11
+ // URL parameter parsing and building
12
+ export * from './params';
13
+
14
+ // Filter building and processing
15
+ export * from './filters';