mbkauthe 2.5.0 → 3.0.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.
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Centralized error messages and error codes for mbkauthe
3
+ * Provides consistent, user-friendly error messages across the application
4
+ */
5
+
6
+ // Error codes for different scenarios
7
+ export const ErrorCodes = {
8
+ // Authentication errors (600-699)
9
+ INVALID_CREDENTIALS: 601,
10
+ USER_NOT_FOUND: 602,
11
+ INCORRECT_PASSWORD: 603,
12
+ ACCOUNT_INACTIVE: 604,
13
+ APP_NOT_AUTHORIZED: 605,
14
+
15
+ // 2FA errors (700-799)
16
+ TWO_FA_REQUIRED: 701,
17
+ TWO_FA_INVALID_TOKEN: 702,
18
+ TWO_FA_NOT_CONFIGURED: 703,
19
+ TWO_FA_EXPIRED: 704,
20
+
21
+ // Session errors (800-899)
22
+ SESSION_EXPIRED: 801,
23
+ SESSION_INVALID: 802,
24
+ SESSION_NOT_FOUND: 803,
25
+
26
+ // Authorization errors (900-999)
27
+ INSUFFICIENT_PERMISSIONS: 901,
28
+ ROLE_NOT_ALLOWED: 902,
29
+
30
+ // Input validation errors (1000-1099)
31
+ MISSING_REQUIRED_FIELD: 1001,
32
+ INVALID_USERNAME_FORMAT: 1002,
33
+ INVALID_PASSWORD_LENGTH: 1003,
34
+ INVALID_TOKEN_FORMAT: 1004,
35
+
36
+ // Rate limiting (1100-1199)
37
+ RATE_LIMIT_EXCEEDED: 1101,
38
+
39
+ // Server errors (1200-1299)
40
+ INTERNAL_SERVER_ERROR: 1201,
41
+ DATABASE_ERROR: 1202,
42
+ CONFIGURATION_ERROR: 1203,
43
+
44
+ // GitHub OAuth errors (1300-1399)
45
+ GITHUB_NOT_LINKED: 1301,
46
+ GITHUB_AUTH_FAILED: 1302,
47
+ OAUTH_STATE_MISMATCH: 1303,
48
+ };
49
+
50
+ // User-friendly error messages
51
+ export const ErrorMessages = {
52
+ // Authentication
53
+ [ErrorCodes.INVALID_CREDENTIALS]: {
54
+ message: "Invalid username or password",
55
+ userMessage: "The username or password you entered is incorrect. Please try again.",
56
+ hint: "Check your spelling and make sure Caps Lock is off"
57
+ },
58
+ [ErrorCodes.USER_NOT_FOUND]: {
59
+ message: "User account not found",
60
+ userMessage: "We couldn't find an account with that username.",
61
+ hint: "Please check the username and try again"
62
+ },
63
+ [ErrorCodes.INCORRECT_PASSWORD]: {
64
+ message: "Incorrect password",
65
+ userMessage: "The password you entered is incorrect.",
66
+ hint: "Make sure you're using the correct password for this account"
67
+ },
68
+ [ErrorCodes.ACCOUNT_INACTIVE]: {
69
+ message: "Account is inactive",
70
+ userMessage: "Your account has been deactivated.",
71
+ hint: "Please contact your administrator to reactivate your account"
72
+ },
73
+ [ErrorCodes.APP_NOT_AUTHORIZED]: {
74
+ message: "Not authorized for this application",
75
+ userMessage: "You don't have permission to access this application.",
76
+ hint: "Contact your administrator if you believe this is an error"
77
+ },
78
+
79
+ // 2FA
80
+ [ErrorCodes.TWO_FA_REQUIRED]: {
81
+ message: "Two-factor authentication required",
82
+ userMessage: "Please enter your 6-digit authentication code.",
83
+ hint: "Check your authenticator app for the code"
84
+ },
85
+ [ErrorCodes.TWO_FA_INVALID_TOKEN]: {
86
+ message: "Invalid 2FA code",
87
+ userMessage: "The authentication code you entered is incorrect.",
88
+ hint: "Make sure you're using the latest code from your authenticator app"
89
+ },
90
+ [ErrorCodes.TWO_FA_NOT_CONFIGURED]: {
91
+ message: "2FA not configured",
92
+ userMessage: "Two-factor authentication is not set up for your account.",
93
+ hint: "Contact your administrator to enable 2FA"
94
+ },
95
+ [ErrorCodes.TWO_FA_EXPIRED]: {
96
+ message: "2FA code expired",
97
+ userMessage: "The authentication code has expired.",
98
+ hint: "Please use a fresh code from your authenticator app"
99
+ },
100
+
101
+ // Session
102
+ [ErrorCodes.SESSION_EXPIRED]: {
103
+ message: "Session expired",
104
+ userMessage: "Your session has expired. Please log in again.",
105
+ hint: "This happens when you've been inactive for too long"
106
+ },
107
+ [ErrorCodes.SESSION_INVALID]: {
108
+ message: "Invalid session",
109
+ userMessage: "Your session is no longer valid. Please log in again.",
110
+ hint: "This may happen if you logged in from another device"
111
+ },
112
+ [ErrorCodes.SESSION_NOT_FOUND]: {
113
+ message: "Session not found",
114
+ userMessage: "Please log in to continue.",
115
+ hint: "You need to be logged in to access this page"
116
+ },
117
+
118
+ // Authorization
119
+ [ErrorCodes.INSUFFICIENT_PERMISSIONS]: {
120
+ message: "Insufficient permissions",
121
+ userMessage: "You don't have permission to perform this action.",
122
+ hint: "Contact your administrator if you need access"
123
+ },
124
+ [ErrorCodes.ROLE_NOT_ALLOWED]: {
125
+ message: "Role not allowed",
126
+ userMessage: "Your account role doesn't have access to this feature.",
127
+ hint: "This feature requires a different permission level"
128
+ },
129
+
130
+ // Input Validation
131
+ [ErrorCodes.MISSING_REQUIRED_FIELD]: {
132
+ message: "Required field missing",
133
+ userMessage: "Please fill in all required fields.",
134
+ hint: "Username and password are required"
135
+ },
136
+ [ErrorCodes.INVALID_USERNAME_FORMAT]: {
137
+ message: "Invalid username format",
138
+ userMessage: "Please enter a valid username.",
139
+ hint: "Username must be 1-255 characters"
140
+ },
141
+ [ErrorCodes.INVALID_PASSWORD_LENGTH]: {
142
+ message: "Invalid password length",
143
+ userMessage: "Password must be at least 8 characters long.",
144
+ hint: "Please use a password with 8 or more characters"
145
+ },
146
+ [ErrorCodes.INVALID_TOKEN_FORMAT]: {
147
+ message: "Invalid token format",
148
+ userMessage: "Please enter a valid 6-digit code.",
149
+ hint: "The code should be 6 numbers from your authenticator app"
150
+ },
151
+
152
+ // Rate Limiting
153
+ [ErrorCodes.RATE_LIMIT_EXCEEDED]: {
154
+ message: "Too many requests",
155
+ userMessage: "Too many attempts. Please try again later.",
156
+ hint: "Wait a few minutes before trying again"
157
+ },
158
+
159
+ // Server Errors
160
+ [ErrorCodes.INTERNAL_SERVER_ERROR]: {
161
+ message: "Internal server error",
162
+ userMessage: "Something went wrong on our end.",
163
+ hint: "Please try again later or contact support if the problem persists"
164
+ },
165
+ [ErrorCodes.DATABASE_ERROR]: {
166
+ message: "Database error",
167
+ userMessage: "We're experiencing technical difficulties.",
168
+ hint: "Please try again in a few moments"
169
+ },
170
+ [ErrorCodes.CONFIGURATION_ERROR]: {
171
+ message: "Configuration error",
172
+ userMessage: "The service is temporarily unavailable.",
173
+ hint: "Please contact your administrator"
174
+ },
175
+
176
+ // GitHub OAuth
177
+ [ErrorCodes.GITHUB_NOT_LINKED]: {
178
+ message: "GitHub account not linked",
179
+ userMessage: "Your GitHub account is not linked to any user account.",
180
+ hint: "Please link your GitHub account in your profile settings first"
181
+ },
182
+ [ErrorCodes.GITHUB_AUTH_FAILED]: {
183
+ message: "GitHub authentication failed",
184
+ userMessage: "We couldn't authenticate you with GitHub.",
185
+ hint: "Please try again or use username/password login"
186
+ },
187
+ [ErrorCodes.OAUTH_STATE_MISMATCH]: {
188
+ message: "OAuth state mismatch",
189
+ userMessage: "Authentication verification failed.",
190
+ hint: "Please try logging in again"
191
+ },
192
+ };
193
+
194
+ /**
195
+ * Get error details by error code
196
+ * @param {number} errorCode - The error code
197
+ * @param {Object} customData - Optional custom data to merge with error
198
+ * @returns {Object} Error details with message, userMessage, and hint
199
+ */
200
+ export function getErrorByCode(errorCode, customData = {}) {
201
+ const errorDetails = ErrorMessages[errorCode] || {
202
+ message: "An error occurred",
203
+ userMessage: "An unexpected error occurred. Please try again.",
204
+ hint: "Contact support if this problem continues"
205
+ };
206
+
207
+ return {
208
+ errorCode,
209
+ ...errorDetails,
210
+ ...customData
211
+ };
212
+ }
213
+
214
+ /**
215
+ * Create a standardized error response
216
+ * @param {number} statusCode - HTTP status code
217
+ * @param {number} errorCode - Application error code
218
+ * @param {Object} customData - Optional custom data
219
+ * @returns {Object} Standardized error response
220
+ */
221
+ export function createErrorResponse(statusCode, errorCode, customData = {}) {
222
+ const error = getErrorByCode(errorCode, customData);
223
+
224
+ return {
225
+ success: false,
226
+ statusCode,
227
+ errorCode: error.errorCode,
228
+ message: error.userMessage || error.message,
229
+ hint: error.hint,
230
+ timestamp: new Date().toISOString(),
231
+ ...customData
232
+ };
233
+ }
234
+
235
+ /**
236
+ * Log error with consistent format
237
+ * @param {string} context - Context where error occurred
238
+ * @param {number} errorCode - Error code
239
+ * @param {Object} additionalInfo - Additional info to log
240
+ */
241
+ export function logError(context, errorCode, additionalInfo = {}) {
242
+ const error = getErrorByCode(errorCode);
243
+ console.error(`[mbkauthe] ${context}:`, {
244
+ errorCode,
245
+ message: error.message,
246
+ ...additionalInfo,
247
+ timestamp: new Date().toISOString()
248
+ });
249
+ }
250
+
251
+ export default {
252
+ ErrorCodes,
253
+ ErrorMessages,
254
+ getErrorByCode,
255
+ createErrorResponse,
256
+ logError
257
+ };
@@ -0,0 +1,21 @@
1
+ import { mbkautheVar, packageJson } from "../config/index.js";
2
+
3
+ // Helper function to render error pages consistently
4
+ export const renderError = (res, { code, error, message, page, pagename, details }) => {
5
+ res.status(code);
6
+ const renderData = {
7
+ layout: false,
8
+ code,
9
+ error,
10
+ message,
11
+ page,
12
+ pagename,
13
+ app: mbkautheVar.APP_NAME,
14
+ version: packageJson.version
15
+ };
16
+
17
+ // Add optional parameters if provided
18
+ if (details !== undefined) renderData.details = details;
19
+
20
+ return res.render("Error/dError.handlebars", renderData);
21
+ };
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "mbkauthe",
3
- "version": "2.5.0",
3
+ "version": "3.0.0",
4
4
  "description": "MBKTech's reusable authentication system for Node.js applications.",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
