@htlkg/core 0.0.2 → 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 (35) hide show
  1. package/README.md +51 -0
  2. package/dist/index.d.ts +219 -0
  3. package/dist/index.js +121 -0
  4. package/dist/index.js.map +1 -1
  5. package/package.json +30 -8
  6. package/src/amplify-astro-adapter/amplify-astro-adapter.md +167 -0
  7. package/src/amplify-astro-adapter/createCookieStorageAdapterFromAstroContext.test.ts +296 -0
  8. package/src/amplify-astro-adapter/createCookieStorageAdapterFromAstroContext.ts +97 -0
  9. package/src/amplify-astro-adapter/createRunWithAmplifyServerContext.ts +84 -0
  10. package/src/amplify-astro-adapter/errors.test.ts +115 -0
  11. package/src/amplify-astro-adapter/errors.ts +105 -0
  12. package/src/amplify-astro-adapter/globalSettings.test.ts +78 -0
  13. package/src/amplify-astro-adapter/globalSettings.ts +16 -0
  14. package/src/amplify-astro-adapter/index.ts +14 -0
  15. package/src/amplify-astro-adapter/types.ts +55 -0
  16. package/src/auth/auth.md +178 -0
  17. package/src/auth/index.test.ts +180 -0
  18. package/src/auth/index.ts +294 -0
  19. package/src/constants/constants.md +132 -0
  20. package/src/constants/index.test.ts +116 -0
  21. package/src/constants/index.ts +98 -0
  22. package/src/core-exports.property.test.ts +186 -0
  23. package/src/errors/errors.md +177 -0
  24. package/src/errors/index.test.ts +153 -0
  25. package/src/errors/index.ts +134 -0
  26. package/src/index.ts +65 -0
  27. package/src/routes/index.ts +225 -0
  28. package/src/routes/routes.md +189 -0
  29. package/src/types/index.ts +94 -0
  30. package/src/types/types.md +144 -0
  31. package/src/utils/index.test.ts +257 -0
  32. package/src/utils/index.ts +112 -0
  33. package/src/utils/logger.ts +88 -0
  34. package/src/utils/utils.md +199 -0
  35. package/src/workspace.property.test.ts +235 -0
