create-nativecore 0.1.1 → 0.2.0

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 (175) hide show
  1. package/README.md +6 -14
  2. package/bin/index.mjs +402 -431
  3. package/package.json +3 -2
  4. package/template/.env.example +28 -0
  5. package/template/.htmlhintrc +14 -0
  6. package/template/api/data/dashboard.json +11 -0
  7. package/template/api/data/users.json +18 -0
  8. package/template/api/mockApi.js +161 -0
  9. package/template/assets/icon.svg +13 -0
  10. package/template/assets/logo.svg +25 -0
  11. package/template/eslint.config.js +94 -0
  12. package/template/index.html +137 -0
  13. package/template/manifest.json +19 -0
  14. package/template/public/.well-known/security.txt +9 -0
  15. package/template/public/_headers +24 -0
  16. package/template/public/_redirects +14 -0
  17. package/template/public/assets/icon.svg +13 -0
  18. package/template/public/assets/logo.svg +25 -0
  19. package/template/public/manifest.json +19 -0
  20. package/template/public/robots.txt +13 -0
  21. package/template/public/sitemap.xml +27 -0
  22. package/template/scripts/build-for-bots.mjs +121 -0
  23. package/template/scripts/convert-to-ts.mjs +106 -0
  24. package/template/scripts/fix-encoding.mjs +38 -0
  25. package/template/scripts/fix-svg-paths.mjs +32 -0
  26. package/template/scripts/generate-cf-router.mjs +52 -0
  27. package/template/scripts/inject-dev-tools.mjs +41 -0
  28. package/template/scripts/inject-version.mjs +65 -0
  29. package/template/scripts/make-component.mjs +445 -0
  30. package/template/scripts/make-component.mjs.backup +432 -0
  31. package/template/scripts/make-controller.mjs +119 -0
  32. package/template/scripts/make-core-component.mjs +303 -0
  33. package/template/scripts/make-view.mjs +346 -0
  34. package/template/scripts/minify.mjs +71 -0
  35. package/template/scripts/prepare-static-assets.mjs +141 -0
  36. package/template/scripts/prompt-bot-build.mjs +223 -0
  37. package/template/scripts/remove-component.mjs +170 -0
  38. package/template/scripts/remove-core-component.mjs +156 -0
  39. package/template/scripts/remove-dev.mjs +13 -0
  40. package/template/scripts/remove-view.mjs +200 -0
  41. package/template/scripts/strip-dev-blocks.mjs +30 -0
  42. package/template/scripts/watch-compile.mjs +69 -0
  43. package/template/server.js +1066 -0
  44. package/template/src/app.ts +115 -0
  45. package/template/src/components/appRegistry.ts +8 -0
  46. package/template/src/components/core/app-footer.ts +27 -0
  47. package/template/src/components/core/app-header.ts +175 -0
  48. package/template/src/components/core/app-sidebar.ts +238 -0
  49. package/template/src/components/core/loading-spinner.ts +25 -0
  50. package/template/src/components/core/nc-a.ts +313 -0
  51. package/template/src/components/core/nc-accordion.ts +186 -0
  52. package/template/src/components/core/nc-alert.ts +153 -0
  53. package/template/src/components/core/nc-animation.ts +1150 -0
  54. package/template/src/components/core/nc-autocomplete.ts +271 -0
  55. package/template/src/components/core/nc-avatar-group.ts +113 -0
  56. package/template/src/components/core/nc-avatar.ts +148 -0
  57. package/template/src/components/core/nc-badge.ts +86 -0
  58. package/template/src/components/core/nc-bottom-nav.ts +214 -0
  59. package/template/src/components/core/nc-breadcrumb.ts +96 -0
  60. package/template/src/components/core/nc-button.ts +307 -0
  61. package/template/src/components/core/nc-card.ts +160 -0
  62. package/template/src/components/core/nc-checkbox.ts +282 -0
  63. package/template/src/components/core/nc-chip.ts +115 -0
  64. package/template/src/components/core/nc-code.ts +314 -0
  65. package/template/src/components/core/nc-collapsible.ts +154 -0
  66. package/template/src/components/core/nc-color-picker.ts +268 -0
  67. package/template/src/components/core/nc-copy-button.ts +119 -0
  68. package/template/src/components/core/nc-date-picker.ts +443 -0
  69. package/template/src/components/core/nc-div.ts +280 -0
  70. package/template/src/components/core/nc-divider.ts +81 -0
  71. package/template/src/components/core/nc-drawer.ts +230 -0
  72. package/template/src/components/core/nc-dropdown.ts +178 -0
  73. package/template/src/components/core/nc-empty-state.ts +134 -0
  74. package/template/src/components/core/nc-file-upload.ts +354 -0
  75. package/template/src/components/core/nc-form.ts +312 -0
  76. package/template/src/components/core/nc-image.ts +184 -0
  77. package/template/src/components/core/nc-input.ts +383 -0
  78. package/template/src/components/core/nc-kbd.ts +48 -0
  79. package/template/src/components/core/nc-menu-item.ts +193 -0
  80. package/template/src/components/core/nc-menu.ts +376 -0
  81. package/template/src/components/core/nc-modal.ts +238 -0
  82. package/template/src/components/core/nc-nav-item.ts +151 -0
  83. package/template/src/components/core/nc-number-input.ts +350 -0
  84. package/template/src/components/core/nc-otp-input.ts +235 -0
  85. package/template/src/components/core/nc-pagination.ts +178 -0
  86. package/template/src/components/core/nc-popover.ts +260 -0
  87. package/template/src/components/core/nc-progress-circular.ts +119 -0
  88. package/template/src/components/core/nc-progress.ts +134 -0
  89. package/template/src/components/core/nc-radio.ts +235 -0
  90. package/template/src/components/core/nc-rating.ts +266 -0
  91. package/template/src/components/core/nc-rich-text.ts +283 -0
  92. package/template/src/components/core/nc-scroll-top.ts +116 -0
  93. package/template/src/components/core/nc-select.ts +452 -0
  94. package/template/src/components/core/nc-skeleton.ts +107 -0
  95. package/template/src/components/core/nc-slider.ts +285 -0
  96. package/template/src/components/core/nc-snackbar.ts +230 -0
  97. package/template/src/components/core/nc-splash.ts +343 -0
  98. package/template/src/components/core/nc-stepper.ts +247 -0
  99. package/template/src/components/core/nc-switch.ts +281 -0
  100. package/template/src/components/core/nc-tab-item.ts +138 -0
  101. package/template/src/components/core/nc-table.ts +279 -0
  102. package/template/src/components/core/nc-tabs.ts +554 -0
  103. package/template/src/components/core/nc-tag-input.ts +279 -0
  104. package/template/src/components/core/nc-textarea.ts +216 -0
  105. package/template/src/components/core/nc-time-picker.ts +438 -0
  106. package/template/src/components/core/nc-timeline.ts +186 -0
  107. package/template/src/components/core/nc-tooltip.ts +143 -0
  108. package/template/src/components/frameworkRegistry.ts +68 -0
  109. package/template/src/components/preloadRegistry.ts +28 -0
  110. package/template/src/components/registry.ts +8 -0
  111. package/template/src/components/ui/dashboard-signal-lab.ts +284 -0
  112. package/template/src/constants/apiEndpoints.ts +27 -0
  113. package/template/src/constants/errorMessages.ts +23 -0
  114. package/template/src/constants/index.ts +8 -0
  115. package/template/src/constants/routePaths.ts +15 -0
  116. package/template/src/constants/storageKeys.ts +18 -0
  117. package/template/src/controllers/dashboard.controller.ts +200 -0
  118. package/template/src/controllers/home.controller.ts +21 -0
  119. package/template/src/controllers/index.ts +11 -0
  120. package/template/src/controllers/login.controller.ts +131 -0
  121. package/template/src/core/component.ts +354 -0
  122. package/template/src/core/errorHandler.ts +85 -0
  123. package/template/src/core/gpu-animation.ts +604 -0
  124. package/template/src/core/http.ts +173 -0
  125. package/template/src/core/lazyComponents.ts +90 -0
  126. package/template/src/core/router.ts +642 -0
  127. package/template/src/core/signals.ts +146 -0
  128. package/template/src/core/state.ts +248 -0
  129. package/template/src/dev/component-editor.ts +1363 -0
  130. package/template/src/dev/component-overlay.ts +278 -0
  131. package/template/src/dev/context-menu.ts +223 -0
  132. package/template/src/dev/denc-tools.ts +250 -0
  133. package/template/src/dev/hmr.ts +189 -0
  134. package/template/src/dev/nfbs.code-workspace +27 -0
  135. package/template/src/dev/outline-panel.ts +1247 -0
  136. package/template/src/middleware/auth.middleware.ts +23 -0
  137. package/template/src/routes/routes.ts +38 -0
  138. package/template/src/services/api.service.ts +394 -0
  139. package/template/src/services/auth.service.ts +176 -0
  140. package/template/src/services/index.ts +8 -0
  141. package/template/src/services/logger.service.ts +74 -0
  142. package/template/src/services/storage.service.ts +88 -0
  143. package/template/src/stores/appStore.ts +57 -0
  144. package/template/src/stores/uiStore.ts +36 -0
  145. package/template/src/styles/core-variables.css +219 -0
  146. package/template/src/styles/core.css +710 -0
  147. package/template/src/styles/main.css +3164 -0
  148. package/template/src/styles/variables.css +152 -0
  149. package/template/src/types/global.d.ts +47 -0
  150. package/template/src/utils/cacheBuster.ts +20 -0
  151. package/template/src/utils/dom.ts +149 -0
  152. package/template/src/utils/events.ts +203 -0
  153. package/template/src/utils/form.ts +176 -0
  154. package/template/src/utils/formatters.ts +169 -0
  155. package/template/src/utils/helpers.ts +195 -0
  156. package/template/src/utils/markdown.ts +307 -0
  157. package/template/src/utils/sidebar.ts +96 -0
  158. package/template/src/utils/smoothScroll.ts +85 -0
  159. package/template/src/utils/templates.ts +23 -0
  160. package/template/src/utils/validation.ts +73 -0
  161. package/template/src/views/protected/dashboard.html +293 -0
  162. package/template/src/views/public/home.html +150 -0
  163. package/template/src/views/public/login.html +102 -0
  164. package/template/tests/unit/component.test.ts +87 -0
  165. package/template/tests/unit/computed.test.ts +79 -0
  166. package/template/tests/unit/form.test.ts +68 -0
  167. package/template/tests/unit/formatters.test.ts +49 -0
  168. package/template/tests/unit/lazy-components.test.ts +59 -0
  169. package/template/tests/unit/markdown.test.ts +62 -0
  170. package/template/tests/unit/router.test.ts +112 -0
  171. package/template/tests/unit/signals.test.ts +54 -0
  172. package/template/tests/unit/validation.test.ts +50 -0
  173. package/template/tsconfig.build.json +21 -0
  174. package/template/tsconfig.json +51 -0
  175. package/template/vitest.config.ts +36 -0
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Authentication Middleware
3
+ * Enforces access control for protected routes in the single-shell SPA
4
+ */
5
+ import auth from '../services/auth.service.js';
6
+ import router from '../core/router.js';
7
+ import { protectedRoutes } from '../routes/routes.js';
8
+ import type { RouteMatch } from '../core/router.js';
9
+
10
+ export async function authMiddleware(route: RouteMatch): Promise<boolean> {
11
+ const isProtected = protectedRoutes.some(path => route.path.startsWith(path));
12
+ const isAuthenticated = auth.isAuthenticated();
13
+
14
+ // Protected route accessed without authentication
15
+ // Redirect to login page
16
+ if (isProtected && !isAuthenticated) {
17
+ router.replace('/login');
18
+ return false;
19
+ }
20
+
21
+ // Note: Shell switching is now handled by the router before middleware runs
22
+ return true;
23
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Route Configuration
3
+ */
4
+ import { bustCache } from '../utils/cacheBuster.js';
5
+ import type { ControllerFunction } from '../core/router.js';
6
+
7
+ function lazyController(controllerName: string, controllerPath: string): ControllerFunction {
8
+ return async (...args: any[]) => {
9
+ const module = await import(bustCache(controllerPath));
10
+ return module[controllerName](...args);
11
+ };
12
+ }
13
+
14
+ export function registerRoutes(router: any): void {
15
+ router
16
+ // Static marketing pages — cache for 5 min, serve stale instantly while refreshing in background
17
+ .register('/', 'src/views/public/home.html', lazyController('homeController', '../controllers/home.controller.js'))
18
+ .cache({ ttl: 300, revalidate: true }) .cache({ ttl: 600, revalidate: true })
19
+ .register('/docs', 'src/views/public/docs.html', lazyController('docsController', '../controllers/docs.controller.js'))
20
+ .cache({ ttl: 300 })
21
+
22
+ // Auth page — never cache (always fresh)
23
+ .register('/login', 'src/views/public/login.html', lazyController('loginController', '../controllers/login.controller.js'))
24
+
25
+ // Component library — cache for 2 min, block on stale (content changes rarely but must be accurate)
26
+ .register('/components', 'src/views/public/components.html', lazyController('componentsController', '../controllers/components.controller.js'))
27
+ .cache({ ttl: 120 })
28
+
29
+ // Protected pages — short cache, revalidate in background
30
+ .register('/dashboard', 'src/views/protected/dashboard.html', lazyController('dashboardController', '../controllers/dashboard.controller.js'))
31
+ .cache({ ttl: 30, revalidate: true })
32
+ .register('/under-construction', 'src/views/protected/under-construction.html')
33
+ .register('/user/:id', 'src/views/protected/user-detail.html', lazyController('userDetailController', '../controllers/user-detail.controller.js'))
34
+ .cache({ ttl: 60, revalidate: true })
35
+
36
+ .register('/testpage', 'src/views/public/testpage.html', lazyController('testpageController', '../controllers/testpage.controller.js'))}
37
+
38
+ export const protectedRoutes = ['/dashboard', '/user', '/under-construction'];
@@ -0,0 +1,394 @@
1
+ /**
2
+ * API Service
3
+ * Reusable service for making backend API calls
4
+ */
5
+ import auth from './auth.service.js';
6
+
7
+ type QueryPrimitive = string | number | boolean | null;
8
+ type QueryKeyValue = QueryPrimitive | QueryKeyObject | QueryKeyValue[];
9
+ type QueryKeyObject = { [key: string]: QueryKeyValue | undefined };
10
+
11
+ export type ApiQueryKey = readonly QueryKeyValue[];
12
+
13
+ export interface ApiCacheOptions {
14
+ ttl: number;
15
+ revalidate?: boolean;
16
+ params?: Record<string, string | number | boolean | null | undefined>;
17
+ forceRefresh?: boolean;
18
+ cacheKey?: string;
19
+ queryKey?: ApiQueryKey;
20
+ tags?: string[];
21
+ }
22
+
23
+ interface CacheEntry<T = unknown> {
24
+ data: T;
25
+ cachedAt: number;
26
+ ttl: number;
27
+ queryKey?: ApiQueryKey;
28
+ tags: string[];
29
+ }
30
+
31
+ class ApiService {
32
+ private baseURL = this.resolveBaseURL();
33
+ private defaultHeaders: Record<string, string> = {
34
+ 'Content-Type': 'application/json',
35
+ };
36
+ private responseCache = new Map<string, CacheEntry>();
37
+ private inFlightRequests = new Map<string, Promise<unknown>>();
38
+ private queryKeyIndex = new Map<string, Set<string>>();
39
+ private tagIndex = new Map<string, Set<string>>();
40
+
41
+ constructor() {
42
+ auth.subscribe(event => {
43
+ if (event === 'logout') {
44
+ this.clearCache();
45
+ }
46
+ });
47
+ }
48
+
49
+ private resolveBaseURL(): string {
50
+ if (typeof window === 'undefined') {
51
+ return '/api';
52
+ }
53
+
54
+ const hostname = window.location.hostname;
55
+ if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname.startsWith('192.168.') || hostname.endsWith('.local')) {
56
+ return '/api';
57
+ }
58
+
59
+ return 'https://api.nativecorejs.com';
60
+ }
61
+
62
+ private isAbsoluteURL(endpoint: string): boolean {
63
+ return /^https?:\/\//i.test(endpoint);
64
+ }
65
+
66
+ /**
67
+ * Set base URL
68
+ */
69
+ setBaseURL(url: string): void {
70
+ this.baseURL = url;
71
+ }
72
+
73
+ clearCache(): void {
74
+ this.responseCache.clear();
75
+ this.inFlightRequests.clear();
76
+ this.queryKeyIndex.clear();
77
+ this.tagIndex.clear();
78
+ }
79
+
80
+ invalidateCache(match: string | RegExp): void {
81
+ for (const key of Array.from(this.responseCache.keys())) {
82
+ const shouldDelete = typeof match === 'string'
83
+ ? key.includes(match)
84
+ : match.test(key);
85
+
86
+ if (shouldDelete) {
87
+ this.deleteCacheEntry(key);
88
+ }
89
+ }
90
+ }
91
+
92
+ invalidateQuery(queryKey: ApiQueryKey, options: { exact?: boolean } = {}): void {
93
+ const target = this.serializeQueryKey(queryKey);
94
+
95
+ for (const [serializedQueryKey, cacheKeys] of this.queryKeyIndex.entries()) {
96
+ const matches = options.exact
97
+ ? serializedQueryKey === target
98
+ : serializedQueryKey === target || serializedQueryKey.startsWith(`${target}|`);
99
+
100
+ if (!matches) {
101
+ continue;
102
+ }
103
+
104
+ for (const cacheKey of Array.from(cacheKeys)) {
105
+ this.deleteCacheEntry(cacheKey);
106
+ }
107
+ }
108
+ }
109
+
110
+ invalidateTags(tags: string | string[]): void {
111
+ const tagList = Array.isArray(tags) ? tags : [tags];
112
+
113
+ for (const tag of tagList) {
114
+ const cacheKeys = this.tagIndex.get(tag);
115
+ if (!cacheKeys) {
116
+ continue;
117
+ }
118
+
119
+ for (const cacheKey of Array.from(cacheKeys)) {
120
+ this.deleteCacheEntry(cacheKey);
121
+ }
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Make HTTP request
127
+ */
128
+ async request<T = any>(endpoint: string, options: RequestInit = {}): Promise<T> {
129
+ const url = this.isAbsoluteURL(endpoint) ? endpoint : `${this.baseURL}${endpoint}`;
130
+
131
+ const config: RequestInit = {
132
+ ...options,
133
+ headers: {
134
+ ...this.defaultHeaders,
135
+ ...auth.getAuthHeader(),
136
+ ...options.headers,
137
+ } as HeadersInit,
138
+ };
139
+
140
+ try {
141
+ const response = await fetch(url, config);
142
+
143
+ const data = await this.parseResponse(response);
144
+
145
+ if (!response.ok) {
146
+ // Don't logout on 401 for login endpoint - it's just invalid credentials
147
+ if (response.status === 401 && !endpoint.includes('/auth/login')) {
148
+ auth.logout();
149
+ window.dispatchEvent(new CustomEvent('unauthorized'));
150
+ throw new Error('Unauthorized - please login again');
151
+ }
152
+
153
+ // Try to get error message from response (supports both 'error' and 'message' fields)
154
+ const errorMessage = (data as any).error || (data as any).message || `HTTP ${response.status}`;
155
+ throw new Error(errorMessage);
156
+ }
157
+
158
+ return data as T;
159
+ } catch (error) {
160
+ console.error('API Error:', error);
161
+ throw error;
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Parse response
167
+ */
168
+ private async parseResponse(response: Response): Promise<any> {
169
+ const contentType = response.headers.get('content-type');
170
+
171
+ if (contentType && contentType.includes('application/json')) {
172
+ return await response.json();
173
+ }
174
+
175
+ return await response.text();
176
+ }
177
+
178
+ /**
179
+ * GET request
180
+ */
181
+ async get<T = any>(endpoint: string, params: Record<string, any> = {}): Promise<T> {
182
+ const queryString = new URLSearchParams(params).toString();
183
+ const url = queryString ? `${endpoint}?${queryString}` : endpoint;
184
+
185
+ return this.request<T>(url, { method: 'GET' });
186
+ }
187
+
188
+ async getCached<T = any>(endpoint: string, options: ApiCacheOptions): Promise<T> {
189
+ const cacheKey = this.buildCacheKey(endpoint, options);
190
+ const cached = this.responseCache.get(cacheKey) as CacheEntry<T> | undefined;
191
+ const now = Date.now();
192
+ const ttlMs = Math.max(options.ttl, 0) * 1000;
193
+ const isFresh = !!cached && ttlMs > 0 && (now - cached.cachedAt) < ttlMs;
194
+ const isStale = !!cached && ttlMs > 0 && !isFresh;
195
+
196
+ if (!options.forceRefresh) {
197
+ if (isFresh && cached) {
198
+ return cached.data;
199
+ }
200
+
201
+ if (isStale && cached && options.revalidate) {
202
+ this.refreshCachedRequest<T>(cacheKey, endpoint, options);
203
+ return cached.data;
204
+ }
205
+ }
206
+
207
+ if (!options.forceRefresh) {
208
+ const inFlight = this.inFlightRequests.get(cacheKey) as Promise<T> | undefined;
209
+ if (inFlight) {
210
+ return inFlight;
211
+ }
212
+ }
213
+
214
+ const requestPromise = this.get<T>(endpoint, options.params ?? {})
215
+ .then(data => {
216
+ this.setCacheEntry(cacheKey, {
217
+ data,
218
+ cachedAt: Date.now(),
219
+ ttl: options.ttl,
220
+ queryKey: options.queryKey,
221
+ tags: options.tags ?? [],
222
+ });
223
+ this.inFlightRequests.delete(cacheKey);
224
+ return data;
225
+ })
226
+ .catch(error => {
227
+ this.inFlightRequests.delete(cacheKey);
228
+ throw error;
229
+ });
230
+
231
+ this.inFlightRequests.set(cacheKey, requestPromise as Promise<unknown>);
232
+ return requestPromise;
233
+ }
234
+
235
+ /**
236
+ * POST request
237
+ */
238
+ async post<T = any>(endpoint: string, body: any = {}): Promise<T> {
239
+ return this.request<T>(endpoint, {
240
+ method: 'POST',
241
+ body: JSON.stringify(body),
242
+ });
243
+ }
244
+
245
+ /**
246
+ * PUT request
247
+ */
248
+ async put<T = any>(endpoint: string, body: any = {}): Promise<T> {
249
+ return this.request<T>(endpoint, {
250
+ method: 'PUT',
251
+ body: JSON.stringify(body),
252
+ });
253
+ }
254
+
255
+ /**
256
+ * PATCH request
257
+ */
258
+ async patch<T = any>(endpoint: string, body: any = {}): Promise<T> {
259
+ return this.request<T>(endpoint, {
260
+ method: 'PATCH',
261
+ body: JSON.stringify(body),
262
+ });
263
+ }
264
+
265
+ /**
266
+ * DELETE request
267
+ */
268
+ async delete<T = any>(endpoint: string): Promise<T> {
269
+ return this.request<T>(endpoint, { method: 'DELETE' });
270
+ }
271
+
272
+ private buildCacheKey(endpoint: string, options: ApiCacheOptions): string {
273
+ if (options.queryKey) {
274
+ return `q:${this.serializeQueryKey(options.queryKey)}`;
275
+ }
276
+
277
+ if (options.cacheKey) {
278
+ return options.cacheKey;
279
+ }
280
+
281
+ const queryString = new URLSearchParams(
282
+ Object.entries(options.params ?? {}).reduce<Record<string, string>>((accumulator, [key, value]) => {
283
+ if (value !== undefined && value !== null) {
284
+ accumulator[key] = String(value);
285
+ }
286
+ return accumulator;
287
+ }, {})
288
+ ).toString();
289
+
290
+ return queryString ? `${endpoint}?${queryString}` : endpoint;
291
+ }
292
+
293
+ private refreshCachedRequest<T>(cacheKey: string, endpoint: string, options: ApiCacheOptions): void {
294
+ if (this.inFlightRequests.has(cacheKey)) {
295
+ return;
296
+ }
297
+
298
+ const refreshPromise = this.get<T>(endpoint, options.params ?? {})
299
+ .then(data => {
300
+ this.setCacheEntry(cacheKey, {
301
+ data,
302
+ cachedAt: Date.now(),
303
+ ttl: options.ttl,
304
+ queryKey: options.queryKey,
305
+ tags: options.tags ?? [],
306
+ });
307
+ this.inFlightRequests.delete(cacheKey);
308
+ return data;
309
+ })
310
+ .catch(() => {
311
+ this.inFlightRequests.delete(cacheKey);
312
+ return undefined;
313
+ });
314
+
315
+ this.inFlightRequests.set(cacheKey, refreshPromise as Promise<unknown>);
316
+ }
317
+
318
+ private setCacheEntry<T>(cacheKey: string, entry: CacheEntry<T>): void {
319
+ this.deleteCacheEntry(cacheKey);
320
+ this.responseCache.set(cacheKey, entry);
321
+
322
+ if (entry.queryKey) {
323
+ const serializedQueryKey = this.serializeQueryKey(entry.queryKey);
324
+ const existingKeys = this.queryKeyIndex.get(serializedQueryKey) ?? new Set<string>();
325
+ existingKeys.add(cacheKey);
326
+ this.queryKeyIndex.set(serializedQueryKey, existingKeys);
327
+ }
328
+
329
+ for (const tag of entry.tags) {
330
+ const existingKeys = this.tagIndex.get(tag) ?? new Set<string>();
331
+ existingKeys.add(cacheKey);
332
+ this.tagIndex.set(tag, existingKeys);
333
+ }
334
+ }
335
+
336
+ private deleteCacheEntry(cacheKey: string): void {
337
+ const existingEntry = this.responseCache.get(cacheKey);
338
+
339
+ this.responseCache.delete(cacheKey);
340
+ this.inFlightRequests.delete(cacheKey);
341
+
342
+ if (!existingEntry) {
343
+ return;
344
+ }
345
+
346
+ if (existingEntry.queryKey) {
347
+ const serializedQueryKey = this.serializeQueryKey(existingEntry.queryKey);
348
+ const existingKeys = this.queryKeyIndex.get(serializedQueryKey);
349
+ if (existingKeys) {
350
+ existingKeys.delete(cacheKey);
351
+ if (existingKeys.size === 0) {
352
+ this.queryKeyIndex.delete(serializedQueryKey);
353
+ }
354
+ }
355
+ }
356
+
357
+ for (const tag of existingEntry.tags) {
358
+ const existingKeys = this.tagIndex.get(tag);
359
+ if (existingKeys) {
360
+ existingKeys.delete(cacheKey);
361
+ if (existingKeys.size === 0) {
362
+ this.tagIndex.delete(tag);
363
+ }
364
+ }
365
+ }
366
+ }
367
+
368
+ private serializeQueryKey(queryKey: ApiQueryKey): string {
369
+ return queryKey.map(part => this.stableSerialize(part)).join('|');
370
+ }
371
+
372
+ private stableSerialize(value: QueryKeyValue | undefined): string {
373
+ if (value === undefined) {
374
+ return 'undefined';
375
+ }
376
+
377
+ if (value === null) {
378
+ return 'null';
379
+ }
380
+
381
+ if (Array.isArray(value)) {
382
+ return `[${value.map(item => this.stableSerialize(item)).join(',')}]`;
383
+ }
384
+
385
+ if (typeof value === 'object') {
386
+ const sortedKeys = Object.keys(value).sort();
387
+ return `{${sortedKeys.map(key => `${key}:${this.stableSerialize(value[key])}`).join(',')}}`;
388
+ }
389
+
390
+ return JSON.stringify(value);
391
+ }
392
+ }
393
+
394
+ export default new ApiService();
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Authentication Service
3
+ * Handles JWT token management and user authentication
4
+ */
5
+ import storage from './storage.service.js';
6
+
7
+ // Types
8
+ export interface User {
9
+ id: string | number;
10
+ email: string;
11
+ name?: string;
12
+ [key: string]: any;
13
+ }
14
+
15
+ export interface TokenPayload {
16
+ exp?: number;
17
+ [key: string]: any;
18
+ }
19
+
20
+ type AuthEvent = 'login' | 'logout';
21
+ type AuthCallback = (event: AuthEvent) => void;
22
+
23
+ class AuthService {
24
+ private readonly TOKEN_KEY = 'access_token';
25
+ private readonly REFRESH_TOKEN_KEY = 'refresh_token';
26
+ private readonly USER_KEY = 'user_data';
27
+ private listeners: AuthCallback[] = [];
28
+
29
+ constructor() {
30
+ storage.setStrategy('session');
31
+ }
32
+
33
+ /**
34
+ * Set authentication tokens
35
+ */
36
+ setTokens(accessToken: string, refreshToken: string | null = null): void {
37
+ if (!accessToken || accessToken === 'undefined') {
38
+ console.error('Cannot set undefined token');
39
+ return;
40
+ }
41
+ storage.set(this.TOKEN_KEY, accessToken);
42
+ if (refreshToken && refreshToken !== 'undefined') {
43
+ storage.set(this.REFRESH_TOKEN_KEY, refreshToken);
44
+ }
45
+ this.notifyListeners('login');
46
+ window.dispatchEvent(new CustomEvent('auth-change'));
47
+ }
48
+
49
+ /**
50
+ * Get access token
51
+ */
52
+ getToken(): string | null {
53
+ const token = storage.get(this.TOKEN_KEY);
54
+ if (!token || token === 'undefined' || token === 'null') {
55
+ return null;
56
+ }
57
+ return token;
58
+ }
59
+
60
+ /**
61
+ * Get refresh token
62
+ */
63
+ getRefreshToken(): string | null {
64
+ return storage.get(this.REFRESH_TOKEN_KEY);
65
+ }
66
+
67
+ /**
68
+ * Set user data
69
+ */
70
+ setUser(user: User): void {
71
+ storage.set(this.USER_KEY, JSON.stringify(user));
72
+ }
73
+
74
+ /**
75
+ * Get user data
76
+ */
77
+ getUser(): User | null {
78
+ const userData = storage.get(this.USER_KEY);
79
+ if (!userData || userData === 'undefined' || userData === 'null') {
80
+ return null;
81
+ }
82
+ try {
83
+ return JSON.parse(userData);
84
+ } catch (error) {
85
+ console.error('Error parsing user data:', error);
86
+ return null;
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Check if user is authenticated
92
+ */
93
+ isAuthenticated(): boolean {
94
+ const token = this.getToken();
95
+ if (!token) return false;
96
+
97
+ if (this.isTokenExpired(token)) {
98
+ this.logout();
99
+ return false;
100
+ }
101
+
102
+ return true;
103
+ }
104
+
105
+ /**
106
+ * Check if JWT token is expired
107
+ */
108
+ isTokenExpired(token: string): boolean {
109
+ try {
110
+ const payload = this.decodeToken(token);
111
+ if (!payload.exp) return false;
112
+
113
+ const currentTime = Date.now() / 1000;
114
+ return payload.exp < currentTime;
115
+ } catch {
116
+ return true;
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Decode JWT token
122
+ */
123
+ decodeToken(token: string): TokenPayload {
124
+ try {
125
+ const base64Url = token.split('.')[1];
126
+ if (!base64Url) {
127
+ throw new Error('Invalid token');
128
+ }
129
+
130
+ const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
131
+ const paddedBase64 = base64.padEnd(base64.length + ((4 - base64.length % 4) % 4), '=');
132
+ const jsonPayload = atob(paddedBase64);
133
+ return JSON.parse(jsonPayload);
134
+ } catch {
135
+ throw new Error('Invalid token');
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Logout user
141
+ */
142
+ logout(): void {
143
+ storage.remove(this.TOKEN_KEY);
144
+ storage.remove(this.REFRESH_TOKEN_KEY);
145
+ storage.remove(this.USER_KEY);
146
+ this.notifyListeners('logout');
147
+ window.dispatchEvent(new CustomEvent('auth-change'));
148
+ }
149
+
150
+ /**
151
+ * Subscribe to auth state changes
152
+ */
153
+ subscribe(callback: AuthCallback): () => void {
154
+ this.listeners.push(callback);
155
+ return () => {
156
+ this.listeners = this.listeners.filter(cb => cb !== callback);
157
+ };
158
+ }
159
+
160
+ /**
161
+ * Notify listeners
162
+ */
163
+ private notifyListeners(event: AuthEvent): void {
164
+ this.listeners.forEach(callback => callback(event));
165
+ }
166
+
167
+ /**
168
+ * Get authorization header
169
+ */
170
+ getAuthHeader(): Record<string, string> {
171
+ const token = this.getToken();
172
+ return token ? { 'Authorization': `Bearer ${token}` } : {};
173
+ }
174
+ }
175
+
176
+ export default new AuthService();
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Services Index
3
+ */
4
+
5
+ export { default as api } from './api.service.js';
6
+ export { default as auth } from './auth.service.js';
7
+ export { default as storage } from './storage.service.js';
8
+ export { default as logger } from './logger.service.js';