+ "types": "index.d.ts",
7
8
  "scripts": {
8
9
  "dev": "cross-env test=dev nodemon index.js"
9
10
  },
@@ -20,7 +21,7 @@
20
21
  "security"
21
22
  ],
22
23
  "author": "Muhammad Bin Khalid <support@mbktech.org>",
23
- "license": "MPL-2.0",
24
+ "license": "GPL-2.0",
24
25
  "bugs": {
25
26
  "url": "https://github.com/MIbnEKhalid/mbkauthe/issues"
26
27
  },
@@ -49,6 +50,8 @@
49
50
  "url": "^0.11.4"
50
51
  },
51
52
  "devDependencies": {
53
+ "@types/express": "^5.0.6",
54
+ "@types/node": "^22.19.1",
52
55
  "cross-env": "^7.0.3",
53
56
  "nodemon": "^3.1.11"
54
57
  }
package/public/main.js CHANGED
@@ -25,7 +25,7 @@ async function logout() {
25
25
  alert(result.message);
26
26
  }
27
27
  } catch (error) {
28
- console.error("Error during logout:", error);
28
+ console.error("[mbkauthe] Error during logout:", error);
29
29
  alert(`Logout failed: ${error.message}`);
30
30
  }
31
31
  }
@@ -64,7 +64,7 @@ async function nuclearCacheClear() {
64
64
  return Promise.resolve();
65
65
  }));