@@ -0,0 +1,180 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import {
3
+ hasAccessToBrand,
4
+ hasAccessToAccount,
5
+ isAdminUser,
6
+ isSuperAdminUser,
7
+ type User,
8
+ } from "./index";
9
+
10
+ describe("Auth Module", () => {
11
+ describe("hasAccessToBrand", () => {
12
+ it("should return false for null user", () => {
13
+ expect(hasAccessToBrand(null, 1)).toBe(false);
14
+ });
15
+
16
+ it("should return true for admin user", () => {
17
+ const user: User = {
18
+ username: "admin",
19
+ email: "admin@test.com",
20
+ brandIds: [],
21
+ accountIds: [],
22
+ isAdmin: true,
23
+ isSuperAdmin: false,
24
+ roles: ["ADMINS"],
25
+ };
26
+ expect(hasAccessToBrand(user, 1)).toBe(true);
27
+ });
28
+
29
+ it("should return true if user has brand access", () => {
30
+ const user: User = {
31
+ username: "user",
32
+ email: "user@test.com",
33
+ brandIds: [1, 2, 3],
34
+ accountIds: [],
35
+ isAdmin: false,
36
+ isSuperAdmin: false,
37
+ roles: [],
38
+ };
39
+ expect(hasAccessToBrand(user, 2)).toBe(true);
40
+ });
41
+
42
+ it("should return false if user does not have brand access", () => {
43
+ const user: User = {
44
+ username: "user",
45
+ email: "user@test.com",
46
+ brandIds: [1, 2, 3],
47
+ accountIds: [],
48
+ isAdmin: false,
49
+ isSuperAdmin: false,
50
+ roles: [],
51
+ };
52
+ expect(hasAccessToBrand(user, 5)).toBe(false);
53
+ });
54
+ });
55
+
56
+ describe("hasAccessToAccount", () => {
57
+ it("should return false for null user", () => {
58
+ expect(hasAccessToAccount(null, 1)).toBe(false);
59
+ });
60
+
61
+ it("should return true for admin user", () => {
62
+ const user: User = {
63
+ username: "admin",
64
+ email: "admin@test.com",
65
+ brandIds: [],
66
+ accountIds: [],
67
+ isAdmin: true,
68
+ isSuperAdmin: false,
69
+ roles: ["ADMINS"],
70
+ };
71
+ expect(hasAccessToAccount(user, 1)).toBe(true);
72
+ });
73
+
74
+ it("should return true if user has account access", () => {
75
+ const user: User = {
76
+ username: "user",
77
+ email: "user@test.com",
78
+ brandIds: [],
79
+ accountIds: [10, 20, 30],
80
+ isAdmin: false,
81
+ isSuperAdmin: false,
82
+ roles: [],
83
+ };
84
+ expect(hasAccessToAccount(user, 20)).toBe(true);
85
+ });
86
+
87
+ it("should return false if user does not have account access", () => {
88
+ const user: User = {
89
+ username: "user",
90
+ email: "user@test.com",
91
+ brandIds: [],
92
+ accountIds: [10, 20, 30],
93
+ isAdmin: false,
94
+ isSuperAdmin: false,
95
+ roles: [],
96
+ };
97
+ expect(hasAccessToAccount(user, 50)).toBe(false);
98
+ });
99
+ });
100
+
101
+ describe("isAdminUser", () => {
102
+ it("should return false for null user", () => {
103
+ expect(isAdminUser(null)).toBe(false);
104
+ });
105
+
106
+ it("should return true for admin user", () => {
107
+ const user: User = {
108
+ username: "admin",
109
+ email: "admin@test.com",
110
+ brandIds: [],
111
+ accountIds: [],
112
+ isAdmin: true,
113
+ isSuperAdmin: false,
114
+ roles: ["ADMINS"],
115
+ };
116
+ expect(isAdminUser(user)).toBe(true);
117
+ });
118
+
119
+ it("should return false for non-admin user", () => {
120
+ const user: User = {
121
+ username: "user",
122
+ email: "user@test.com",
123
+ brandIds: [],
124
+ accountIds: [],
125
+ isAdmin: false,
126
+ isSuperAdmin: false,
127
+ roles: [],
128
+ };
129
+ expect(isAdminUser(user)).toBe(false);
130
+ });
131
+ });
132
+
133
+ describe("isSuperAdminUser", () => {
134
+ it("should return false for null user", () => {
135
+ expect(isSuperAdminUser(null)).toBe(false);
136
+ });
137
+
138
+ it("should return true for super admin user", () => {
139
+ const user: User = {
140
+ username: "superadmin",
141
+ email: "superadmin@test.com",
142
+ brandIds: [],
143
+ accountIds: [],
144
+ isAdmin: true,
145
+ isSuperAdmin: true,
146
+ roles: ["SUPER_ADMINS"],
147
+ };
148
+ expect(isSuperAdminUser(user)).toBe(true);
149
+ });
150
+
151
+ it("should return false for regular admin user", () => {
152
+ const user: User = {
153
+ username: "admin",
154
+ email: "admin@test.com",
155
+ brandIds: [],
156
+ accountIds: [],
157
+ isAdmin: true,
158
+ isSuperAdmin: false,
159
+ roles: ["ADMINS"],
160
+ };
161
+ expect(isSuperAdminUser(user)).toBe(false);
162
+ });
163
+
164
+ it("should return false for non-admin user", () => {
165
+ const user: User = {
166
+ username: "user",
167
+ email: "user@test.com",
168
+ brandIds: [],
169
+ accountIds: [],
170
+ isAdmin: false,
171
+ isSuperAdmin: false,
172
+ roles: [],
173
+ };
174
+ expect(isSuperAdminUser(user)).toBe(false);
175
+ });
176
+ });
177
+
178
+ // Note: getClientUser tests are skipped because they require mocking AWS Amplify
179
+ // which is complex in a unit test environment. These should be tested in integration tests.
180
+ });
@@ -0,0 +1,294 @@
1
+ /**
2
+ * @htlkg/core/auth
3
+ * Core authentication functions and utilities
4
+ */
5
+
6
+ import type { APIContext } from "astro";
7
+ import { Amplify } from "aws-amplify";
8
+ import { fetchAuthSession } from "aws-amplify/auth";
9
+ import {
10
+ fetchAuthSession as fetchServerAuthSession,
11
+ getCurrentUser as getServerCurrentUser,
12
+ } from "aws-amplify/auth/server";
13
+ import { createRunWithAmplifyServerContext, globalSettings } from "../amplify-astro-adapter";
14
+
15
+ // Re-export types
16
+ export type { APIContext } from "astro";
17
+
18
+ /**
19
+ * User interface representing an authenticated user
20
+ */
21
+ export interface User {
22
+ username: string;
23
+ email: string;
24
+ brandIds: number[];
25
+ accountIds: number[];
26
+ isAdmin: boolean;
27
+ isSuperAdmin: boolean;
28
+ roles: string[];
29
+ }
30
+
31
+ /**
32
+ * Parse comma-separated IDs from Cognito custom attributes
33
+ */
34
+ function parseIds(ids: string | undefined): number[] {
35
+ if (!ids) return [];
36
+ return ids
37
+ .split(",")
38
+ .map((id) => Number.parseInt(id.trim(), 10))
39
+ .filter((id) => !Number.isNaN(id));
40
+ }
41
+
42
+ // Track if Amplify has been configured
43
+ let amplifyConfigured = false;
44
+
45
+ /**
46
+ * Configure Amplify on first use (server-side)
47
+ * This must be called before any auth operations
48
+ */
49
+ function ensureAmplifyConfigured(): void {
50
+ if (amplifyConfigured) return;
51
+
52
+ try {
53
+ // Amplify should already be configured by the middleware
54
+ // This is just a safety check
55
+ const config = Amplify.getConfig();
56
+ if (!config.Auth) {
57
+ throw new Error("Amplify Auth not configured");
58
+ }
59
+ amplifyConfigured = true;
60
+ } catch (error) {
61
+ const errorMsg = error instanceof Error ? error.message : "Unknown error";
62
+ console.error(`[Auth] Amplify not configured: ${errorMsg}`);
63
+ throw error;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Get current authenticated user (server-side)
69
+ * Reads Amplify auth cookies from the request and validates the session
70
+ */
71
+ export async function getUser(context: APIContext): Promise<User | null> {
72
+ try {
73
+ ensureAmplifyConfigured();
74
+
75
+ // Get the current Amplify configuration
76
+ const amplifyConfig = Amplify.getConfig();
77
+
78
+ // Create the server context runner - pass globalSettings explicitly
79
+ // to ensure the same instance is used across all modules
80
+ const runWithAmplifyServerContext = createRunWithAmplifyServerContext({
81
+ config: amplifyConfig,
82
+ globalSettings,
83
+ });
84
+
85
+ // Create Astro server context
86
+ const astroServerContext = {
87
+ cookies: context.cookies,
88
+ request: context.request,
89
+ };
90
+
91
+ // Run in Amplify server context
92
+ const user = await runWithAmplifyServerContext({
93
+ astroServerContext,
94
+ operation: async (contextSpec) => {
95
+ try {
96
+ const currentUser = await getServerCurrentUser(contextSpec as any);
97
+ const session = await fetchServerAuthSession(contextSpec as any);
98
+
99
+ if (!session.tokens?.accessToken) {
100
+ return null;
101
+ }
102
+
103
+ const accessPayload = session.tokens.accessToken.payload;
104
+ const idPayload = session.tokens.idToken?.payload;
105
+
106
+ // Parse user attributes - email is typically in ID token, not access token
107
+ const email =
108
+ (idPayload?.email as string) ||
109
+ (accessPayload.email as string) ||
110
+ "";
111
+ const brandIds = parseIds(
112
+ accessPayload["custom:brand_ids"] as string,
113
+ );
114
+ const accountIds = parseIds(
115
+ accessPayload["custom:account_ids"] as string,
116
+ );
117
+
118
+ // Check admin status
119
+ const groups = (accessPayload["cognito:groups"] as string[]) || [];
120
+ const isAdmin =
121
+ groups.includes("admin") ||
122
+ groups.includes("ADMINS") ||
123
+ groups.includes("SUPER_ADMINS");
124
+ const isSuperAdmin = groups.includes("SUPER_ADMINS");
125
+
126
+ return {
127
+ username: currentUser.username,
128
+ email,
129
+ brandIds,
130
+ accountIds,
131
+ isAdmin,
132
+ isSuperAdmin,
133
+ roles: groups,
134
+ };
135
+ } catch (error) {
136
+ // UserUnAuthenticatedException is expected when user is not logged in
137
+ // Only log other errors without sensitive data
138
+ if (
139
+ error instanceof Error &&
140
+ error.name !== "UserUnAuthenticatedException"
141
+ ) {
142
+ // Log error name and message without stack trace or sensitive data
143
+ console.error(
144
+ "[Auth] Error in server context:",
145
+ error.name,
146
+ "-",
147
+ error.message.replace(/token[=:]\s*[^\s,}]+/gi, "token=***"),
148
+ );
149
+ }
150
+ return null;
151
+ }
152
+ },
153
+ });
154
+
155
+ return user;
156
+ } catch (error) {
157
+ // Log error without exposing sensitive information
158
+ if (error instanceof Error) {
159
+ console.error(
160
+ "[Auth] Server-side auth error:",
161
+ error.name,
162
+ "-",
163
+ error.message.replace(/token[=:]\s*[^\s,}]+/gi, "token=***"),
164
+ );
165
+ } else {
166
+ console.error("[Auth] Server-side auth error: Unknown error");
167
+ }
168
+ return null;
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Get current authenticated user (client-side)
174
+ */
175
+ export async function getClientUser(): Promise<User | null> {
176
+ try {
177
+ const session = await fetchAuthSession();
178
+
179
+ if (!session.tokens?.accessToken) {
180
+ return null;
181
+ }
182
+
183
+ const accessPayload = session.tokens.accessToken.payload;
184
+ const idPayload = session.tokens.idToken?.payload;
185
+
186
+ const email =
187
+ (idPayload?.email as string) || (accessPayload.email as string) || "";
188
+ const brandIds = parseIds(accessPayload["custom:brand_ids"] as string);
189
+ const accountIds = parseIds(accessPayload["custom:account_ids"] as string);
190
+
191
+ const groups = (accessPayload["cognito:groups"] as string[]) || [];
192
+ const isAdmin =
193
+ groups.includes("admin") ||
194
+ groups.includes("ADMINS") ||
195
+ groups.includes("SUPER_ADMINS");
196
+ const isSuperAdmin = groups.includes("SUPER_ADMINS");
197
+
198
+ return {
199
+ username: (accessPayload.username as string) || "",
200
+ email,
201
+ brandIds,
202
+ accountIds,
203
+ isAdmin,
204
+ isSuperAdmin,
205
+ roles: groups,
206
+ };
207
+ } catch (error) {
208
+ if (error instanceof Error) {
209
+ console.error(
210
+ "[Auth] Client auth error:",
211
+ error.name,
212
+ "-",
213
+ error.message.replace(/token[=:]\s*[^\s,}]+/gi, "token=***"),
214
+ );
215
+ }
216
+ return null;
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Check if user has access to a specific brand
222
+ */
223
+ export function hasAccessToBrand(
224
+ user: User | null,
225
+ brandId: number,
226
+ ): boolean {
227
+ if (!user) return false;
228
+ if (user.isAdmin) return true;
229
+ return user.brandIds.includes(brandId);
230
+ }
231
+
232
+ /**
233
+ * Check if user has access to a specific account
234
+ */
235
+ export function hasAccessToAccount(
236
+ user: User | null,
237
+ accountId: number,
238
+ ): boolean {
239
+ if (!user) return false;
240
+ if (user.isAdmin) return true;
241
+ return user.accountIds.includes(accountId);
242
+ }
243
+
244
+ /**
245
+ * Check if user is an admin
246
+ */
247
+ export function isAdminUser(user: User | null): boolean {
248
+ return user?.isAdmin || false;
249
+ }
250
+
251
+ /**
252
+ * Check if user is a super admin
253
+ */
254
+ export function isSuperAdminUser(user: User | null): boolean {
255
+ return user?.isSuperAdmin || false;
256
+ }
257
+
258
+ /**
259
+ * Placeholder for requireAuth - will be implemented in @htlkg/integrations
260
+ * This is here for type exports and documentation
261
+ */
262
+ export async function requireAuth(
263
+ context: APIContext,
264
+ loginUrl?: string,
265
+ ): Promise<{ success: boolean; user?: User; redirectTo?: string }> {
266
+ throw new Error(
267
+ "requireAuth should be imported from @htlkg/astro/middleware",
268
+ );
269
+ }
270
+
271
+ /**
272
+ * Placeholder for requireAdminAccess - will be implemented in @htlkg/integrations
273
+ */
274
+ export async function requireAdminAccess(
275
+ context: APIContext,
276
+ loginUrl?: string,
277
+ ): Promise<{ success: boolean; user?: User; redirectTo?: string }> {
278
+ throw new Error(
279
+ "requireAdminAccess should be imported from @htlkg/astro/middleware",
280
+ );
281
+ }
282
+
283
+ /**
284
+ * Placeholder for requireBrandAccess - will be implemented in @htlkg/integrations
285
+ */
286
+ export async function requireBrandAccess(
287
+ context: APIContext,
288
+ brandId: number,
289
+ loginUrl?: string,
290
+ ): Promise<{ success: boolean; user?: User; redirectTo?: string }> {
291
+ throw new Error(
292
+ "requireBrandAccess should be imported from @htlkg/astro/middleware",
293
+ );
294
+ }
@@ -0,0 +1,132 @@
1
+ # Constants Module
2
+
3
+ Centralized constants for routes, products, permissions, and user roles.
4
+
5
+ ## Installation
6
+
7
+ ```typescript
8
+ import { ROUTES, PRODUCTS, PERMISSIONS, USER_ROLES } from '@htlkg/core/constants';
9
+ ```
10
+
11
+ ## ROUTES
12
+
13
+ Application route definitions.
14
+
15
+ ```typescript
16
+ // Admin routes
17
+ ROUTES.ADMIN.HOME // "/admin"
18
+ ROUTES.ADMIN.BRANDS // "/admin/brands"
19
+ ROUTES.ADMIN.ACCOUNTS // "/admin/accounts"
20
+ ROUTES.ADMIN.USERS // "/admin/users"
21
+ ROUTES.ADMIN.PRODUCTS // "/admin/products"
22
+ ROUTES.ADMIN.SETTINGS // "/admin/settings"
23
+
24
+ // Brand routes (with brandId parameter)
25
+ ROUTES.BRAND.HOME(brandId) // "/{brandId}"
26
+ ROUTES.BRAND.ADMIN(brandId) // "/{brandId}/admin"
27
+ ROUTES.BRAND.TEMPLATES(brandId) // "/{brandId}/admin/templates"
28
+ ROUTES.BRAND.SETTINGS(brandId) // "/{brandId}/admin/settings"
29
+
30
+ // Auth routes
31
+ ROUTES.AUTH.LOGIN // "/login"
32
+ ROUTES.AUTH.LOGOUT // "/logout"
33
+ ROUTES.AUTH.SIGNUP // "/signup"
34
+ ROUTES.AUTH.FORGOT_PASSWORD // "/forgot-password"
35
+ ```
36
+
37
+ ## PRODUCTS
38
+
39
+ Product definitions for the platform.
40
+
41
+ ```typescript
42
+ PRODUCTS.WIFI_PORTAL
43
+ // { id: "wifi-portal", name: "WiFi Portal", description: "..." }
44
+
45
+ PRODUCTS.WHATSAPP_CRM
46
+ // { id: "whatsapp-crm", name: "WhatsApp CRM", description: "..." }
47
+
48
+ PRODUCTS.ANALYTICS
49
+ // { id: "analytics", name: "Analytics", description: "..." }
50
+ ```
51
+
52
+ ## PERMISSIONS
53
+
54
+ Permission string constants for authorization.
55
+
56
+ ```typescript
57
+ // Brand permissions
58
+ PERMISSIONS.BRAND_VIEW // "brand:view"
59
+ PERMISSIONS.BRAND_EDIT // "brand:edit"
60
+ PERMISSIONS.BRAND_DELETE // "brand:delete"
61
+ PERMISSIONS.BRAND_CREATE // "brand:create"
62
+
63
+ // Account permissions
64
+ PERMISSIONS.ACCOUNT_VIEW // "account:view"
65
+ PERMISSIONS.ACCOUNT_EDIT // "account:edit"
66
+ PERMISSIONS.ACCOUNT_DELETE // "account:delete"
67
+ PERMISSIONS.ACCOUNT_CREATE // "account:create"
68
+
69
+ // User permissions
70
+ PERMISSIONS.USER_VIEW // "user:view"
71
+ PERMISSIONS.USER_EDIT // "user:edit"
72
+ PERMISSIONS.USER_DELETE // "user:delete"
73
+ PERMISSIONS.USER_CREATE // "user:create"
74
+
75
+ // Product permissions
76
+ PERMISSIONS.PRODUCT_VIEW // "product:view"
77
+ PERMISSIONS.PRODUCT_EDIT // "product:edit"
78
+ PERMISSIONS.PRODUCT_ENABLE // "product:enable"
79
+ PERMISSIONS.PRODUCT_DISABLE // "product:disable"
80
+
81
+ // Admin permissions
82
+ PERMISSIONS.ADMIN_ACCESS // "admin:access"
83
+ PERMISSIONS.SUPER_ADMIN_ACCESS // "super_admin:access"
84
+ ```
85
+
86
+ ## USER_ROLES
87
+
88
+ Cognito group names for user roles.
89
+
90
+ ```typescript
91
+ USER_ROLES.SUPER_ADMIN // "SUPER_ADMINS"
92
+ USER_ROLES.ADMIN // "ADMINS"
93
+ USER_ROLES.BRAND_ADMIN // "BRAND_ADMINS"
94
+ USER_ROLES.BRAND_USER // "BRAND_USERS"
95
+ USER_ROLES.USER // "USERS"
96
+ ```
97
+
98
+ ## Usage Examples
99
+
100
+ ### Route Navigation
101
+
102
+ ```typescript
103
+ import { ROUTES } from '@htlkg/core/constants';
104
+
105
+ // Static routes
106
+ window.location.href = ROUTES.AUTH.LOGIN;
107
+
108
+ // Dynamic routes
109
+ const brandId = '123';
110
+ window.location.href = ROUTES.BRAND.ADMIN(brandId);
111
+ ```
112
+
113
+ ### Permission Checks
114
+
115
+ ```typescript
116
+ import { PERMISSIONS } from '@htlkg/core/constants';
117
+
118
+ function canEditBrand(userPermissions: string[]): boolean {
119
+ return userPermissions.includes(PERMISSIONS.BRAND_EDIT);
120
+ }
121
+ ```
122
+
123
+ ### Role Checks
124
+
125
+ ```typescript
126
+ import { USER_ROLES } from '@htlkg/core/constants';
127
+
128
+ function isAdmin(userRoles: string[]): boolean {
129
+ return userRoles.includes(USER_ROLES.ADMIN) ||
130
+ userRoles.includes(USER_ROLES.SUPER_ADMIN);
131
+ }
132
+ ```