@riligar/elysia-backup 1.6.0 → 1.7.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/README.md +158 -46
- package/package.json +1 -1
- package/src/index.js +47 -10
- package/src/services/backup.service.js +40 -15
- package/src/views/DashboardPage.js +4 -0
- package/src/views/LoginPage.js +9 -2
- package/src/views/OnboardingPage.js +9 -2
- package/src/views/components/Footer.js +25 -0
- package/src/views/scripts/backupApp.js +27 -2
package/README.md
CHANGED
|
@@ -1,23 +1,37 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @riligar/elysia-backup
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://riligar.click/)
|
|
4
|
+
[](https://www.npmjs.com/package/@riligar/elysia-backup)
|
|
5
|
+
[](LICENSE)
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
> **An open source project by [RiLiGar](https://riligar.click/)**
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
- 🔄 Scheduled backups with cron expressions
|
|
9
|
-
- 🖥️ Built-in UI dashboard
|
|
10
|
-
- ⬇️ One-click restore
|
|
11
|
-
- 🗑️ Delete remote backups
|
|
12
|
-
- ⚙️ Runtime configuration
|
|
9
|
+
A powerful Elysia plugin for R2/S3 backup with a beautiful built-in UI dashboard. Designed for Bun runtime with native S3 client for optimal performance.
|
|
13
10
|
|
|
14
|
-
##
|
|
11
|
+
## ✨ Features
|
|
12
|
+
|
|
13
|
+
- 📁 **Smart Backup** — Backup local directories to R2/S3 with file filtering
|
|
14
|
+
- 🔄 **Scheduled Backups** — Cron-based automatic backups
|
|
15
|
+
- 🖥️ **Beautiful Dashboard** — Modern UI with real-time status
|
|
16
|
+
- 🔐 **Secure Authentication** — Session-based auth with 2FA/TOTP support
|
|
17
|
+
- 🧭 **Guided Onboarding** — Multi-step setup wizard for first-time configuration
|
|
18
|
+
- ⬇️ **One-Click Restore** — Easily restore files from backup
|
|
19
|
+
- 🗑️ **Backup Management** — Delete old backups from the UI
|
|
20
|
+
- ⚙️ **Runtime Configuration** — Update settings without restarting
|
|
21
|
+
|
|
22
|
+
## 📦 Installation
|
|
15
23
|
|
|
16
24
|
```bash
|
|
17
25
|
bun add @riligar/elysia-backup
|
|
18
26
|
```
|
|
19
27
|
|
|
20
|
-
|
|
28
|
+
### Peer Dependencies
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
bun add elysia @elysiajs/html
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## 🚀 Quick Start
|
|
21
35
|
|
|
22
36
|
```javascript
|
|
23
37
|
import { Elysia } from 'elysia'
|
|
@@ -26,60 +40,158 @@ import { r2Backup } from '@riligar/elysia-backup'
|
|
|
26
40
|
const app = new Elysia()
|
|
27
41
|
.use(
|
|
28
42
|
r2Backup({
|
|
29
|
-
bucket: process.env.R2_BUCKET,
|
|
30
|
-
accessKeyId: process.env.R2_ACCESS_KEY_ID,
|
|
31
|
-
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
|
|
32
|
-
endpoint: process.env.R2_ENDPOINT,
|
|
33
43
|
sourceDir: './data',
|
|
34
|
-
|
|
35
|
-
extensions: ['.db', '.sqlite'],
|
|
36
|
-
cronSchedule: '0 0 * * *', // Daily at midnight
|
|
37
|
-
cronEnabled: true,
|
|
44
|
+
configPath: './config.json',
|
|
38
45
|
})
|
|
39
46
|
)
|
|
40
47
|
.listen(3000)
|
|
41
48
|
|
|
42
|
-
console.log('Server running at http://localhost:3000')
|
|
43
|
-
console.log('Backup UI at http://localhost:3000/backup')
|
|
49
|
+
console.log('🦊 Server running at http://localhost:3000')
|
|
50
|
+
console.log('📦 Backup UI at http://localhost:3000/backup')
|
|
44
51
|
```
|
|
45
52
|
|
|
46
|
-
|
|
53
|
+
On first run, you'll be guided through an onboarding wizard to configure:
|
|
54
|
+
|
|
55
|
+
- Storage credentials (R2/S3)
|
|
56
|
+
- Backup schedule
|
|
57
|
+
- Admin authentication
|
|
58
|
+
|
|
59
|
+
## ⚙️ Configuration
|
|
60
|
+
|
|
61
|
+
### Plugin Options
|
|
47
62
|
|
|
48
|
-
| Option
|
|
49
|
-
|
|
|
50
|
-
| `
|
|
51
|
-
| `
|
|
52
|
-
| `secretAccessKey` | string | ✅ | R2/S3 Secret Access Key |
|
|
53
|
-
| `endpoint` | string | ✅ | R2/S3 Endpoint URL |
|
|
54
|
-
| `sourceDir` | string | ✅ | Local directory to backup |
|
|
55
|
-
| `prefix` | string | ❌ | Prefix for S3 keys (e.g., 'backups/') |
|
|
56
|
-
| `extensions` | string[] | ❌ | File extensions to include |
|
|
57
|
-
| `cronSchedule` | string | ❌ | Cron expression for scheduled backups |
|
|
58
|
-
| `cronEnabled` | boolean | ❌ | Enable/disable scheduled backups |
|
|
59
|
-
| `configPath` | string | ❌ | Path to save runtime config (default: './config.json') |
|
|
63
|
+
| Option | Type | Required | Description |
|
|
64
|
+
| ------------ | ------ | -------- | ------------------------------------------------------ |
|
|
65
|
+
| `sourceDir` | string | ✅ | Local directory to backup |
|
|
66
|
+
| `configPath` | string | ❌ | Path to save runtime config (default: `./config.json`) |
|
|
60
67
|
|
|
61
|
-
|
|
68
|
+
### Runtime Configuration (via UI or config.json)
|
|
69
|
+
|
|
70
|
+
| Option | Type | Description |
|
|
71
|
+
| ----------------- | -------- | ---------------------------------------------- |
|
|
72
|
+
| `bucket` | string | R2/S3 bucket name |
|
|
73
|
+
| `accessKeyId` | string | R2/S3 Access Key ID |
|
|
74
|
+
| `secretAccessKey` | string | R2/S3 Secret Access Key |
|
|
75
|
+
| `endpoint` | string | R2/S3 Endpoint URL |
|
|
76
|
+
| `prefix` | string | Prefix for S3 keys (e.g., `backups/`) |
|
|
77
|
+
| `extensions` | string[] | File extensions to include (empty = all files) |
|
|
78
|
+
| `cronSchedule` | string | Cron expression for scheduled backups |
|
|
79
|
+
| `cronEnabled` | boolean | Enable/disable scheduled backups |
|
|
80
|
+
| `auth.username` | string | Admin username |
|
|
81
|
+
| `auth.password` | string | Admin password |
|
|
82
|
+
| `auth.totpSecret` | string | TOTP secret for 2FA (optional) |
|
|
83
|
+
|
|
84
|
+
## 🔌 API Endpoints
|
|
62
85
|
|
|
63
86
|
The plugin adds the following routes under `/backup`:
|
|
64
87
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
|
68
|
-
|
|
|
69
|
-
| GET | `/backup
|
|
70
|
-
|
|
|
71
|
-
|
|
|
72
|
-
|
|
88
|
+
### Pages
|
|
89
|
+
|
|
90
|
+
| Method | Path | Description |
|
|
91
|
+
| ------ | -------------------- | ----------------------- |
|
|
92
|
+
| GET | `/backup` | Dashboard UI |
|
|
93
|
+
| GET | `/backup/login` | Login page |
|
|
94
|
+
| GET | `/backup/onboarding` | First-time setup wizard |
|
|
95
|
+
|
|
96
|
+
### Authentication
|
|
97
|
+
|
|
98
|
+
| Method | Path | Description |
|
|
99
|
+
| ------ | --------------------- | ----------------- |
|
|
100
|
+
| POST | `/backup/auth/login` | Authenticate user |
|
|
101
|
+
| POST | `/backup/auth/logout` | End session |
|
|
102
|
+
|
|
103
|
+
### Backup Operations
|
|
104
|
+
|
|
105
|
+
| Method | Path | Description |
|
|
106
|
+
| ------ | --------------------- | --------------------------- |
|
|
107
|
+
| POST | `/backup/api/run` | Trigger manual backup |
|
|
108
|
+
| GET | `/backup/api/files` | List remote backup files |
|
|
109
|
+
| POST | `/backup/api/restore` | Restore a specific file |
|
|
110
|
+
| POST | `/backup/api/delete` | Delete a remote backup file |
|
|
111
|
+
| POST | `/backup/api/config` | Update configuration |
|
|
112
|
+
|
|
113
|
+
### Two-Factor Authentication (TOTP)
|
|
114
|
+
|
|
115
|
+
| Method | Path | Description |
|
|
116
|
+
| ------ | --------------------------- | ----------------------------- |
|
|
117
|
+
| GET | `/backup/api/totp/status` | Check 2FA status |
|
|
118
|
+
| POST | `/backup/api/totp/generate` | Generate new TOTP secret & QR |
|
|
119
|
+
| POST | `/backup/api/totp/verify` | Verify and enable 2FA |
|
|
120
|
+
| POST | `/backup/api/totp/disable` | Disable 2FA |
|
|
73
121
|
|
|
74
|
-
##
|
|
122
|
+
## 🔐 Security Features
|
|
123
|
+
|
|
124
|
+
### Session-Based Authentication
|
|
125
|
+
|
|
126
|
+
- Secure session tokens with configurable expiration
|
|
127
|
+
- HTTP-only cookies for session management
|
|
128
|
+
- Automatic session cleanup
|
|
129
|
+
|
|
130
|
+
### Two-Factor Authentication (TOTP)
|
|
131
|
+
|
|
132
|
+
- Compatible with any authenticator app (Google Authenticator, Authy, etc.)
|
|
133
|
+
- QR code for easy setup
|
|
134
|
+
- Backup codes support
|
|
135
|
+
|
|
136
|
+
## 📂 Project Structure
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
src/
|
|
140
|
+
├── index.js # Main plugin entry
|
|
141
|
+
├── assets/ # Static assets (favicon, logo)
|
|
142
|
+
├── views/
|
|
143
|
+
│ ├── LoginPage.js # Login page
|
|
144
|
+
│ ├── DashboardPage.js # Main dashboard
|
|
145
|
+
│ ├── OnboardingPage.js # Setup wizard
|
|
146
|
+
│ ├── components/ # UI components
|
|
147
|
+
│ │ ├── ActionArea.js
|
|
148
|
+
│ │ ├── FilesTab.js
|
|
149
|
+
│ │ ├── Footer.js
|
|
150
|
+
│ │ ├── Head.js
|
|
151
|
+
│ │ ├── Header.js
|
|
152
|
+
│ │ ├── LoginCard.js
|
|
153
|
+
│ │ ├── OnboardingCard.js
|
|
154
|
+
│ │ ├── SecuritySection.js
|
|
155
|
+
│ │ ├── SettingsTab.js
|
|
156
|
+
│ │ └── StatusCards.js
|
|
157
|
+
│ └── scripts/ # Client-side JavaScript
|
|
158
|
+
│ ├── backupApp.js
|
|
159
|
+
│ ├── loginApp.js
|
|
160
|
+
│ └── onboardingApp.js
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## 🌍 Environment Variables Example
|
|
75
164
|
|
|
76
165
|
```env
|
|
166
|
+
# Optional - for initial configuration
|
|
77
167
|
R2_BUCKET=your-bucket-name
|
|
78
168
|
R2_ACCESS_KEY_ID=your-access-key
|
|
79
169
|
R2_SECRET_ACCESS_KEY=your-secret-key
|
|
80
170
|
R2_ENDPOINT=https://your-account.r2.cloudflarestorage.com
|
|
81
171
|
```
|
|
82
172
|
|
|
83
|
-
|
|
173
|
+
> **Note:** All configuration can be set via the onboarding wizard. Environment variables are optional.
|
|
174
|
+
|
|
175
|
+
## 📸 Screenshots
|
|
176
|
+
|
|
177
|
+
The dashboard provides:
|
|
178
|
+
|
|
179
|
+
- **Status Overview** — Cron status, next run time, bucket info
|
|
180
|
+
- **Quick Actions** — Manual backup trigger with real-time feedback
|
|
181
|
+
- **Files Browser** — View, restore, and delete backup files
|
|
182
|
+
- **Settings** — Update storage and backup configuration
|
|
183
|
+
- **Security** — Enable/disable 2FA
|
|
184
|
+
|
|
185
|
+
## 🤝 Contributing
|
|
186
|
+
|
|
187
|
+
Contributions are welcome! See our [GitHub repository](https://github.com/riligar-solutions/elysia-backup) for more information.
|
|
188
|
+
|
|
189
|
+
## 📄 License
|
|
190
|
+
|
|
191
|
+
MIT © [RiLiGar](https://riligar.click/)
|
|
192
|
+
|
|
193
|
+
---
|
|
84
194
|
|
|
85
|
-
|
|
195
|
+
<p align="center">
|
|
196
|
+
Made with ❤️ by <a href="https://riligar.click/">RiLiGar</a>
|
|
197
|
+
</p>
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -132,10 +132,23 @@ export const r2Backup = initialConfig => app => {
|
|
|
132
132
|
const dir = dirname(relativePath)
|
|
133
133
|
const filename = relativePath.split('/').pop()
|
|
134
134
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
135
|
+
// Parse timestamp to extract date and time parts
|
|
136
|
+
// Expected format: YYYY-MM-DD_HH-mm-ss
|
|
137
|
+
const timestamp = timestampPrefix || new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
|
|
138
|
+
const datePart = timestamp.slice(0, 10) // YYYY-MM-DD
|
|
139
|
+
const timePart = timestamp.slice(11) || '00-00-00' // HH-mm-ss
|
|
140
|
+
|
|
141
|
+
// Format: YYYY-MM-DD/HH-mm-ss_filename.ext
|
|
142
|
+
const newFilename = `${timePart}_${filename}`
|
|
143
|
+
const dateFolder = datePart
|
|
144
|
+
|
|
145
|
+
// Reconstruct path: prefix/date/[subdir/]time_filename
|
|
146
|
+
let finalPath
|
|
147
|
+
if (dir === '.') {
|
|
148
|
+
finalPath = join(dateFolder, newFilename)
|
|
149
|
+
} else {
|
|
150
|
+
finalPath = join(dateFolder, dir, newFilename)
|
|
151
|
+
}
|
|
139
152
|
const key = config.prefix ? join(config.prefix, finalPath) : finalPath
|
|
140
153
|
|
|
141
154
|
console.log(`Uploading ${key}...`)
|
|
@@ -178,7 +191,6 @@ export const r2Backup = initialConfig => app => {
|
|
|
178
191
|
backupJob = new CronJob(
|
|
179
192
|
config.cronSchedule,
|
|
180
193
|
async () => {
|
|
181
|
-
console.log('Running scheduled backup...')
|
|
182
194
|
try {
|
|
183
195
|
const now = new Date()
|
|
184
196
|
const timestamp =
|
|
@@ -248,16 +260,34 @@ export const r2Backup = initialConfig => app => {
|
|
|
248
260
|
const arrayBuffer = await file.arrayBuffer()
|
|
249
261
|
const byteArray = new Uint8Array(arrayBuffer)
|
|
250
262
|
|
|
263
|
+
// Remove prefix from key to get relative path
|
|
251
264
|
const relativePath = config.prefix ? key.replace(config.prefix, '') : key
|
|
252
265
|
const cleanRelative = relativePath.replace(/^[\/\\]/, '')
|
|
253
266
|
|
|
254
|
-
|
|
255
|
-
|
|
267
|
+
// New structure: YYYY-MM-DD/[subdir/]HH-mm-ss_filename.ext
|
|
268
|
+
// Old structure: YYYY-MM-DD_HH-mm-ss_filename.ext or timestamp_filename.ext
|
|
269
|
+
const pathParts = cleanRelative.split('/')
|
|
270
|
+
const filename = pathParts.pop()
|
|
271
|
+
|
|
272
|
+
// Check if first part is a date folder (YYYY-MM-DD)
|
|
273
|
+
const dateFolderRegex = /^\d{4}-\d{2}-\d{2}$/
|
|
274
|
+
let subdir = ''
|
|
275
|
+
|
|
276
|
+
if (pathParts.length > 0 && dateFolderRegex.test(pathParts[0])) {
|
|
277
|
+
// New format: remove date folder, keep remaining subdirs
|
|
278
|
+
pathParts.shift() // Remove date folder
|
|
279
|
+
subdir = pathParts.join('/')
|
|
280
|
+
} else {
|
|
281
|
+
// Old format: use dir as-is
|
|
282
|
+
subdir = pathParts.join('/')
|
|
283
|
+
}
|
|
256
284
|
|
|
257
|
-
|
|
258
|
-
const
|
|
285
|
+
// Strip time prefix from filename (HH-mm-ss_filename or old YYYY-MM-DD_HH-mm-ss_filename)
|
|
286
|
+
const timeOnlyRegex = /^\d{2}-\d{2}-\d{2}_/
|
|
287
|
+
const fullTimestampRegex = /^(\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}|\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)_/
|
|
288
|
+
let originalFilename = filename.replace(timeOnlyRegex, '').replace(fullTimestampRegex, '')
|
|
259
289
|
|
|
260
|
-
const finalLocalRelativePath =
|
|
290
|
+
const finalLocalRelativePath = subdir ? join(subdir, originalFilename) : originalFilename
|
|
261
291
|
const localPath = join(config.sourceDir, finalLocalRelativePath)
|
|
262
292
|
|
|
263
293
|
await mkdir(dirname(localPath), { recursive: true })
|
|
@@ -320,6 +350,13 @@ export const r2Backup = initialConfig => app => {
|
|
|
320
350
|
const session = getSession(sessionToken)
|
|
321
351
|
|
|
322
352
|
if (!session) {
|
|
353
|
+
// Return JSON error for API routes
|
|
354
|
+
if (path.startsWith('/backup/api/')) {
|
|
355
|
+
context.set.status = 401
|
|
356
|
+
return { status: 'error', message: 'Session expired. Please login again.' }
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Redirect to login for page routes
|
|
323
360
|
context.set.status = 302
|
|
324
361
|
context.set.headers['Location'] = '/backup/login'
|
|
325
362
|
return new Response('Redirecting to login', {
|
|
@@ -17,7 +17,7 @@ export const createBackupService = getConfig => {
|
|
|
17
17
|
* Upload a single file to S3
|
|
18
18
|
* @param {string} filePath - Local file path
|
|
19
19
|
* @param {string} rootDir - Root directory for relative path calculation
|
|
20
|
-
* @param {string} timestampPrefix - Timestamp to prepend to filename
|
|
20
|
+
* @param {string} timestampPrefix - Timestamp to prepend to filename (YYYY-MM-DD_HH-mm-ss)
|
|
21
21
|
*/
|
|
22
22
|
const uploadFile = async (filePath, rootDir, timestampPrefix) => {
|
|
23
23
|
const config = getConfig()
|
|
@@ -28,15 +28,25 @@ export const createBackupService = getConfig => {
|
|
|
28
28
|
const dir = dirname(relativePath)
|
|
29
29
|
const filename = relativePath.split('/').pop()
|
|
30
30
|
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
// Parse timestamp to extract date and time parts
|
|
32
|
+
// Expected format: YYYY-MM-DD_HH-mm-ss
|
|
33
|
+
const timestamp = timestampPrefix || new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
|
|
34
|
+
const datePart = timestamp.slice(0, 10) // YYYY-MM-DD
|
|
35
|
+
const timePart = timestamp.slice(11) || '00-00-00' // HH-mm-ss
|
|
36
|
+
|
|
37
|
+
// Format: YYYY-MM-DD/HH-mm-ss_filename.ext
|
|
38
|
+
const newFilename = `${timePart}_${filename}`
|
|
39
|
+
const dateFolder = datePart
|
|
40
|
+
|
|
41
|
+
// Reconstruct path: prefix/date/[subdir/]time_filename
|
|
42
|
+
let finalPath
|
|
43
|
+
if (dir === '.') {
|
|
44
|
+
finalPath = join(dateFolder, newFilename)
|
|
45
|
+
} else {
|
|
46
|
+
finalPath = join(dateFolder, dir, newFilename)
|
|
47
|
+
}
|
|
37
48
|
const key = config.prefix ? join(config.prefix, finalPath) : finalPath
|
|
38
49
|
|
|
39
|
-
console.log(`Uploading ${key}...`)
|
|
40
50
|
await s3.write(key, fileContent)
|
|
41
51
|
}
|
|
42
52
|
|
|
@@ -132,15 +142,30 @@ export const createBackupService = getConfig => {
|
|
|
132
142
|
const relativePath = config.prefix ? key.replace(config.prefix, '') : key
|
|
133
143
|
const cleanRelative = relativePath.replace(/^[\/\\]/, '')
|
|
134
144
|
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
const
|
|
145
|
+
// New structure: YYYY-MM-DD/[subdir/]HH-mm-ss_filename.ext
|
|
146
|
+
// Old structure: YYYY-MM-DD_HH-mm-ss_filename.ext or timestamp_filename.ext
|
|
147
|
+
const pathParts = cleanRelative.split('/')
|
|
148
|
+
const filename = pathParts.pop()
|
|
149
|
+
|
|
150
|
+
// Check if first part is a date folder (YYYY-MM-DD)
|
|
151
|
+
const dateFolderRegex = /^\d{4}-\d{2}-\d{2}$/
|
|
152
|
+
let subdir = ''
|
|
153
|
+
|
|
154
|
+
if (pathParts.length > 0 && dateFolderRegex.test(pathParts[0])) {
|
|
155
|
+
// New format: remove date folder, keep remaining subdirs
|
|
156
|
+
pathParts.shift() // Remove date folder
|
|
157
|
+
subdir = pathParts.join('/')
|
|
158
|
+
} else {
|
|
159
|
+
// Old format: use dir as-is
|
|
160
|
+
subdir = pathParts.join('/')
|
|
161
|
+
}
|
|
138
162
|
|
|
139
|
-
// Strip
|
|
140
|
-
const
|
|
141
|
-
const
|
|
163
|
+
// Strip time prefix from filename (HH-mm-ss_filename or old YYYY-MM-DD_HH-mm-ss_filename)
|
|
164
|
+
const timeOnlyRegex = /^\d{2}-\d{2}-\d{2}_/
|
|
165
|
+
const fullTimestampRegex = /^(\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}|\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)_/
|
|
166
|
+
let originalFilename = filename.replace(timeOnlyRegex, '').replace(fullTimestampRegex, '')
|
|
142
167
|
|
|
143
|
-
const finalLocalRelativePath =
|
|
168
|
+
const finalLocalRelativePath = subdir ? join(subdir, originalFilename) : originalFilename
|
|
144
169
|
const localPath = join(config.sourceDir, finalLocalRelativePath)
|
|
145
170
|
|
|
146
171
|
// Ensure directory exists
|
|
@@ -11,6 +11,7 @@ import { ActionArea } from './components/ActionArea.js'
|
|
|
11
11
|
import { FilesTab } from './components/FilesTab.js'
|
|
12
12
|
import { SettingsTab } from './components/SettingsTab.js'
|
|
13
13
|
import { SecuritySection } from './components/SecuritySection.js'
|
|
14
|
+
import { Footer } from './components/Footer.js'
|
|
14
15
|
import { backupAppScript } from './scripts/backupApp.js'
|
|
15
16
|
|
|
16
17
|
export const DashboardPage = ({ config, jobStatus, hasAuth }) => `
|
|
@@ -48,6 +49,9 @@ export const DashboardPage = ({ config, jobStatus, hasAuth }) => `
|
|
|
48
49
|
${SettingsTab()}
|
|
49
50
|
${SecuritySection()}
|
|
50
51
|
</div>
|
|
52
|
+
|
|
53
|
+
<!-- Footer -->
|
|
54
|
+
${Footer()}
|
|
51
55
|
</div>
|
|
52
56
|
|
|
53
57
|
${backupAppScript({ config, jobStatus })}
|
package/src/views/LoginPage.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { Head } from './components/Head.js'
|
|
8
8
|
import { LoginCard } from './components/LoginCard.js'
|
|
9
|
+
import { Footer } from './components/Footer.js'
|
|
9
10
|
import { loginAppScript } from './scripts/loginApp.js'
|
|
10
11
|
|
|
11
12
|
export const LoginPage = ({ totpEnabled = false }) => `
|
|
@@ -14,8 +15,14 @@ export const LoginPage = ({ totpEnabled = false }) => `
|
|
|
14
15
|
<head>
|
|
15
16
|
${Head({ title: 'Login - Backup Manager' })}
|
|
16
17
|
</head>
|
|
17
|
-
<body class="bg-gray-50 min-h-screen flex items-center justify-center p-6 antialiased">
|
|
18
|
-
|
|
18
|
+
<body class="bg-gray-50 min-h-screen flex flex-col items-center justify-center p-6 antialiased">
|
|
19
|
+
<div class="flex-1 flex items-center justify-center w-full">
|
|
20
|
+
${LoginCard({ totpEnabled })}
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="w-full max-w-md">
|
|
24
|
+
${Footer()}
|
|
25
|
+
</div>
|
|
19
26
|
|
|
20
27
|
${loginAppScript({ totpEnabled })}
|
|
21
28
|
</body>
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { Head } from './components/Head.js'
|
|
7
7
|
import { OnboardingCard } from './components/OnboardingCard.js'
|
|
8
|
+
import { Footer } from './components/Footer.js'
|
|
8
9
|
import { onboardingAppScript } from './scripts/onboardingApp.js'
|
|
9
10
|
|
|
10
11
|
export const OnboardingPage = ({ sourceDir }) => `
|
|
@@ -13,8 +14,14 @@ export const OnboardingPage = ({ sourceDir }) => `
|
|
|
13
14
|
<head>
|
|
14
15
|
${Head({ title: 'Setup - Backup Manager' })}
|
|
15
16
|
</head>
|
|
16
|
-
<body class="bg-gray-50 min-h-screen flex items-center justify-center p-6 antialiased">
|
|
17
|
-
|
|
17
|
+
<body class="bg-gray-50 min-h-screen flex flex-col items-center justify-center p-6 antialiased">
|
|
18
|
+
<div class="flex-1 flex items-center justify-center w-full">
|
|
19
|
+
${OnboardingCard({ sourceDir })}
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div class="w-full max-w-2xl">
|
|
23
|
+
${Footer()}
|
|
24
|
+
</div>
|
|
18
25
|
|
|
19
26
|
${onboardingAppScript({ sourceDir })}
|
|
20
27
|
</body>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Footer component with RiLiGar branding
|
|
3
|
+
* Shows open source project attribution
|
|
4
|
+
* @returns {string} HTML string
|
|
5
|
+
*/
|
|
6
|
+
export const Footer = () => `
|
|
7
|
+
<footer class="pt-4 text-center">
|
|
8
|
+
<p class="text-sm text-gray-500">
|
|
9
|
+
Open source project by
|
|
10
|
+
<a href="https://riligar.click/" target="_blank" rel="noopener noreferrer"
|
|
11
|
+
class="text-gray-700 hover:text-gray-900 font-medium transition-colors underline underline-offset-2 decoration-gray-300 hover:decoration-gray-600">
|
|
12
|
+
RiLiGar
|
|
13
|
+
</a>
|
|
14
|
+
</p>
|
|
15
|
+
<p class="text-xs text-gray-400 mt-1">
|
|
16
|
+
<a href="https://github.com/riligar/elysia-backup" target="_blank" rel="noopener noreferrer"
|
|
17
|
+
class="hover:text-gray-600 transition-colors inline-flex items-center gap-1">
|
|
18
|
+
<svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
19
|
+
<path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd" />
|
|
20
|
+
</svg>
|
|
21
|
+
View on GitHub
|
|
22
|
+
</a>
|
|
23
|
+
</p>
|
|
24
|
+
</footer>
|
|
25
|
+
`
|
|
@@ -201,6 +201,14 @@ export const backupAppScript = ({ config, jobStatus }) => `
|
|
|
201
201
|
headers: { 'Content-Type': 'application/json' },
|
|
202
202
|
body: JSON.stringify({ timestamp })
|
|
203
203
|
})
|
|
204
|
+
|
|
205
|
+
// Handle session expiration
|
|
206
|
+
if (res.status === 401) {
|
|
207
|
+
this.addLog('Session expired. Redirecting to login...', 'error')
|
|
208
|
+
setTimeout(() => window.location.href = '/backup/login', 1500)
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
|
|
204
212
|
const data = await res.json()
|
|
205
213
|
if (data.status === 'success') {
|
|
206
214
|
this.lastBackup = new Date().toLocaleString()
|
|
@@ -220,14 +228,31 @@ export const backupAppScript = ({ config, jobStatus }) => `
|
|
|
220
228
|
this.loadingFiles = true
|
|
221
229
|
try {
|
|
222
230
|
const res = await fetch('/backup/api/files')
|
|
231
|
+
|
|
232
|
+
// Handle session expiration
|
|
233
|
+
if (res.status === 401) {
|
|
234
|
+
this.addLog('Session expired. Redirecting to login...', 'error')
|
|
235
|
+
setTimeout(() => window.location.href = '/backup/login', 1500)
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
|
|
223
239
|
const data = await res.json()
|
|
240
|
+
|
|
241
|
+
// Check for error response
|
|
242
|
+
if (data.status === 'error') {
|
|
243
|
+
throw new Error(data.message)
|
|
244
|
+
}
|
|
245
|
+
|
|
224
246
|
if (data.files) {
|
|
225
247
|
const sortedFiles = data.files.sort((a, b) => b.key.localeCompare(a.key));
|
|
226
248
|
|
|
227
249
|
const groupsMap = {};
|
|
228
250
|
sortedFiles.forEach(file => {
|
|
229
|
-
|
|
230
|
-
|
|
251
|
+
// Try to extract date from folder path first (new format: YYYY-MM-DD/)
|
|
252
|
+
// Then fall back to extracting from filename (legacy format: YYYY-MM-DD_HH-mm-ss_filename)
|
|
253
|
+
const folderMatch = file.key.match(/(?:^|\\/)(\\d{4}-\\d{2}-\\d{2})\\//);
|
|
254
|
+
const filenameMatch = file.key.match(/(?:^|\\/)(\\d{4}-\\d{2}-\\d{2})[_T]/);
|
|
255
|
+
const dateKey = folderMatch ? folderMatch[1] : (filenameMatch ? filenameMatch[1] : 'Others');
|
|
231
256
|
|
|
232
257
|
if (!groupsMap[dateKey]) {
|
|
233
258
|
groupsMap[dateKey] = [];
|