66
66
  } catch (error) {
67
- console.error("Error clearing IndexedDB:", error);
67
+ console.error("[mbkauthe] Error clearing IndexedDB:", error);
68
68
  }
69
69
  }
70
70
 
@@ -84,7 +84,7 @@ async function nuclearCacheClear() {
84
84
  window.location.reload();
85
85
 
86
86
  } catch (error) {
87
- console.error('Nuclear cache clear failed:', error);
87
+ console.error('[mbkauthe] Nuclear cache clear failed:', error);
88
88
  window.location.reload(true);
89
89
  }
90
90
  }
@@ -103,7 +103,7 @@ function checkSession() {
103
103
  window.location.reload(); // Reload the page to update the session status
104
104
  }
105
105
  })
106
- .catch((error) => console.error("Error checking session:", error));
106
+ .catch((error) => console.error("[mbkauthe] Error checking session:", error));
107
107
  }
108
108
  // Call validateSession every 2 minutes (120000 milliseconds)
109
109
  // setInterval(checkSession, validateSessionInterval);
@@ -55,7 +55,7 @@
55
55
  color: var(--accent);
56
56
  text-decoration: none;
57
57
  transition: var(--transition);
58
- margin: 0 15px;
58
+ margin: 0 auto;
59
59
  font-size: 1rem;
