@riligar/elysia-backup 1.3.0 → 1.5.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,324 @@
1
+ /**
2
+ * Alpine.js backup app logic
3
+ * @param {{ config: object, jobStatus: object }} props
4
+ * @returns {string} JavaScript code string
5
+ */
6
+ export const backupAppScript = ({ config, jobStatus }) => `
7
+ <script>
8
+ document.addEventListener('alpine:init', () => {
9
+ Alpine.data('holdButton', (action) => ({
10
+ progress: 0,
11
+ interval: null,
12
+ start() {
13
+ this.progress = 0
14
+ this.interval = setInterval(() => {
15
+ this.progress += 1
16
+ if (this.progress >= 100) {
17
+ this.trigger()
18
+ }
19
+ }, 30)
20
+ },
21
+ stop() {
22
+ clearInterval(this.interval)
23
+ this.progress = 0
24
+ },
25
+ trigger() {
26
+ this.stop()
27
+ action()
28
+ }
29
+ }))
30
+ })
31
+
32
+ function backupApp() {
33
+ return {
34
+ activeTab: 'dashboard',
35
+ loading: false,
36
+ loadingFiles: false,
37
+ lastBackup: null,
38
+ files: [],
39
+ groups: [],
40
+ logs: [],
41
+ config: ${JSON.stringify(config)},
42
+ cronStatus: ${JSON.stringify(jobStatus)},
43
+ configForm: { ...${JSON.stringify(config)} },
44
+
45
+ // TOTP State
46
+ totpEnabled: ${!!config.auth?.totpSecret},
47
+ showTotpSetup: false,
48
+ showDisableTotp: false,
49
+ totpLoading: false,
50
+ totpVerifying: false,
51
+ totpDisabling: false,
52
+ totpSecret: '',
53
+ totpQrCode: '',
54
+ totpVerifyCode: '',
55
+ totpDisableCode: '',
56
+ totpError: '',
57
+
58
+ init() {
59
+ this.$nextTick(() => {
60
+ lucide.createIcons()
61
+ })
62
+ },
63
+
64
+ // TOTP Methods
65
+ async generateTotp() {
66
+ this.showTotpSetup = true;
67
+ this.totpLoading = true;
68
+ this.totpError = '';
69
+ this.$nextTick(() => lucide.createIcons());
70
+
71
+ try {
72
+ const response = await fetch('/backup/api/totp/generate', { method: 'POST' });
73
+ const data = await response.json();
74
+
75
+ if (data.status === 'success') {
76
+ this.totpSecret = data.secret;
77
+ this.totpQrCode = data.qrCode;
78
+ } else {
79
+ this.totpError = data.message || 'Failed to generate TOTP';
80
+ }
81
+ } catch (err) {
82
+ this.totpError = 'Connection failed. Please try again.';
83
+ } finally {
84
+ this.totpLoading = false;
85
+ this.$nextTick(() => lucide.createIcons());
86
+ }
87
+ },
88
+
89
+ async verifyTotp() {
90
+ this.totpVerifying = true;
91
+ this.totpError = '';
92
+
93
+ try {
94
+ const response = await fetch('/backup/api/totp/verify', {
95
+ method: 'POST',
96
+ headers: { 'Content-Type': 'application/json' },
97
+ body: JSON.stringify({
98
+ secret: this.totpSecret,
99
+ code: this.totpVerifyCode
100
+ })
101
+ });
102
+
103
+ const data = await response.json();
104
+
105
+ if (data.status === 'success') {
106
+ this.totpEnabled = true;
107
+ this.showTotpSetup = false;
108
+ this.totpSecret = '';
109
+ this.totpQrCode = '';
110
+ this.totpVerifyCode = '';
111
+ this.addLog('Two-factor authentication enabled', 'success');
112
+ } else {
113
+ this.totpError = data.message || 'Verification failed';
114
+ }
115
+ } catch (err) {
116
+ this.totpError = 'Connection failed. Please try again.';
117
+ } finally {
118
+ this.totpVerifying = false;
119
+ this.$nextTick(() => lucide.createIcons());
120
+ }
121
+ },
122
+
123
+ cancelTotpSetup() {
124
+ this.showTotpSetup = false;
125
+ this.totpSecret = '';
126
+ this.totpQrCode = '';
127
+ this.totpVerifyCode = '';
128
+ this.totpError = '';
129
+ this.$nextTick(() => lucide.createIcons());
130
+ },
131
+
132
+ async disableTotp() {
133
+ this.totpDisabling = true;
134
+ this.totpError = '';
135
+
136
+ try {
137
+ const response = await fetch('/backup/api/totp/disable', {
138
+ method: 'POST',
139
+ headers: { 'Content-Type': 'application/json' },
140
+ body: JSON.stringify({ code: this.totpDisableCode })
141
+ });
142
+
143
+ const data = await response.json();
144
+
145
+ if (data.status === 'success') {
146
+ this.totpEnabled = false;
147
+ this.showDisableTotp = false;
148
+ this.totpDisableCode = '';
149
+ this.addLog('Two-factor authentication disabled', 'info');
150
+ } else {
151
+ this.totpError = data.message || 'Failed to disable 2FA';
152
+ }
153
+ } catch (err) {
154
+ this.totpError = 'Connection failed. Please try again.';
155
+ } finally {
156
+ this.totpDisabling = false;
157
+ this.$nextTick(() => lucide.createIcons());
158
+ }
159
+ },
160
+
161
+ addLog(message, type = 'info') {
162
+ this.logs.unshift({
163
+ id: Date.now(),
164
+ message,
165
+ type,
166
+ time: new Date().toLocaleTimeString()
167
+ })
168
+ this.$nextTick(() => lucide.createIcons())
169
+ },
170
+
171
+ formatBytes(bytes, decimals = 2) {
172
+ if (!+bytes) return '0 Bytes'
173
+ const k = 1024
174
+ const dm = decimals < 0 ? 0 : decimals
175
+ const sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB']
176
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
177
+ return \`\${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} \${sizes[i]}\`
178
+ },
179
+
180
+ formatDateHeader(dateStr) {
181
+ if (dateStr === 'Others') return 'Others';
182
+ const [y, m, d] = dateStr.split('-').map(Number);
183
+ const date = new Date(y, m - 1, d);
184
+ return date.toLocaleDateString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
185
+ },
186
+
187
+ async runBackup() {
188
+ this.loading = true
189
+
190
+ const now = new Date()
191
+ const timestamp = now.getFullYear() + '-' +
192
+ String(now.getMonth() + 1).padStart(2, '0') + '-' +
193
+ String(now.getDate()).padStart(2, '0') + '_' +
194
+ String(now.getHours()).padStart(2, '0') + '-' +
195
+ String(now.getMinutes()).padStart(2, '0') + '-' +
196
+ String(now.getSeconds()).padStart(2, '0')
197
+
198
+ try {
199
+ const res = await fetch('/backup/api/run', {
200
+ method: 'POST',
201
+ headers: { 'Content-Type': 'application/json' },
202
+ body: JSON.stringify({ timestamp })
203
+ })
204
+ const data = await res.json()
205
+ if (data.status === 'success') {
206
+ this.lastBackup = new Date().toLocaleString()
207
+ this.addLog('Backup completed successfully', 'success')
208
+ this.fetchFiles()
209
+ } else {
210
+ throw new Error(data.message)
211
+ }
212
+ } catch (err) {
213
+ this.addLog('Backup failed: ' + err.message, 'error')
214
+ } finally {
215
+ this.loading = false
216
+ }
217
+ },
218
+
219
+ async fetchFiles() {
220
+ this.loadingFiles = true
221
+ try {
222
+ const res = await fetch('/backup/api/files')
223
+ const data = await res.json()
224
+ if (data.files) {
225
+ const sortedFiles = data.files.sort((a, b) => b.key.localeCompare(a.key));
226
+
227
+ const groupsMap = {};
228
+ sortedFiles.forEach(file => {
229
+ const match = file.key.match(/(?:^|\\/)(\\d{4}-\\d{2}-\\d{2})[_T]/);
230
+ const dateKey = match ? match[1] : 'Others';
231
+
232
+ if (!groupsMap[dateKey]) {
233
+ groupsMap[dateKey] = [];
234
+ }
235
+ groupsMap[dateKey].push(file);
236
+ });
237
+
238
+ this.groups = Object.keys(groupsMap)
239
+ .sort()
240
+ .reverse()
241
+ .map((dateKey, index) => ({
242
+ name: dateKey,
243
+ files: groupsMap[dateKey],
244
+ expanded: false
245
+ }));
246
+
247
+ this.$nextTick(() => lucide.createIcons())
248
+ }
249
+ } catch (err) {
250
+ this.addLog('Failed to fetch files: ' + err.message, 'error')
251
+ } finally {
252
+ this.loadingFiles = false
253
+ }
254
+ },
255
+
256
+ async restoreFile(key) {
257
+ try {
258
+ const res = await fetch('/backup/api/restore', {
259
+ method: 'POST',
260
+ headers: { 'Content-Type': 'application/json' },
261
+ body: JSON.stringify({ key })
262
+ })
263
+ const data = await res.json()
264
+ if (data.status === 'success') {
265
+ this.addLog(data.message, 'success')
266
+ } else {
267
+ throw new Error(data.message)
268
+ }
269
+ } catch (err) {
270
+ this.addLog('Restore failed: ' + err.message, 'error')
271
+ }
272
+ },
273
+
274
+ async deleteFile(key) {
275
+ try {
276
+ const res = await fetch('/backup/api/delete', {
277
+ method: 'POST',
278
+ headers: { 'Content-Type': 'application/json' },
279
+ body: JSON.stringify({ key })
280
+ })
281
+ const data = await res.json()
282
+ if (data.status === 'success') {
283
+ this.addLog(data.message, 'success')
284
+ this.fetchFiles()
285
+ } else {
286
+ throw new Error(data.message)
287
+ }
288
+ } catch (err) {
289
+ this.addLog('Delete failed: ' + err.message, 'error')
290
+ }
291
+ },
292
+
293
+ async saveConfig() {
294
+ try {
295
+ const res = await fetch('/backup/api/config', {
296
+ method: 'POST',
297
+ headers: { 'Content-Type': 'application/json' },
298
+ body: JSON.stringify(this.configForm)
299
+ })
300
+ const data = await res.json()
301
+ if (data.status === 'success') {
302
+ this.config = data.config
303
+ if (data.jobStatus) this.cronStatus = data.jobStatus
304
+ this.addLog('Configuration updated', 'success')
305
+ this.activeTab = 'dashboard'
306
+ }
307
+ } catch (err) {
308
+ this.addLog('Failed to save config: ' + err.message, 'error')
309
+ }
310
+ },
311
+
312
+ async logout() {
313
+ try {
314
+ await fetch('/backup/auth/logout', { method: 'POST' });
315
+ window.location.href = '/backup/login';
316
+ } catch (err) {
317
+ console.error('Logout failed:', err);
318
+ window.location.href = '/backup/login';
319
+ }
320
+ }
321
+ }
322
+ }
323
+ </script>
324
+ `
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Alpine.js login app logic
3
+ * @param {{ totpEnabled: boolean }} props
4
+ * @returns {string} JavaScript code string
5
+ */
6
+ export const loginAppScript = ({ totpEnabled }) => `
7
+ <script>
8
+ document.addEventListener('alpine:init', () => {
9
+ Alpine.data('loginApp', () => ({
10
+ username: '',
11
+ password: '',
12
+ totpCode: '',
13
+ totpEnabled: ${totpEnabled},
14
+ loading: false,
15
+ error: '',
16
+
17
+ init() {
18
+ this.$nextTick(() => lucide.createIcons());
19
+ },
20
+
21
+ async login() {
22
+ this.loading = true;
23
+ this.error = '';
24
+
25
+ try {
26
+ const payload = {
27
+ username: this.username,
28
+ password: this.password
29
+ };
30
+
31
+ if (this.totpEnabled && this.totpCode) {
32
+ payload.totpCode = this.totpCode;
33
+ }
34
+
35
+ const response = await fetch('/backup/auth/login', {
36
+ method: 'POST',
37
+ headers: { 'Content-Type': 'application/json' },
38
+ body: JSON.stringify(payload)
39
+ });
40
+
41
+ const data = await response.json();
42
+
43
+ if (response.ok && data.status === 'success') {
44
+ window.location.href = '/backup';
45
+ } else {
46
+ this.error = data.message || 'Invalid credentials';
47
+ this.$nextTick(() => lucide.createIcons());
48
+ }
49
+ } catch (err) {
50
+ this.error = 'Connection failed. Please try again.';
51
+ this.$nextTick(() => lucide.createIcons());
52
+ } finally {
53
+ this.loading = false;
54
+ this.$nextTick(() => lucide.createIcons());
55
+ }
56
+ }
57
+ }));
58
+ });
59
+ </script>
60
+ `