@riligar/elysia-backup 1.3.0 → 1.4.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.
- package/package.json +4 -4
- package/src/assets/favicon.ico +0 -0
- package/src/assets/logo.png +0 -0
- package/src/core/config.js +70 -0
- package/src/core/scheduler.js +99 -0
- package/src/core/session.js +90 -0
- package/src/index.js +609 -3
- package/src/middleware/auth.middleware.js +41 -0
- package/src/services/backup.service.js +169 -0
- package/src/services/s3-client.js +28 -0
- package/src/views/DashboardPage.js +56 -0
- package/src/views/LoginPage.js +23 -0
- package/src/views/components/ActionArea.js +48 -0
- package/src/views/components/FilesTab.js +111 -0
- package/src/views/components/Head.js +43 -0
- package/src/views/components/Header.js +54 -0
- package/src/views/components/LoginCard.js +114 -0
- package/src/views/components/SecuritySection.js +166 -0
- package/src/views/components/SettingsTab.js +88 -0
- package/src/views/components/StatusCards.js +54 -0
- package/src/views/scripts/backupApp.js +324 -0
- package/src/views/scripts/loginApp.js +60 -0
- package/src/elysia-backup.js +0 -1736
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security section component with TOTP 2FA setup
|
|
3
|
+
* @returns {string} HTML string
|
|
4
|
+
*/
|
|
5
|
+
export const SecuritySection = () => `
|
|
6
|
+
<div class="bg-white rounded-2xl border border-gray-100 shadow-[0_2px_8px_rgba(0,0,0,0.04)] p-10 mt-8">
|
|
7
|
+
<h2 class="text-xl font-bold text-gray-900 mb-8 flex items-center gap-2">
|
|
8
|
+
<i data-lucide="shield" class="w-5 h-5"></i>
|
|
9
|
+
Security
|
|
10
|
+
</h2>
|
|
11
|
+
|
|
12
|
+
<!-- TOTP Setup -->
|
|
13
|
+
<div class="space-y-6">
|
|
14
|
+
<div class="flex items-start justify-between">
|
|
15
|
+
<div>
|
|
16
|
+
<h3 class="font-semibold text-gray-900 flex items-center gap-2">
|
|
17
|
+
<i data-lucide="smartphone" class="w-4 h-4"></i>
|
|
18
|
+
Two-Factor Authentication (2FA)
|
|
19
|
+
</h3>
|
|
20
|
+
<p class="text-sm text-gray-500 mt-1">
|
|
21
|
+
Add an extra layer of security using an authenticator app
|
|
22
|
+
</p>
|
|
23
|
+
</div>
|
|
24
|
+
<div x-show="!totpEnabled && !showTotpSetup">
|
|
25
|
+
<button
|
|
26
|
+
@click="generateTotp()"
|
|
27
|
+
class="bg-gray-900 hover:bg-gray-800 text-white font-bold py-2.5 px-5 rounded-lg transition-all flex items-center gap-2"
|
|
28
|
+
>
|
|
29
|
+
<i data-lucide="plus" class="w-4 h-4"></i>
|
|
30
|
+
Enable 2FA
|
|
31
|
+
</button>
|
|
32
|
+
</div>
|
|
33
|
+
<div x-show="totpEnabled && !showTotpSetup">
|
|
34
|
+
<span class="inline-flex items-center gap-2 px-4 py-2 bg-green-100 text-green-800 rounded-lg font-semibold text-sm">
|
|
35
|
+
<i data-lucide="check-circle" class="w-4 h-4"></i>
|
|
36
|
+
Enabled
|
|
37
|
+
</span>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<!-- TOTP Setup Flow -->
|
|
42
|
+
<div x-show="showTotpSetup" x-cloak class="border-t border-gray-100 pt-6 mt-6">
|
|
43
|
+
<!-- Loading -->
|
|
44
|
+
<div x-show="totpLoading" class="text-center py-8">
|
|
45
|
+
<i data-lucide="loader-2" class="w-8 h-8 animate-spin text-gray-400 mx-auto"></i>
|
|
46
|
+
<p class="text-sm text-gray-500 mt-2">Generating secure key...</p>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<!-- QR Code Display -->
|
|
50
|
+
<div x-show="!totpLoading && totpQrCode" class="space-y-6">
|
|
51
|
+
<div class="bg-gray-50 rounded-xl p-6 text-center">
|
|
52
|
+
<p class="text-sm font-medium text-gray-700 mb-4">
|
|
53
|
+
Scan this QR code with your authenticator app:
|
|
54
|
+
</p>
|
|
55
|
+
<img :src="totpQrCode" alt="TOTP QR Code" class="mx-auto w-48 h-48 rounded-lg shadow-sm">
|
|
56
|
+
|
|
57
|
+
<div class="mt-4 text-xs text-gray-500">
|
|
58
|
+
<p class="mb-2">Or enter this code manually:</p>
|
|
59
|
+
<code class="bg-white px-3 py-1.5 rounded border border-gray-200 font-mono text-gray-800 select-all" x-text="totpSecret"></code>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<!-- Verification -->
|
|
64
|
+
<div class="space-y-4">
|
|
65
|
+
<label class="block text-sm font-semibold text-gray-700">
|
|
66
|
+
Enter the 6-digit code from your authenticator app:
|
|
67
|
+
</label>
|
|
68
|
+
<div class="flex gap-4">
|
|
69
|
+
<input
|
|
70
|
+
type="text"
|
|
71
|
+
x-model="totpVerifyCode"
|
|
72
|
+
inputmode="numeric"
|
|
73
|
+
pattern="[0-9]*"
|
|
74
|
+
maxlength="6"
|
|
75
|
+
placeholder="000000"
|
|
76
|
+
class="flex-grow bg-gray-50 border border-gray-200 rounded-lg px-4 py-3 text-gray-900 focus:ring-2 focus:ring-gray-900 focus:border-transparent outline-none transition-all font-medium tracking-widest text-center text-lg"
|
|
77
|
+
>
|
|
78
|
+
<button
|
|
79
|
+
@click="verifyTotp()"
|
|
80
|
+
:disabled="totpVerifyCode.length !== 6 || totpVerifying"
|
|
81
|
+
class="bg-green-600 hover:bg-green-700 text-white font-bold py-3 px-6 rounded-lg transition-all flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
82
|
+
>
|
|
83
|
+
<template x-if="!totpVerifying">
|
|
84
|
+
<span class="flex items-center gap-2">
|
|
85
|
+
<i data-lucide="check" class="w-4 h-4"></i>
|
|
86
|
+
Verify & Enable
|
|
87
|
+
</span>
|
|
88
|
+
</template>
|
|
89
|
+
<template x-if="totpVerifying">
|
|
90
|
+
<span class="flex items-center gap-2">
|
|
91
|
+
<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i>
|
|
92
|
+
Verifying...
|
|
93
|
+
</span>
|
|
94
|
+
</template>
|
|
95
|
+
</button>
|
|
96
|
+
</div>
|
|
97
|
+
<div x-show="totpError" x-cloak class="bg-red-50 border border-red-200 rounded-lg p-3 text-sm text-red-800 flex items-center gap-2">
|
|
98
|
+
<i data-lucide="alert-circle" class="w-4 h-4"></i>
|
|
99
|
+
<span x-text="totpError"></span>
|
|
100
|
+
</div>
|
|
101
|
+
<button
|
|
102
|
+
@click="cancelTotpSetup()"
|
|
103
|
+
class="text-sm text-gray-500 hover:text-gray-700 underline"
|
|
104
|
+
>
|
|
105
|
+
Cancel
|
|
106
|
+
</button>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<!-- Disable 2FA -->
|
|
112
|
+
<div x-show="totpEnabled && !showTotpSetup" x-cloak class="border-t border-gray-100 pt-6 mt-6">
|
|
113
|
+
<div x-show="!showDisableTotp">
|
|
114
|
+
<button
|
|
115
|
+
@click="showDisableTotp = true"
|
|
116
|
+
class="text-sm text-red-600 hover:text-red-700 font-medium flex items-center gap-2"
|
|
117
|
+
>
|
|
118
|
+
<i data-lucide="shield-off" class="w-4 h-4"></i>
|
|
119
|
+
Disable two-factor authentication
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
<div x-show="showDisableTotp" class="space-y-4">
|
|
123
|
+
<p class="text-sm text-gray-600">
|
|
124
|
+
Enter your current authenticator code to disable 2FA:
|
|
125
|
+
</p>
|
|
126
|
+
<div class="flex gap-4">
|
|
127
|
+
<input
|
|
128
|
+
type="text"
|
|
129
|
+
x-model="totpDisableCode"
|
|
130
|
+
inputmode="numeric"
|
|
131
|
+
pattern="[0-9]*"
|
|
132
|
+
maxlength="6"
|
|
133
|
+
placeholder="000000"
|
|
134
|
+
class="flex-grow bg-gray-50 border border-gray-200 rounded-lg px-4 py-3 text-gray-900 focus:ring-2 focus:ring-gray-900 focus:border-transparent outline-none transition-all font-medium tracking-widest text-center text-lg"
|
|
135
|
+
>
|
|
136
|
+
<button
|
|
137
|
+
@click="disableTotp()"
|
|
138
|
+
:disabled="totpDisableCode.length !== 6 || totpDisabling"
|
|
139
|
+
class="bg-red-600 hover:bg-red-700 text-white font-bold py-3 px-6 rounded-lg transition-all flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
140
|
+
>
|
|
141
|
+
<template x-if="!totpDisabling">
|
|
142
|
+
<span>Disable 2FA</span>
|
|
143
|
+
</template>
|
|
144
|
+
<template x-if="totpDisabling">
|
|
145
|
+
<span class="flex items-center gap-2">
|
|
146
|
+
<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i>
|
|
147
|
+
Disabling...
|
|
148
|
+
</span>
|
|
149
|
+
</template>
|
|
150
|
+
</button>
|
|
151
|
+
</div>
|
|
152
|
+
<div x-show="totpError" x-cloak class="bg-red-50 border border-red-200 rounded-lg p-3 text-sm text-red-800 flex items-center gap-2">
|
|
153
|
+
<i data-lucide="alert-circle" class="w-4 h-4"></i>
|
|
154
|
+
<span x-text="totpError"></span>
|
|
155
|
+
</div>
|
|
156
|
+
<button
|
|
157
|
+
@click="showDisableTotp = false; totpDisableCode = ''; totpError = ''"
|
|
158
|
+
class="text-sm text-gray-500 hover:text-gray-700 underline"
|
|
159
|
+
>
|
|
160
|
+
Cancel
|
|
161
|
+
</button>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
`
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings tab component with configuration form
|
|
3
|
+
* @returns {string} HTML string
|
|
4
|
+
*/
|
|
5
|
+
export const SettingsTab = () => `
|
|
6
|
+
<div class="bg-white rounded-2xl border border-gray-100 shadow-[0_2px_8px_rgba(0,0,0,0.04)] p-10">
|
|
7
|
+
<h2 class="text-xl font-bold text-gray-900 mb-8 flex items-center gap-2">
|
|
8
|
+
<i data-lucide="sliders" class="w-5 h-5"></i>
|
|
9
|
+
Configuration
|
|
10
|
+
</h2>
|
|
11
|
+
<form @submit.prevent="saveConfig" class="space-y-8">
|
|
12
|
+
<div class="space-y-6">
|
|
13
|
+
<div>
|
|
14
|
+
<label class="block text-sm font-semibold text-gray-700 mb-2">Endpoint URL</label>
|
|
15
|
+
<input type="text" x-model="configForm.endpoint" class="w-full bg-gray-50 border border-gray-200 rounded-lg px-4 py-3 text-gray-900 focus:ring-2 focus:ring-gray-900 focus:border-transparent outline-none transition-all font-medium">
|
|
16
|
+
</div>
|
|
17
|
+
<div class="grid grid-cols-2 gap-6">
|
|
18
|
+
<div>
|
|
19
|
+
<label class="block text-sm font-semibold text-gray-700 mb-2">Bucket Name</label>
|
|
20
|
+
<input type="text" x-model="configForm.bucket" class="w-full bg-gray-50 border border-gray-200 rounded-lg px-4 py-3 text-gray-900 focus:ring-2 focus:ring-gray-900 focus:border-transparent outline-none transition-all font-medium">
|
|
21
|
+
</div>
|
|
22
|
+
<div>
|
|
23
|
+
<label class="block text-sm font-semibold text-gray-700 mb-2">Prefix (Folder)</label>
|
|
24
|
+
<input type="text" x-model="configForm.prefix" class="w-full bg-gray-50 border border-gray-200 rounded-lg px-4 py-3 text-gray-900 focus:ring-2 focus:ring-gray-900 focus:border-transparent outline-none transition-all font-medium">
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
<div>
|
|
28
|
+
<label class="block text-sm font-semibold text-gray-700 mb-2">Source Directory (Local)</label>
|
|
29
|
+
<input type="text" x-model="configForm.sourceDir" readonly class="w-full bg-gray-100 border border-gray-200 rounded-lg px-4 py-3 text-gray-500 cursor-not-allowed focus:outline-none font-medium">
|
|
30
|
+
<p class="text-xs text-gray-400 mt-1">Defined in server configuration</p>
|
|
31
|
+
</div>
|
|
32
|
+
<div>
|
|
33
|
+
<label class="block text-sm font-semibold text-gray-700 mb-2">Allowed Extensions (comma separated)</label>
|
|
34
|
+
<input type="text" x-model="configForm.extensions" placeholder=".db, .sqlite" class="w-full bg-gray-50 border border-gray-200 rounded-lg px-4 py-3 text-gray-900 focus:ring-2 focus:ring-gray-900 focus:border-transparent outline-none transition-all font-medium">
|
|
35
|
+
</div>
|
|
36
|
+
<div>
|
|
37
|
+
<label class="block text-sm font-semibold text-gray-700 mb-2">Cron Schedule</label>
|
|
38
|
+
<div class="flex gap-4 items-center">
|
|
39
|
+
<div class="relative flex-grow">
|
|
40
|
+
<input type="text" x-model="configForm.cronSchedule" placeholder="0 0 * * *" class="w-full bg-gray-50 border border-gray-200 rounded-lg px-4 py-3 text-gray-900 focus:ring-2 focus:ring-gray-900 focus:border-transparent outline-none transition-all font-medium">
|
|
41
|
+
<div class="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-gray-500">
|
|
42
|
+
<a href="https://crontab.guru/" target="_blank" class="underline hover:text-gray-800">Help</a>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="flex gap-2 shrink-0">
|
|
46
|
+
<button
|
|
47
|
+
type="button"
|
|
48
|
+
@click="configForm.cronEnabled = true; saveConfig()"
|
|
49
|
+
:class="configForm.cronEnabled !== false ? 'bg-green-600 text-white shadow-md' : 'bg-gray-100 text-gray-400 hover:bg-gray-200'"
|
|
50
|
+
class="px-4 py-3 rounded-lg font-bold text-sm transition-all flex items-center gap-2"
|
|
51
|
+
>
|
|
52
|
+
<i data-lucide="play" class="w-4 h-4"></i>
|
|
53
|
+
Start
|
|
54
|
+
</button>
|
|
55
|
+
<button
|
|
56
|
+
type="button"
|
|
57
|
+
@click="configForm.cronEnabled = false; saveConfig()"
|
|
58
|
+
:class="configForm.cronEnabled === false ? 'bg-red-600 text-white shadow-md' : 'bg-gray-100 text-gray-400 hover:bg-gray-200'"
|
|
59
|
+
class="px-4 py-3 rounded-lg font-bold text-sm transition-all flex items-center gap-2"
|
|
60
|
+
>
|
|
61
|
+
<i data-lucide="square" class="w-4 h-4"></i>
|
|
62
|
+
Stop
|
|
63
|
+
</button>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
<p class="text-xs text-gray-400 mt-1">Format: Minute Hour Day Month DayOfWeek (e.g., "0 0 * * *" for daily at midnight)</p>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="grid grid-cols-2 gap-6">
|
|
69
|
+
<div>
|
|
70
|
+
<label class="block text-sm font-semibold text-gray-700 mb-2">Access Key ID</label>
|
|
71
|
+
<input type="text" x-model="configForm.accessKeyId" class="w-full bg-gray-50 border border-gray-200 rounded-lg px-4 py-3 text-gray-900 focus:ring-2 focus:ring-gray-900 focus:border-transparent outline-none transition-all font-medium">
|
|
72
|
+
</div>
|
|
73
|
+
<div>
|
|
74
|
+
<label class="block text-sm font-semibold text-gray-700 mb-2">Secret Access Key</label>
|
|
75
|
+
<input type="password" x-model="configForm.secretAccessKey" class="w-full bg-gray-50 border border-gray-200 rounded-lg px-4 py-3 text-gray-900 focus:ring-2 focus:ring-gray-900 focus:border-transparent outline-none transition-all font-medium">
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div class="pt-4 flex justify-end">
|
|
81
|
+
<button type="submit" class="bg-gray-900 hover:bg-gray-800 text-white font-bold py-3 px-8 rounded-xl transition-all shadow-lg hover:shadow-xl transform hover:-translate-y-0.5 flex items-center gap-2">
|
|
82
|
+
<i data-lucide="save" class="w-4 h-4"></i>
|
|
83
|
+
Save Changes
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
</form>
|
|
87
|
+
</div>
|
|
88
|
+
`
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status cards component for dashboard overview
|
|
3
|
+
* @returns {string} HTML string
|
|
4
|
+
*/
|
|
5
|
+
export const StatusCards = () => `
|
|
6
|
+
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
7
|
+
<div class="bg-white p-8 rounded-2xl border border-gray-100 shadow-[0_2px_8px_rgba(0,0,0,0.04)]">
|
|
8
|
+
<div class="text-gray-500 text-sm font-medium mb-2 flex items-center gap-2">
|
|
9
|
+
<i data-lucide="folder" class="w-4 h-4"></i>
|
|
10
|
+
Local Source
|
|
11
|
+
</div>
|
|
12
|
+
<div class="text-gray-900 font-semibold truncate" x-text="config.sourceDir"></div>
|
|
13
|
+
</div>
|
|
14
|
+
<div class="bg-white p-8 rounded-2xl border border-gray-100 shadow-[0_2px_8px_rgba(0,0,0,0.04)]">
|
|
15
|
+
<div class="text-gray-500 text-sm font-medium mb-2 flex items-center gap-2">
|
|
16
|
+
<i data-lucide="cloud" class="w-4 h-4"></i>
|
|
17
|
+
Target Bucket
|
|
18
|
+
</div>
|
|
19
|
+
<div class="text-gray-900 font-semibold truncate" x-text="config.bucket"></div>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="bg-white p-8 rounded-2xl border border-gray-100 shadow-[0_2px_8px_rgba(0,0,0,0.04)]">
|
|
22
|
+
<div class="text-gray-500 text-sm font-medium mb-2 flex items-center gap-2">
|
|
23
|
+
<i data-lucide="clock" class="w-4 h-4"></i>
|
|
24
|
+
Last Backup
|
|
25
|
+
</div>
|
|
26
|
+
<div class="text-gray-900 font-semibold" x-text="lastBackup || 'No backup recorded'"></div>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="bg-white p-8 rounded-2xl border border-gray-100 shadow-[0_2px_8px_rgba(0,0,0,0.04)] relative overflow-hidden group">
|
|
29
|
+
<div class="text-gray-500 text-sm font-medium mb-3 flex items-center gap-2">
|
|
30
|
+
<i data-lucide="calendar-clock" class="w-4 h-4"></i>
|
|
31
|
+
Schedule Status
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<template x-if="cronStatus.isRunning">
|
|
35
|
+
<div class="flex items-center gap-3">
|
|
36
|
+
<span class="relative flex h-2.5 w-2.5 shrink-0" title="Active">
|
|
37
|
+
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
|
|
38
|
+
<span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-green-500"></span>
|
|
39
|
+
</span>
|
|
40
|
+
<div class="flex items-baseline gap-1.5 min-w-0">
|
|
41
|
+
<span class="text-gray-900 font-bold text-xs font-mono truncate" x-text="new Date(cronStatus.nextRun).toLocaleString(undefined, { dateStyle: 'short', timeStyle: 'medium' })"></span>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</template>
|
|
45
|
+
|
|
46
|
+
<template x-if="!cronStatus.isRunning">
|
|
47
|
+
<div class="flex items-center gap-3">
|
|
48
|
+
<span class="h-2.5 w-2.5 rounded-full bg-gray-300 shrink-0" title="Stopped"></span>
|
|
49
|
+
<span class="text-gray-400 text-sm italic">Scheduler paused</span>
|
|
50
|
+
</div>
|
|
51
|
+
</template>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
`
|
|
@@ -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
|
+
`
|