apidocly 1.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.
- package/LICENSE +21 -0
- package/README.md +246 -0
- package/bin/apidocly.js +62 -0
- package/lib/generator/data-builder.js +170 -0
- package/lib/generator/index.js +334 -0
- package/lib/index.js +59 -0
- package/lib/parser/annotations.js +230 -0
- package/lib/parser/index.js +86 -0
- package/lib/parser/languages.js +57 -0
- package/lib/utils/config-loader.js +67 -0
- package/lib/utils/file-scanner.js +64 -0
- package/lib/utils/minifier.js +106 -0
- package/package.json +46 -0
- package/template/css/style.css +2670 -0
- package/template/index.html +243 -0
- package/template/js/auth.js +281 -0
- package/template/js/main.js +2933 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<meta name="theme-color" content="#09090b">
|
|
7
|
+
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
|
8
|
+
<meta http-equiv="Pragma" content="no-cache">
|
|
9
|
+
<meta http-equiv="Expires" content="0">
|
|
10
|
+
<base href="./">
|
|
11
|
+
<title>{{PROJECT_TITLE}}</title>
|
|
12
|
+
<link rel="stylesheet" href="css/style.css?v={{CACHE_VERSION}}">
|
|
13
|
+
</head>
|
|
14
|
+
<body>
|
|
15
|
+
<div id="login-container"></div>
|
|
16
|
+
|
|
17
|
+
<div id="app" class="app hidden">
|
|
18
|
+
<!-- Noise Overlay -->
|
|
19
|
+
<div class="noise-overlay"></div>
|
|
20
|
+
|
|
21
|
+
<!-- Header -->
|
|
22
|
+
<header class="app-header">
|
|
23
|
+
<div class="header-left">
|
|
24
|
+
<a href="#" class="logo-link" id="logo-link">
|
|
25
|
+
<div class="logo-icon">{}</div>
|
|
26
|
+
<h1 class="logo">{{PROJECT_NAME}}</h1>
|
|
27
|
+
</a>
|
|
28
|
+
<span class="version-badge">v{{PROJECT_VERSION}}</span>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="header-right">
|
|
31
|
+
<div class="version-selector-wrapper hidden" id="version-selector-wrapper">
|
|
32
|
+
<select class="version-selector" id="version-selector" title="Select API version">
|
|
33
|
+
</select>
|
|
34
|
+
<button class="version-compare-toggle" id="version-compare-toggle" title="Compare versions">
|
|
35
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
36
|
+
<path d="M16 3h5v5"></path>
|
|
37
|
+
<path d="M8 3H3v5"></path>
|
|
38
|
+
<path d="M12 22v-8.3a4 4 0 0 0-1.172-2.872L3 3"></path>
|
|
39
|
+
<path d="m15 9 6-6"></path>
|
|
40
|
+
</svg>
|
|
41
|
+
</button>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="env-selector-wrapper hidden" id="env-selector-wrapper">
|
|
44
|
+
<select class="env-selector" id="env-selector" title="Select environment">
|
|
45
|
+
</select>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="search-wrapper">
|
|
48
|
+
<svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
49
|
+
<circle cx="11" cy="11" r="8"></circle>
|
|
50
|
+
<path d="m21 21-4.35-4.35"></path>
|
|
51
|
+
</svg>
|
|
52
|
+
<input type="text" class="search-input" placeholder="Search endpoints..." id="search-input">
|
|
53
|
+
<button class="search-clear hidden" id="search-clear">×</button>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="settings-wrapper">
|
|
56
|
+
<button class="settings-btn" id="settings-btn" title="Settings">
|
|
57
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
58
|
+
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"></path>
|
|
59
|
+
<circle cx="12" cy="12" r="3"></circle>
|
|
60
|
+
</svg>
|
|
61
|
+
</button>
|
|
62
|
+
<div class="settings-panel hidden" id="settings-panel">
|
|
63
|
+
<div class="settings-header">
|
|
64
|
+
<span>Settings</span>
|
|
65
|
+
<button class="settings-close" id="settings-close">×</button>
|
|
66
|
+
</div>
|
|
67
|
+
<div class="settings-content">
|
|
68
|
+
<div class="settings-group">
|
|
69
|
+
<div class="settings-group-title">Default States</div>
|
|
70
|
+
<label class="settings-item">
|
|
71
|
+
<span class="settings-label">Expand examples by default</span>
|
|
72
|
+
<input type="checkbox" class="settings-toggle" id="setting-expand-examples">
|
|
73
|
+
</label>
|
|
74
|
+
<label class="settings-item">
|
|
75
|
+
<span class="settings-label">Expand endpoints by default</span>
|
|
76
|
+
<input type="checkbox" class="settings-toggle" id="setting-expand-endpoints">
|
|
77
|
+
</label>
|
|
78
|
+
</div>
|
|
79
|
+
<div class="settings-group">
|
|
80
|
+
<div class="settings-group-title">Behavior</div>
|
|
81
|
+
<label class="settings-item">
|
|
82
|
+
<span class="settings-label">Auto-collapse other endpoints</span>
|
|
83
|
+
<input type="checkbox" class="settings-toggle" id="setting-auto-collapse" checked>
|
|
84
|
+
</label>
|
|
85
|
+
<label class="settings-item">
|
|
86
|
+
<span class="settings-label">Show "Try it out" section</span>
|
|
87
|
+
<input type="checkbox" class="settings-toggle" id="setting-show-tryit" checked>
|
|
88
|
+
</label>
|
|
89
|
+
<label class="settings-item">
|
|
90
|
+
<span class="settings-label">Show code samples</span>
|
|
91
|
+
<input type="checkbox" class="settings-toggle" id="setting-show-codesamples" checked>
|
|
92
|
+
</label>
|
|
93
|
+
</div>
|
|
94
|
+
<div class="settings-group">
|
|
95
|
+
<div class="settings-group-title">Appearance</div>
|
|
96
|
+
<label class="settings-item">
|
|
97
|
+
<span class="settings-label">Compact mode</span>
|
|
98
|
+
<input type="checkbox" class="settings-toggle" id="setting-compact-mode">
|
|
99
|
+
</label>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
<button class="logout-btn hidden" id="logout-btn" title="Logout">
|
|
105
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
106
|
+
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
|
|
107
|
+
<polyline points="16 17 21 12 16 7"></polyline>
|
|
108
|
+
<line x1="21" y1="12" x2="9" y2="12"></line>
|
|
109
|
+
</svg>
|
|
110
|
+
</button>
|
|
111
|
+
</div>
|
|
112
|
+
</header>
|
|
113
|
+
|
|
114
|
+
<!-- Main Content -->
|
|
115
|
+
<main class="main-content">
|
|
116
|
+
<!-- Stats Cards -->
|
|
117
|
+
<div class="stats-grid" id="stats-grid">
|
|
118
|
+
<div class="stat-card">
|
|
119
|
+
<div class="stat-value" id="stat-endpoints">0</div>
|
|
120
|
+
<div class="stat-label">Endpoints</div>
|
|
121
|
+
</div>
|
|
122
|
+
<div class="stat-card">
|
|
123
|
+
<div class="stat-value" id="stat-groups">0</div>
|
|
124
|
+
<div class="stat-label">Groups</div>
|
|
125
|
+
</div>
|
|
126
|
+
<div class="stat-card">
|
|
127
|
+
<div class="stat-value" id="stat-private">0</div>
|
|
128
|
+
<div class="stat-label">Private</div>
|
|
129
|
+
</div>
|
|
130
|
+
<div class="stat-card">
|
|
131
|
+
<div class="stat-value" id="stat-version">-</div>
|
|
132
|
+
<div class="stat-label">Version</div>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<!-- Version Comparison Panel -->
|
|
137
|
+
<div class="version-compare-panel hidden" id="version-compare-panel">
|
|
138
|
+
<div class="version-compare-header">
|
|
139
|
+
<h3>Compare Versions</h3>
|
|
140
|
+
<button class="version-compare-close" id="version-compare-close">×</button>
|
|
141
|
+
</div>
|
|
142
|
+
<div class="version-compare-selectors">
|
|
143
|
+
<div class="version-select-group">
|
|
144
|
+
<label>From version:</label>
|
|
145
|
+
<select id="version-from" class="version-select"></select>
|
|
146
|
+
</div>
|
|
147
|
+
<div class="version-arrow">→</div>
|
|
148
|
+
<div class="version-select-group">
|
|
149
|
+
<label>To version:</label>
|
|
150
|
+
<select id="version-to" class="version-select"></select>
|
|
151
|
+
</div>
|
|
152
|
+
<button class="version-compare-btn" id="version-compare-btn">Compare</button>
|
|
153
|
+
</div>
|
|
154
|
+
<div class="version-compare-results hidden" id="version-compare-results">
|
|
155
|
+
<div class="version-diff-summary" id="version-diff-summary"></div>
|
|
156
|
+
<div class="version-diff-details" id="version-diff-details"></div>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<!-- Content Container -->
|
|
161
|
+
<div class="content-container" id="content-container">
|
|
162
|
+
<!-- Groups View (default) -->
|
|
163
|
+
<div class="groups-view" id="groups-view">
|
|
164
|
+
<div class="section-header">
|
|
165
|
+
<h2>API Groups</h2>
|
|
166
|
+
<div class="export-dropdown-wrapper">
|
|
167
|
+
<button class="export-btn export-dropdown-toggle" id="export-dropdown-toggle">
|
|
168
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
169
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
|
170
|
+
<polyline points="7 10 12 15 17 10"></polyline>
|
|
171
|
+
<line x1="12" y1="15" x2="12" y2="3"></line>
|
|
172
|
+
</svg>
|
|
173
|
+
<span>Export</span>
|
|
174
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="dropdown-arrow">
|
|
175
|
+
<path d="m6 9 6 6 6-6"></path>
|
|
176
|
+
</svg>
|
|
177
|
+
</button>
|
|
178
|
+
<div class="export-dropdown-menu hidden" id="export-dropdown-menu">
|
|
179
|
+
<button class="export-dropdown-item" onclick="downloadOpenAPI('__all__')">
|
|
180
|
+
<span class="export-icon">{ }</span> OpenAPI JSON
|
|
181
|
+
</button>
|
|
182
|
+
<button class="export-dropdown-item" onclick="downloadPostmanCollection('__all__')">
|
|
183
|
+
<span class="export-icon">PM</span> Postman Collection
|
|
184
|
+
</button>
|
|
185
|
+
<button class="export-dropdown-item" onclick="downloadInsomniaCollection('__all__')">
|
|
186
|
+
<span class="export-icon">IN</span> Insomnia Collection
|
|
187
|
+
</button>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
<div class="groups-list" id="groups-list">
|
|
192
|
+
<!-- Generated by JS -->
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
<!-- Endpoints View (when group selected) -->
|
|
197
|
+
<div class="endpoints-view hidden" id="endpoints-view">
|
|
198
|
+
<div class="section-header">
|
|
199
|
+
<button class="back-btn" id="back-to-groups">
|
|
200
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
201
|
+
<path d="m15 18-6-6 6-6"></path>
|
|
202
|
+
</svg>
|
|
203
|
+
Back to groups
|
|
204
|
+
</button>
|
|
205
|
+
<div class="current-group" id="current-group">
|
|
206
|
+
<span class="group-color-bar"></span>
|
|
207
|
+
<span class="group-name"></span>
|
|
208
|
+
<span class="group-count"></span>
|
|
209
|
+
</div>
|
|
210
|
+
<button class="group-export-btn" id="group-export-btn" title="Download as OpenAPI JSON">
|
|
211
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
212
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
|
213
|
+
<polyline points="7 10 12 15 17 10"></polyline>
|
|
214
|
+
<line x1="12" y1="15" x2="12" y2="3"></line>
|
|
215
|
+
</svg>
|
|
216
|
+
<span>OpenAPI</span>
|
|
217
|
+
</button>
|
|
218
|
+
</div>
|
|
219
|
+
<div class="endpoints-list" id="endpoints-list">
|
|
220
|
+
<!-- Generated by JS -->
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<!-- Search Results View -->
|
|
225
|
+
<div class="search-view hidden" id="search-view">
|
|
226
|
+
<div class="section-header">
|
|
227
|
+
<h2>Search results for "<span id="search-query"></span>"</h2>
|
|
228
|
+
<button class="clear-search-btn" id="clear-search">Clear ×</button>
|
|
229
|
+
</div>
|
|
230
|
+
<div class="search-results" id="search-results">
|
|
231
|
+
<!-- Generated by JS -->
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
</main>
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
<script src="api_data.js?v={{CACHE_VERSION}}"></script>
|
|
239
|
+
<script src="versions.js?v={{CACHE_VERSION}}" onerror="window.apiVersions=[];window.apiVersionsConfig={withCompare:false};window.apiVersionsData={}"></script>
|
|
240
|
+
<script src="js/auth.js?v={{CACHE_VERSION}}"></script>
|
|
241
|
+
<script src="js/main.js?v={{CACHE_VERSION}}"></script>
|
|
242
|
+
</body>
|
|
243
|
+
</html>
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
(function() {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const AUTH_STORAGE_KEY = 'apidocly-auth';
|
|
5
|
+
const AUTH_PWD_KEY = 'apidocly-pwd'; // Encrypted password storage for session
|
|
6
|
+
|
|
7
|
+
window.apiDoclyAuth = {
|
|
8
|
+
passwordHash: null,
|
|
9
|
+
passwordMessage: 'Enter password to view API documentation',
|
|
10
|
+
authData: null,
|
|
11
|
+
onAuthenticatedCallback: null,
|
|
12
|
+
isReady: false,
|
|
13
|
+
|
|
14
|
+
init: function(authData) {
|
|
15
|
+
this.authData = authData;
|
|
16
|
+
|
|
17
|
+
if (!authData) {
|
|
18
|
+
// No auth required
|
|
19
|
+
this.showApp();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
this.passwordHash = authData.hash;
|
|
24
|
+
if (authData.message) {
|
|
25
|
+
this.passwordMessage = authData.message;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (this.isAuthenticated()) {
|
|
29
|
+
// Already authenticated, try to decrypt if needed
|
|
30
|
+
this.handleDecryptionAndShow();
|
|
31
|
+
} else {
|
|
32
|
+
this.showLoginForm();
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
isAuthenticated: function() {
|
|
37
|
+
const stored = localStorage.getItem(AUTH_STORAGE_KEY);
|
|
38
|
+
return stored === this.passwordHash;
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
handleDecryptionAndShow: async function() {
|
|
42
|
+
if (this.authData && this.authData.encrypted && typeof apiDataEncrypted !== 'undefined' && apiDataEncrypted) {
|
|
43
|
+
// Need to decrypt - get stored password
|
|
44
|
+
const storedPwd = sessionStorage.getItem(AUTH_PWD_KEY);
|
|
45
|
+
if (storedPwd) {
|
|
46
|
+
try {
|
|
47
|
+
const decrypted = await this.decryptData(apiDataEncrypted, storedPwd);
|
|
48
|
+
window.apiData = decrypted;
|
|
49
|
+
this.showApp();
|
|
50
|
+
} catch (e) {
|
|
51
|
+
// Decryption failed, need to re-login
|
|
52
|
+
console.error('Decryption failed:', e);
|
|
53
|
+
localStorage.removeItem(AUTH_STORAGE_KEY);
|
|
54
|
+
sessionStorage.removeItem(AUTH_PWD_KEY);
|
|
55
|
+
this.showLoginForm();
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
// No stored password, need to re-login
|
|
59
|
+
localStorage.removeItem(AUTH_STORAGE_KEY);
|
|
60
|
+
this.showLoginForm();
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
// Not encrypted or no encryption needed
|
|
64
|
+
this.showApp();
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
showLoginForm: function() {
|
|
69
|
+
const container = document.getElementById('login-container');
|
|
70
|
+
|
|
71
|
+
container.innerHTML = `
|
|
72
|
+
<div class="login-page">
|
|
73
|
+
<div class="login-bg-pattern"></div>
|
|
74
|
+
<div class="login-card">
|
|
75
|
+
<div class="login-icon">
|
|
76
|
+
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
77
|
+
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
|
78
|
+
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
|
79
|
+
</svg>
|
|
80
|
+
</div>
|
|
81
|
+
<h1 class="login-title">Protected Documentation</h1>
|
|
82
|
+
<p class="login-subtitle">${this.escapeHtml(this.passwordMessage)}</p>
|
|
83
|
+
<form class="login-form" id="login-form">
|
|
84
|
+
<div class="login-input-wrapper">
|
|
85
|
+
<svg class="login-input-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
86
|
+
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
|
87
|
+
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
|
88
|
+
</svg>
|
|
89
|
+
<input type="password" class="login-input" id="password-input" placeholder="Enter password" autofocus>
|
|
90
|
+
</div>
|
|
91
|
+
<button type="submit" class="login-btn">
|
|
92
|
+
<span>Unlock Documentation</span>
|
|
93
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
94
|
+
<path d="M5 12h14M12 5l7 7-7 7"></path>
|
|
95
|
+
</svg>
|
|
96
|
+
</button>
|
|
97
|
+
<div class="login-error hidden" id="login-error">
|
|
98
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
99
|
+
<circle cx="12" cy="12" r="10"></circle>
|
|
100
|
+
<line x1="15" y1="9" x2="9" y2="15"></line>
|
|
101
|
+
<line x1="9" y1="9" x2="15" y2="15"></line>
|
|
102
|
+
</svg>
|
|
103
|
+
<span>Invalid password. Please try again.</span>
|
|
104
|
+
</div>
|
|
105
|
+
</form>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
`;
|
|
109
|
+
|
|
110
|
+
document.getElementById('login-form').addEventListener('submit', (e) => {
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
this.handleLogin();
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
handleLogin: async function() {
|
|
117
|
+
const passwordInput = document.getElementById('password-input');
|
|
118
|
+
const errorDiv = document.getElementById('login-error');
|
|
119
|
+
const loginBtn = document.querySelector('.login-btn');
|
|
120
|
+
const password = passwordInput.value;
|
|
121
|
+
|
|
122
|
+
// Show loading state
|
|
123
|
+
if (loginBtn) {
|
|
124
|
+
loginBtn.disabled = true;
|
|
125
|
+
loginBtn.innerHTML = '<span>Decrypting...</span>';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const hash = await this.hashPassword(password);
|
|
129
|
+
|
|
130
|
+
if (hash === this.passwordHash) {
|
|
131
|
+
// Password correct, now decrypt if needed
|
|
132
|
+
if (this.authData && this.authData.encrypted && typeof apiDataEncrypted !== 'undefined' && apiDataEncrypted) {
|
|
133
|
+
try {
|
|
134
|
+
const decrypted = await this.decryptData(apiDataEncrypted, password);
|
|
135
|
+
window.apiData = decrypted;
|
|
136
|
+
// Store password in sessionStorage for page refreshes
|
|
137
|
+
sessionStorage.setItem(AUTH_PWD_KEY, password);
|
|
138
|
+
localStorage.setItem(AUTH_STORAGE_KEY, hash);
|
|
139
|
+
this.showApp();
|
|
140
|
+
} catch (e) {
|
|
141
|
+
console.error('Decryption failed:', e);
|
|
142
|
+
errorDiv.classList.remove('hidden');
|
|
143
|
+
passwordInput.value = '';
|
|
144
|
+
passwordInput.focus();
|
|
145
|
+
if (loginBtn) {
|
|
146
|
+
loginBtn.disabled = false;
|
|
147
|
+
loginBtn.innerHTML = '<span>Unlock Documentation</span><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"></path></svg>';
|
|
148
|
+
}
|
|
149
|
+
setTimeout(() => {
|
|
150
|
+
errorDiv.classList.add('hidden');
|
|
151
|
+
}, 3000);
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
// No encryption, just authenticate
|
|
155
|
+
localStorage.setItem(AUTH_STORAGE_KEY, hash);
|
|
156
|
+
this.showApp();
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
errorDiv.classList.remove('hidden');
|
|
160
|
+
passwordInput.value = '';
|
|
161
|
+
passwordInput.focus();
|
|
162
|
+
if (loginBtn) {
|
|
163
|
+
loginBtn.disabled = false;
|
|
164
|
+
loginBtn.innerHTML = '<span>Unlock Documentation</span><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"></path></svg>';
|
|
165
|
+
}
|
|
166
|
+
setTimeout(() => {
|
|
167
|
+
errorDiv.classList.add('hidden');
|
|
168
|
+
}, 3000);
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
decryptData: async function(encryptedBase64, password) {
|
|
173
|
+
const auth = this.authData;
|
|
174
|
+
|
|
175
|
+
// Decode base64
|
|
176
|
+
const combined = Uint8Array.from(atob(encryptedBase64), c => c.charCodeAt(0));
|
|
177
|
+
|
|
178
|
+
// Extract parts
|
|
179
|
+
const salt = combined.slice(0, auth.saltLength);
|
|
180
|
+
const iv = combined.slice(auth.saltLength, auth.saltLength + auth.ivLength);
|
|
181
|
+
const authTag = combined.slice(auth.saltLength + auth.ivLength, auth.saltLength + auth.ivLength + auth.authTagLength);
|
|
182
|
+
const encrypted = combined.slice(auth.saltLength + auth.ivLength + auth.authTagLength);
|
|
183
|
+
|
|
184
|
+
// Combine encrypted data with auth tag (Web Crypto expects them together)
|
|
185
|
+
const encryptedWithTag = new Uint8Array(encrypted.length + authTag.length);
|
|
186
|
+
encryptedWithTag.set(encrypted);
|
|
187
|
+
encryptedWithTag.set(authTag, encrypted.length);
|
|
188
|
+
|
|
189
|
+
// Derive key using PBKDF2
|
|
190
|
+
const encoder = new TextEncoder();
|
|
191
|
+
const passwordKey = await crypto.subtle.importKey(
|
|
192
|
+
'raw',
|
|
193
|
+
encoder.encode(password),
|
|
194
|
+
'PBKDF2',
|
|
195
|
+
false,
|
|
196
|
+
['deriveKey']
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const aesKey = await crypto.subtle.deriveKey(
|
|
200
|
+
{
|
|
201
|
+
name: 'PBKDF2',
|
|
202
|
+
salt: salt,
|
|
203
|
+
iterations: auth.pbkdf2Iterations,
|
|
204
|
+
hash: 'SHA-256'
|
|
205
|
+
},
|
|
206
|
+
passwordKey,
|
|
207
|
+
{ name: 'AES-GCM', length: 256 },
|
|
208
|
+
false,
|
|
209
|
+
['decrypt']
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// Decrypt
|
|
213
|
+
const decrypted = await crypto.subtle.decrypt(
|
|
214
|
+
{ name: 'AES-GCM', iv: iv },
|
|
215
|
+
aesKey,
|
|
216
|
+
encryptedWithTag
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
// Parse JSON
|
|
220
|
+
const decoder = new TextDecoder();
|
|
221
|
+
return JSON.parse(decoder.decode(decrypted));
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
showApp: function() {
|
|
225
|
+
const container = document.getElementById('login-container');
|
|
226
|
+
container.innerHTML = '';
|
|
227
|
+
|
|
228
|
+
const app = document.getElementById('app');
|
|
229
|
+
app.classList.remove('hidden');
|
|
230
|
+
|
|
231
|
+
if (this.passwordHash) {
|
|
232
|
+
const logoutBtn = document.getElementById('logout-btn');
|
|
233
|
+
logoutBtn.classList.remove('hidden');
|
|
234
|
+
logoutBtn.addEventListener('click', () => this.logout());
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
this.isReady = true;
|
|
238
|
+
|
|
239
|
+
if (this.onAuthenticatedCallback) {
|
|
240
|
+
this.onAuthenticatedCallback();
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
logout: function() {
|
|
245
|
+
localStorage.removeItem(AUTH_STORAGE_KEY);
|
|
246
|
+
sessionStorage.removeItem(AUTH_PWD_KEY);
|
|
247
|
+
window.location.reload();
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
onAuthenticated: function(callback) {
|
|
251
|
+
this.onAuthenticatedCallback = callback;
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
hashPassword: async function(password) {
|
|
255
|
+
const encoder = new TextEncoder();
|
|
256
|
+
const data = encoder.encode(password);
|
|
257
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
258
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
259
|
+
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
escapeHtml: function(str) {
|
|
263
|
+
if (!str) return '';
|
|
264
|
+
return String(str)
|
|
265
|
+
.replace(/&/g, '&')
|
|
266
|
+
.replace(/</g, '<')
|
|
267
|
+
.replace(/>/g, '>')
|
|
268
|
+
.replace(/"/g, '"')
|
|
269
|
+
.replace(/'/g, ''');
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
274
|
+
// Use apiDataAuth for auth info (separate from potentially encrypted apiData)
|
|
275
|
+
if (typeof apiDataAuth !== 'undefined' && apiDataAuth) {
|
|
276
|
+
window.apiDoclyAuth.init(apiDataAuth);
|
|
277
|
+
} else {
|
|
278
|
+
window.apiDoclyAuth.init(null);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
})();
|