60
60
  font-weight: 600;
61
61
  }
@@ -0,0 +1,341 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ {{> head}}
5
+
6
+ <body>
7
+ {{> header}}
8
+
9
+ <style>
10
+ .container {
11
+ max-width: 1200px;
12
+ margin: 0 auto;
13
+ padding: 2rem;
14
+ padding-top: 100px;
15
+ }
16
+
17
+ .page-header {
18
+ text-align: center;
19
+ padding: 3rem 0 2rem;
20
+ border-bottom: 2px solid rgba(0, 184, 148, 0.3);
21
+ }
22
+
23
+ .page-header h1 {
24
+ font-size: 2.5rem;
25
+ color: var(--accent);
26
+ margin-bottom: 0.5rem;
27
+ }
28
+
29
+ .page-header p {
30
+ font-size: 1.1rem;
31
+ color: var(--text-light);
32
+ }
33
+
34
+ .search-box {
35
+ margin-bottom: 2rem;
36
+ position: sticky;
37
+ top: 70px;
38
+ background: transparent;
39
+ backdrop-filter: blur(10px);
40
+ padding: 1rem 0;
41
+ z-index: 100;
42
+ border-bottom: 1px solid rgba(0, 184, 148, 0.2);
43
+ }
44
+
45
+ .search-input {
46
+ width: 100%;
47
+ padding: 1rem 1.5rem;
48
+ background: rgba(255, 255, 255, 0.05);
49
+ border: 2px solid rgba(0, 184, 148, 0.3);
50
+ border-radius: 8px;
51
+ color: #e0f7fa;
52
+ font-size: 1rem;
53
+ transition: all 0.3s ease;
54
+ }
55
+
56
+ .search-input:focus {
57
+ outline: none;
58
+ border-color: #00b894;
59
+ background: rgba(255, 255, 255, 0.08);
60
+ }
61
+
62
+ .search-input::placeholder {
63
+ color: #b2dfdb;
64
+ opacity: 0.6;
65
+ }
66
+
67
+ .category {
68
+ margin-bottom: 3rem;
69
+ }
70
+
71
+ .category-header {
72
+ display: flex;
73
+ align-items: center;
74
+ gap: 1rem;
75
+ margin-bottom: 1.5rem;
76
+ padding: 1rem;
77
+ background: rgba(0, 184, 148, 0.1);
78
+ border-left: 4px solid #00b894;
79
+ border-radius: 4px;
80
+ }
81
+
82
+ .category-header h2 {
83
+ font-size: 1.8rem;
84
+ color: var(--accent);
85
+ }
86
+
87
+ .category-header .range {
88
+ font-size: 1rem;
89
+ color: var(--text-light);
90
+ font-weight: normal;
91
+ }
92
+
93
+ .error-card {
94
+ background: rgba(10, 20, 20, 0.8);
95
+ border: 1px solid rgba(0, 184, 148, 0.2);
96
+ border-radius: 8px;
97
+ padding: 1.5rem;
98
+ margin-bottom: 1.5rem;
99
+ transition: all 0.3s ease;
100
+ scroll-margin-top: 100px;
101
+ }
102
+
103
+ .error-card:hover {
104
+ border-color: #00b894;
105
+ box-shadow: 0 4px 20px rgba(0, 184, 148, 0.2);
106
+ transform: translateY(-2px);
107
+ }
108
+
109
+ .error-header {
110
+ display: flex;
111
+ align-items: center;
112
+ gap: 1rem;
113
+ margin-bottom: 1rem;
114
+ }
115
+
116
+ .error-code {
117
+ background: var(--warning);
118
+ color: var(--darker);
119
+ padding: 0.5rem 1rem;
120
+ border-radius: 6px;
121
+ font-weight: bold;
122
+ font-size: 1.1rem;
123
+ }
124
+
125
+ .error-name {
126
+ color: var(--accent);
127
+ font-size: 1.3rem;
128
+ font-weight: 600;
129
+ }
130
+
131
+ .error-message {
132
+ color: var(--text);
133
+ font-size: 1.1rem;
134
+ margin-bottom: 0.75rem;
135
+ padding-left: 1rem;
136
+ border-left: 3px solid rgba(0, 184, 148, 0.3);
137
+ }
138
+
139
+ .error-hint {
140
+ background: rgba(255, 209, 102, 0.1);
141
+ color: var(--warning);
142
+ padding: 0.75rem 1rem;
143
+ border-radius: 6px;
144
+ border-left: 3px solid var(--warning);
145
+ font-size: 0.95rem;
146
+ margin-top: 0.75rem;
147
+ }
148
+
149
+
150
+
151
+ .no-results {
152
+ text-align: center;
153
+ padding: 3rem;
154
+ color: var(--text-light);
155
+ font-size: 1.2rem;
156
+ }
157
+
158
+ .back-to-top {
159
+ position: fixed;
160
+ bottom: 2rem;
161
+ right: 2rem;
162
+ background: var(--accent);
163
+ color: var(--darker);
164
+ width: 50px;
165
+ height: 50px;
166
+ border-radius: 50%;
167
+ display: flex;
168
+ align-items: center;
169
+ justify-content: center;
170
+ font-size: 1.5rem;
171
+ cursor: pointer;
172
+ box-shadow: 0 4px 20px rgba(0, 184, 148, 0.3);
173
+ transition: all 0.3s ease;
174
+ opacity: 0;
175
+ pointer-events: none;
176
+ }
177
+
178
+ .back-to-top.visible {
179
+ opacity: 1;
180
+ pointer-events: all;
181
+ }
182
+
183
+ .back-to-top:hover {
184
+ transform: translateY(-5px);
185
+ box-shadow: 0 6px 25px rgba(0, 184, 148, 0.4);
186
+ }
187
+
188
+ @media (max-width: 768px) {
189
+ .page-header h1 {
190
+ font-size: 2rem;
191
+ }
192
+
193
+ .category-header h2 {
194
+ font-size: 1.5rem;
195
+ }
196
+
197
+ .error-card {
198
+ padding: 1rem;
199
+ }
200
+
201
+ .error-header {
202
+ flex-direction: column;
203
+ align-items: flex-start;
204
+ }
205
+ }
206
+ </style>
207
+
208
+ <div class="container">
209
+ <div class="page-header">
210
+ <h1>🔐 MBKAuthe Error Codes</h1>
211
+ <p>Complete reference guide for all error codes and their solutions</p>
212
+ </div>
213
+
214
+ <div class="search-box">
215
+ <input type="text" class="search-input" id="searchInput" placeholder="Search by error code, name, or description...">
216
+ </div>
217
+
218
+ <div id="errorContainer">
219
+ {{#each errorCategories}}
220
+ <div class="category" data-category="{{this.category}}">
221
+ <div class="category-header">
222
+ <h2>{{this.icon}} {{this.name}}</h2>
223
+ <span class="range">{{this.range}}</span>
224
+ </div>
225
+
226
+ {{#each this.errors}}
227
+ <div class="error-card" id="error-{{this.code}}" data-search="{{this.code}} {{this.name}} {{this.message}} {{this.userMessage}} {{this.hint}}">
228
+ <div class="error-header">
229
+ <span class="error-code">{{this.code}}</span>
230
+ <span class="error-name">{{this.name}}</span>
231
+ </div>
232
+ <div class="error-message">
233
+ {{this.userMessage}}
234
+ </div>
235
+ <div class="error-hint">
236
+ 💡 {{this.hint}}
237
+ </div>
238
+ </div>
239
+ {{/each}}
240
+ </div>
241
+ {{/each}}
242
+ </div>
243
+
244
+ <div class="no-results" id="noResults" style="display: none;">
245
+ No error codes found matching your search.
246
+ </div>
247
+ </div>
248
+
249
+ <div class="back-to-top" id="backToTop">↑</div>
250
+
251
+ <script>
252
+ // Search functionality
253
+ const searchInput = document.getElementById('searchInput');
254
+ const errorCards = document.querySelectorAll('.error-card');
255
+ const categories = document.querySelectorAll('.category');
256
+ const noResults = document.getElementById('noResults');
257
+
258
+ searchInput.addEventListener('input', function() {
259
+ const searchTerm = this.value.toLowerCase().trim();
260
+ let hasResults = false;
261
+
262
+ if (!searchTerm) {
263
+ // Show all cards and categories
264
+ errorCards.forEach(card => card.style.display = 'block');
265
+ categories.forEach(cat => cat.style.display = 'block');
266
+ noResults.style.display = 'none';
267
+ return;
268
+ }
269
+
270
+ // Search through error cards
271
+ errorCards.forEach(card => {
272
+ const searchData = card.getAttribute('data-search').toLowerCase();
273
+ if (searchData.includes(searchTerm)) {
274
+ card.style.display = 'block';
275
+ hasResults = true;
276
+ } else {
277
+ card.style.display = 'none';
278
+ }
279
+ });
280
+
281
+ // Hide/show categories based on visible cards
282
+ categories.forEach(category => {
283
+ const visibleCards = category.querySelectorAll('.error-card[style="display: block;"]');
284
+ if (visibleCards.length > 0) {
285
+ category.style.display = 'block';
286
+ } else {
287
+ category.style.display = 'none';
288
+ }
289
+ });
290
+
291
+ noResults.style.display = hasResults ? 'none' : 'block';
292
+ });
293
+
294
+ // Back to top button
295
+ const backToTop = document.getElementById('backToTop');
296
+
297
+ window.addEventListener('scroll', function() {
298
+ if (window.pageYOffset > 300) {
299
+ backToTop.classList.add('visible');
300
+ } else {
301
+ backToTop.classList.remove('visible');
302
+ }
303
+ });
304
+
305
+ backToTop.addEventListener('click', function() {
306
+ window.scrollTo({ top: 0, behavior: 'smooth' });
307
+ });
308
+
309
+ // Handle hash navigation
310
+ function scrollToError() {
311
+ if (window.location.hash) {
312
+ let targetId = window.location.hash.substring(1);
313
+
314
+ // If hash doesn't start with "error-", add it
315
+ if (!targetId.startsWith('error-') && /^\d+$/.test(targetId)) {
316
+ targetId = 'error-' + targetId;
317
+ }
318
+
319
+ const targetElement = document.getElementById(targetId);
320
+ if (targetElement) {
321
+ setTimeout(() => {
322
+ targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
323
+ targetElement.style.background = 'rgba(0, 184, 148, 0.2)';
324
+ targetElement.style.transition = 'background 0.3s ease';
325
+ setTimeout(() => {
326
+ targetElement.style.background = '';
327
+ }, 2000);
328
+ }, 100);
329
+ }
330
+ }
331
+ }
332
+
333
+ // Scroll on page load
334
+ scrollToError();
335
+
336
+ // Scroll when hash changes
337
+ window.addEventListener('hashchange', scrollToError);
338
+ </script>
339
+ </body>
340
+
341
+ </html>