@riligar/elysia-backup 1.4.0 → 1.6.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 +1 -1
- package/src/index.js +135 -2
- package/src/views/OnboardingPage.js +22 -0
- package/src/views/components/ActionArea.js +1 -1
- package/src/views/components/FilesTab.js +2 -2
- package/src/views/components/Head.js +13 -0
- package/src/views/components/Header.js +14 -12
- package/src/views/components/LoginCard.js +5 -7
- package/src/views/components/OnboardingCard.js +278 -0
- package/src/views/components/SecuritySection.js +3 -3
- package/src/views/components/SettingsTab.js +8 -8
- package/src/views/scripts/onboardingApp.js +149 -0
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -14,6 +14,7 @@ import { html } from '@elysiajs/html'
|
|
|
14
14
|
// Import page components
|
|
15
15
|
import { LoginPage } from './views/LoginPage.js'
|
|
16
16
|
import { DashboardPage } from './views/DashboardPage.js'
|
|
17
|
+
import { OnboardingPage } from './views/OnboardingPage.js'
|
|
17
18
|
|
|
18
19
|
// Session Management
|
|
19
20
|
const sessions = new Map()
|
|
@@ -93,6 +94,19 @@ export const r2Backup = initialConfig => app => {
|
|
|
93
94
|
let config = { ...initialConfig, ...savedConfig }
|
|
94
95
|
let backupJob = null
|
|
95
96
|
|
|
97
|
+
// Helper to check if config.json exists and has required fields
|
|
98
|
+
const hasValidConfig = () => {
|
|
99
|
+
if (!existsSync(configPath)) return false
|
|
100
|
+
try {
|
|
101
|
+
const content = readFileSync(configPath, 'utf-8')
|
|
102
|
+
const parsed = JSON.parse(content)
|
|
103
|
+
// Check minimum required fields for system to work
|
|
104
|
+
return !!(parsed.bucket && parsed.endpoint && parsed.accessKeyId && parsed.secretAccessKey && parsed.auth?.username && parsed.auth?.password)
|
|
105
|
+
} catch {
|
|
106
|
+
return false
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
96
110
|
const getS3Client = () => {
|
|
97
111
|
console.log('S3 Config:', {
|
|
98
112
|
bucket: config.bucket,
|
|
@@ -275,13 +289,27 @@ export const r2Backup = initialConfig => app => {
|
|
|
275
289
|
return app.use(html()).group('/backup', app => {
|
|
276
290
|
// Authentication Middleware
|
|
277
291
|
const authMiddleware = context => {
|
|
292
|
+
// Skip auth entirely if no valid config (needs onboarding)
|
|
293
|
+
if (!hasValidConfig()) {
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
|
|
278
297
|
if (!config.auth || !config.auth.username || !config.auth.password) {
|
|
279
298
|
return
|
|
280
299
|
}
|
|
281
300
|
|
|
282
301
|
const path = context.path
|
|
283
302
|
|
|
284
|
-
|
|
303
|
+
// Skip auth for login, logout, onboarding, and static assets
|
|
304
|
+
if (
|
|
305
|
+
path === '/backup/login' ||
|
|
306
|
+
path === '/backup/auth/login' ||
|
|
307
|
+
path === '/backup/auth/logout' ||
|
|
308
|
+
path === '/backup/onboarding' ||
|
|
309
|
+
path === '/backup/api/onboarding' ||
|
|
310
|
+
path === '/backup/favicon.ico' ||
|
|
311
|
+
path === '/backup/logo.png'
|
|
312
|
+
) {
|
|
285
313
|
return
|
|
286
314
|
}
|
|
287
315
|
|
|
@@ -601,8 +629,113 @@ export const r2Backup = initialConfig => app => {
|
|
|
601
629
|
}
|
|
602
630
|
)
|
|
603
631
|
|
|
632
|
+
// Static Assets: Favicon
|
|
633
|
+
.get('/favicon.ico', () => {
|
|
634
|
+
const faviconPath = new URL('./assets/favicon.ico', import.meta.url).pathname
|
|
635
|
+
const content = readFileSync(faviconPath)
|
|
636
|
+
return new Response(content, {
|
|
637
|
+
headers: { 'Content-Type': 'image/x-icon', 'Cache-Control': 'public, max-age=86400' },
|
|
638
|
+
})
|
|
639
|
+
})
|
|
640
|
+
|
|
641
|
+
// Static Assets: Logo
|
|
642
|
+
.get('/logo.png', () => {
|
|
643
|
+
const logoPath = new URL('./assets/logo.png', import.meta.url).pathname
|
|
644
|
+
const content = readFileSync(logoPath)
|
|
645
|
+
return new Response(content, {
|
|
646
|
+
headers: { 'Content-Type': 'image/png', 'Cache-Control': 'public, max-age=86400' },
|
|
647
|
+
})
|
|
648
|
+
})
|
|
649
|
+
|
|
650
|
+
// ONBOARDING: Setup Page
|
|
651
|
+
.get('/onboarding', ({ set }) => {
|
|
652
|
+
// If already configured, redirect to dashboard
|
|
653
|
+
if (hasValidConfig()) {
|
|
654
|
+
set.status = 302
|
|
655
|
+
set.headers['Location'] = '/backup'
|
|
656
|
+
return
|
|
657
|
+
}
|
|
658
|
+
return OnboardingPage({ sourceDir: config.sourceDir })
|
|
659
|
+
})
|
|
660
|
+
|
|
661
|
+
// ONBOARDING: Save Initial Config
|
|
662
|
+
.post(
|
|
663
|
+
'/api/onboarding',
|
|
664
|
+
async ({ body, set }) => {
|
|
665
|
+
// Don't allow if already configured
|
|
666
|
+
if (hasValidConfig()) {
|
|
667
|
+
set.status = 403
|
|
668
|
+
return { status: 'error', message: 'System is already configured' }
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const { endpoint, bucket, prefix, accessKeyId, secretAccessKey, extensions, cronSchedule, cronEnabled, username, password } = body
|
|
672
|
+
|
|
673
|
+
// Parse extensions
|
|
674
|
+
let parsedExtensions = []
|
|
675
|
+
if (extensions) {
|
|
676
|
+
parsedExtensions = extensions
|
|
677
|
+
.split(',')
|
|
678
|
+
.map(e => e.trim())
|
|
679
|
+
.filter(Boolean)
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Build initial config
|
|
683
|
+
const initialConfigData = {
|
|
684
|
+
endpoint,
|
|
685
|
+
bucket,
|
|
686
|
+
prefix: prefix || '',
|
|
687
|
+
accessKeyId,
|
|
688
|
+
secretAccessKey,
|
|
689
|
+
extensions: parsedExtensions,
|
|
690
|
+
cronSchedule: cronSchedule || '0 0 * * *',
|
|
691
|
+
cronEnabled: cronEnabled !== false,
|
|
692
|
+
auth: {
|
|
693
|
+
username,
|
|
694
|
+
password,
|
|
695
|
+
},
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
try {
|
|
699
|
+
await writeFile(configPath, JSON.stringify(initialConfigData, null, 2))
|
|
700
|
+
|
|
701
|
+
// Update runtime config
|
|
702
|
+
config = { ...config, ...initialConfigData }
|
|
703
|
+
|
|
704
|
+
// Setup cron if enabled
|
|
705
|
+
setupCron()
|
|
706
|
+
|
|
707
|
+
return { status: 'success', message: 'Configuration saved successfully' }
|
|
708
|
+
} catch (e) {
|
|
709
|
+
console.error('Failed to save onboarding config:', e)
|
|
710
|
+
set.status = 500
|
|
711
|
+
return { status: 'error', message: 'Failed to save configuration' }
|
|
712
|
+
}
|
|
713
|
+
},
|
|
714
|
+
{
|
|
715
|
+
body: t.Object({
|
|
716
|
+
endpoint: t.String(),
|
|
717
|
+
bucket: t.String(),
|
|
718
|
+
prefix: t.Optional(t.String()),
|
|
719
|
+
accessKeyId: t.String(),
|
|
720
|
+
secretAccessKey: t.String(),
|
|
721
|
+
extensions: t.Optional(t.String()),
|
|
722
|
+
cronSchedule: t.Optional(t.String()),
|
|
723
|
+
cronEnabled: t.Optional(t.Boolean()),
|
|
724
|
+
username: t.String(),
|
|
725
|
+
password: t.String(),
|
|
726
|
+
}),
|
|
727
|
+
}
|
|
728
|
+
)
|
|
729
|
+
|
|
604
730
|
// UI: Dashboard
|
|
605
|
-
.get('/', () => {
|
|
731
|
+
.get('/', ({ set }) => {
|
|
732
|
+
// Redirect to onboarding if no valid config
|
|
733
|
+
if (!hasValidConfig()) {
|
|
734
|
+
set.status = 302
|
|
735
|
+
set.headers['Location'] = '/backup/onboarding'
|
|
736
|
+
return
|
|
737
|
+
}
|
|
738
|
+
|
|
606
739
|
const jobStatus = getJobStatus()
|
|
607
740
|
const hasAuth = !!(config.auth && config.auth.username && config.auth.password)
|
|
608
741
|
return DashboardPage({ config, jobStatus, hasAuth })
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding page component
|
|
3
|
+
* First-run setup wizard for configuring the backup system
|
|
4
|
+
* @returns {string} HTML string
|
|
5
|
+
*/
|
|
6
|
+
import { Head } from './components/Head.js'
|
|
7
|
+
import { OnboardingCard } from './components/OnboardingCard.js'
|
|
8
|
+
import { onboardingAppScript } from './scripts/onboardingApp.js'
|
|
9
|
+
|
|
10
|
+
export const OnboardingPage = ({ sourceDir }) => `
|
|
11
|
+
<!DOCTYPE html>
|
|
12
|
+
<html lang="en">
|
|
13
|
+
<head>
|
|
14
|
+
${Head({ title: 'Setup - Backup Manager' })}
|
|
15
|
+
</head>
|
|
16
|
+
<body class="bg-gray-50 min-h-screen flex items-center justify-center p-6 antialiased">
|
|
17
|
+
${OnboardingCard({ sourceDir })}
|
|
18
|
+
|
|
19
|
+
${onboardingAppScript({ sourceDir })}
|
|
20
|
+
</body>
|
|
21
|
+
</html>
|
|
22
|
+
`
|
|
@@ -14,7 +14,7 @@ export const ActionArea = () => `
|
|
|
14
14
|
<button
|
|
15
15
|
@click="runBackup()"
|
|
16
16
|
:disabled="loading"
|
|
17
|
-
class="group relative inline-flex items-center justify-center px-8 py-3.5 text-base font-semibold text-white transition-all duration-200 bg-
|
|
17
|
+
class="group relative inline-flex items-center justify-center px-8 py-3.5 text-base font-semibold text-white transition-all duration-200 bg-primary-500 rounded-xl hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-70 disabled:cursor-not-allowed">
|
|
18
18
|
<span x-show="!loading" class="flex items-center gap-2">
|
|
19
19
|
<i data-lucide="play-circle" class="w-5 h-5"></i>
|
|
20
20
|
Start Backup Process
|
|
@@ -57,10 +57,10 @@ export const FilesTab = () => `
|
|
|
57
57
|
x-data="holdButton(() => restoreFile(file.key))"
|
|
58
58
|
@mousedown="start()" @touchstart.prevent="start()"
|
|
59
59
|
@mouseup="stop()" @mouseleave="stop()" @touchend="stop()"
|
|
60
|
-
class="relative overflow-hidden px-4 py-2 rounded-lg text-xs font-bold uppercase tracking-wider text-
|
|
60
|
+
class="relative overflow-hidden px-4 py-2 rounded-lg text-xs font-bold uppercase tracking-wider text-primary-600 bg-primary-50 hover:bg-primary-100 transition-colors select-none"
|
|
61
61
|
title="Hold 3s to Restore"
|
|
62
62
|
>
|
|
63
|
-
<div class="absolute inset-0 bg-
|
|
63
|
+
<div class="absolute inset-0 bg-primary-200/50 origin-left transition-all duration-0 ease-linear" :style="'width: ' + progress + '%'"></div>
|
|
64
64
|
<span class="relative z-10 flex items-center gap-2">
|
|
65
65
|
<i data-lucide="rotate-ccw" class="w-3 h-3"></i>
|
|
66
66
|
<span x-text="progress > 0 ? 'Hold...' : 'Restore'"></span>
|
|
@@ -8,6 +8,7 @@ export const Head = ({ title = 'Backup Manager' }) => `
|
|
|
8
8
|
<meta charset="UTF-8">
|
|
9
9
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
10
10
|
<title>${title}</title>
|
|
11
|
+
<link rel="icon" type="image/x-icon" href="/backup/favicon.ico">
|
|
11
12
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
12
13
|
<script>
|
|
13
14
|
tailwind.config = {
|
|
@@ -17,6 +18,18 @@ export const Head = ({ title = 'Backup Manager' }) => `
|
|
|
17
18
|
sans: ['Montserrat', 'sans-serif'],
|
|
18
19
|
},
|
|
19
20
|
colors: {
|
|
21
|
+
primary: {
|
|
22
|
+
50: '#E8F4FD',
|
|
23
|
+
100: '#C5E3FA',
|
|
24
|
+
200: '#9DD0F6',
|
|
25
|
+
300: '#6DB9F1',
|
|
26
|
+
400: '#47A7ED',
|
|
27
|
+
500: '#209CEE',
|
|
28
|
+
600: '#1A8AD8',
|
|
29
|
+
700: '#1574B8',
|
|
30
|
+
800: '#115E95',
|
|
31
|
+
900: '#0C4A77',
|
|
32
|
+
},
|
|
20
33
|
gray: {
|
|
21
34
|
50: '#F9FAFB',
|
|
22
35
|
100: '#F3F4F6',
|
|
@@ -5,33 +5,35 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export const Header = ({ hasAuth }) => `
|
|
7
7
|
<div class="flex flex-col md:flex-row md:items-end justify-between mb-16 gap-6">
|
|
8
|
-
<div>
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
<div class="flex items-center gap-4">
|
|
9
|
+
<img src="/backup/logo.png" alt="Backup Manager" class="w-12 h-12 rounded-lg">
|
|
10
|
+
<div>
|
|
11
|
+
<h6 class="text-xs font-bold tracking-widest text-gray-500 uppercase mb-1 flex items-center gap-2">
|
|
12
|
+
System Administration
|
|
13
|
+
</h6>
|
|
14
|
+
<h1 class="text-3xl font-bold text-gray-900 tracking-tight">
|
|
15
|
+
Backup Manager
|
|
16
|
+
</h1>
|
|
17
|
+
</div>
|
|
16
18
|
</div>
|
|
17
19
|
|
|
18
20
|
<div class="flex items-center gap-4">
|
|
19
21
|
<!-- Tabs -->
|
|
20
|
-
<div class="flex p-1 bg-
|
|
22
|
+
<div class="flex p-1 bg-primary-100/50 rounded-xl">
|
|
21
23
|
<button @click="activeTab = 'dashboard'; $nextTick(() => lucide.createIcons())"
|
|
22
|
-
:class="activeTab === 'dashboard' ? 'bg-white text-
|
|
24
|
+
:class="activeTab === 'dashboard' ? 'bg-white text-primary-700 shadow-sm' : 'text-gray-500 hover:text-primary-600'"
|
|
23
25
|
class="px-6 py-2.5 rounded-lg text-sm font-semibold transition-all duration-200 flex items-center gap-2">
|
|
24
26
|
<i data-lucide="layout-dashboard" class="w-4 h-4"></i>
|
|
25
27
|
Overview
|
|
26
28
|
</button>
|
|
27
29
|
<button @click="activeTab = 'files'; fetchFiles()"
|
|
28
|
-
:class="activeTab === 'files' ? 'bg-white text-
|
|
30
|
+
:class="activeTab === 'files' ? 'bg-white text-primary-700 shadow-sm' : 'text-gray-500 hover:text-primary-600'"
|
|
29
31
|
class="px-6 py-2.5 rounded-lg text-sm font-semibold transition-all duration-200 flex items-center gap-2">
|
|
30
32
|
<i data-lucide="folder-open" class="w-4 h-4"></i>
|
|
31
33
|
Files & Restore
|
|
32
34
|
</button>
|
|
33
35
|
<button @click="activeTab = 'settings'; $nextTick(() => lucide.createIcons())"
|
|
34
|
-
:class="activeTab === 'settings' ? 'bg-white text-
|
|
36
|
+
:class="activeTab === 'settings' ? 'bg-white text-primary-700 shadow-sm' : 'text-gray-500 hover:text-primary-600'"
|
|
35
37
|
class="px-6 py-2.5 rounded-lg text-sm font-semibold transition-all duration-200 flex items-center gap-2">
|
|
36
38
|
<i data-lucide="settings" class="w-4 h-4"></i>
|
|
37
39
|
Settings
|
|
@@ -9,9 +9,7 @@ export const LoginCard = ({ totpEnabled }) => `
|
|
|
9
9
|
<div class="bg-white rounded-2xl border border-gray-100 shadow-[0_8px_30px_rgba(0,0,0,0.08)] overflow-hidden">
|
|
10
10
|
<!-- Header -->
|
|
11
11
|
<div class="p-10 text-center border-b border-gray-100">
|
|
12
|
-
<
|
|
13
|
-
<i data-lucide="shield-check" class="w-8 h-8 text-white"></i>
|
|
14
|
-
</div>
|
|
12
|
+
<img src="/backup/logo.png" alt="Backup Manager" class="w-20 h-20 mx-auto mb-4 rounded-xl">
|
|
15
13
|
<h1 class="text-2xl font-bold text-gray-900 mb-2">Backup Manager</h1>
|
|
16
14
|
<p class="text-sm text-gray-500">Access Control Panel</p>
|
|
17
15
|
</div>
|
|
@@ -38,7 +36,7 @@ export const LoginCard = ({ totpEnabled }) => `
|
|
|
38
36
|
type="text"
|
|
39
37
|
x-model="username"
|
|
40
38
|
required
|
|
41
|
-
class="w-full bg-gray-50 border border-gray-200 rounded-lg pl-12 pr-4 py-3 text-gray-900 focus:ring-2 focus:ring-
|
|
39
|
+
class="w-full bg-gray-50 border border-gray-200 rounded-lg pl-12 pr-4 py-3 text-gray-900 focus:ring-2 focus:ring-primary-500 focus:border-transparent outline-none transition-all font-medium"
|
|
42
40
|
placeholder="Enter your username"
|
|
43
41
|
autofocus
|
|
44
42
|
>
|
|
@@ -56,7 +54,7 @@ export const LoginCard = ({ totpEnabled }) => `
|
|
|
56
54
|
type="password"
|
|
57
55
|
x-model="password"
|
|
58
56
|
required
|
|
59
|
-
class="w-full bg-gray-50 border border-gray-200 rounded-lg pl-12 pr-4 py-3 text-gray-900 focus:ring-2 focus:ring-
|
|
57
|
+
class="w-full bg-gray-50 border border-gray-200 rounded-lg pl-12 pr-4 py-3 text-gray-900 focus:ring-2 focus:ring-primary-500 focus:border-transparent outline-none transition-all font-medium"
|
|
60
58
|
placeholder="Enter your password"
|
|
61
59
|
>
|
|
62
60
|
</div>
|
|
@@ -76,7 +74,7 @@ export const LoginCard = ({ totpEnabled }) => `
|
|
|
76
74
|
pattern="[0-9]*"
|
|
77
75
|
maxlength="6"
|
|
78
76
|
:required="totpEnabled"
|
|
79
|
-
class="w-full bg-gray-50 border border-gray-200 rounded-lg pl-12 pr-4 py-3 text-gray-900 focus:ring-2 focus:ring-
|
|
77
|
+
class="w-full bg-gray-50 border border-gray-200 rounded-lg pl-12 pr-4 py-3 text-gray-900 focus:ring-2 focus:ring-primary-500 focus:border-transparent outline-none transition-all font-medium tracking-widest text-center text-lg"
|
|
80
78
|
placeholder="000000"
|
|
81
79
|
>
|
|
82
80
|
</div>
|
|
@@ -90,7 +88,7 @@ export const LoginCard = ({ totpEnabled }) => `
|
|
|
90
88
|
<button
|
|
91
89
|
type="submit"
|
|
92
90
|
:disabled="loading"
|
|
93
|
-
class="w-full bg-
|
|
91
|
+
class="w-full bg-primary-500 hover:bg-primary-600 text-white font-bold py-3.5 px-6 rounded-xl transition-all shadow-lg hover:shadow-xl transform hover:-translate-y-0.5 flex items-center justify-center gap-2 disabled:opacity-70 disabled:cursor-not-allowed disabled:transform-none"
|
|
94
92
|
>
|
|
95
93
|
<span x-show="!loading" class="flex items-center gap-2">
|
|
96
94
|
<span>Sign In</span>
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding card component - Multi-step setup wizard
|
|
3
|
+
* @param {{ sourceDir: string }} props
|
|
4
|
+
* @returns {string} HTML string
|
|
5
|
+
*/
|
|
6
|
+
export const OnboardingCard = ({ sourceDir }) => `
|
|
7
|
+
<div class="w-full max-w-2xl" x-data="onboardingApp()">
|
|
8
|
+
<!-- Onboarding Card -->
|
|
9
|
+
<div class="bg-white rounded-2xl border border-gray-100 shadow-[0_8px_30px_rgba(0,0,0,0.08)] overflow-hidden">
|
|
10
|
+
<!-- Header -->
|
|
11
|
+
<div class="p-8 text-center border-b border-gray-100 bg-gradient-to-br from-primary-50 to-white">
|
|
12
|
+
<img src="/backup/logo.png" alt="Backup Manager" class="w-16 h-16 mx-auto mb-4 rounded-xl">
|
|
13
|
+
<h1 class="text-2xl font-bold text-gray-900 mb-2">Welcome to Backup Manager</h1>
|
|
14
|
+
<p class="text-sm text-gray-500">Let's configure your backup system in a few steps</p>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<!-- Progress Steps -->
|
|
18
|
+
<div class="px-8 py-4 bg-gray-50 border-b border-gray-100">
|
|
19
|
+
<div class="flex items-center justify-center gap-2">
|
|
20
|
+
<template x-for="(stepName, index) in ['Storage', 'Backup', 'Security']" :key="index">
|
|
21
|
+
<div class="flex items-center">
|
|
22
|
+
<div
|
|
23
|
+
:class="step > index + 1 ? 'bg-primary-500 text-white' : (step === index + 1 ? 'bg-primary-500 text-white ring-4 ring-primary-100' : 'bg-gray-200 text-gray-500')"
|
|
24
|
+
class="w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold transition-all"
|
|
25
|
+
>
|
|
26
|
+
<span x-show="step <= index + 1" x-text="index + 1"></span>
|
|
27
|
+
<i x-show="step > index + 1" data-lucide="check" class="w-4 h-4"></i>
|
|
28
|
+
</div>
|
|
29
|
+
<span
|
|
30
|
+
:class="step >= index + 1 ? 'text-gray-900 font-semibold' : 'text-gray-400'"
|
|
31
|
+
class="ml-2 text-sm hidden sm:inline"
|
|
32
|
+
x-text="stepName"
|
|
33
|
+
></span>
|
|
34
|
+
<div x-show="index < 2" class="w-8 h-0.5 mx-3 bg-gray-200 hidden sm:block">
|
|
35
|
+
<div
|
|
36
|
+
:class="step > index + 1 ? 'w-full' : 'w-0'"
|
|
37
|
+
class="h-full bg-primary-500 transition-all duration-300"
|
|
38
|
+
></div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</template>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<!-- Form -->
|
|
46
|
+
<div class="p-8">
|
|
47
|
+
<!-- Error Message -->
|
|
48
|
+
<div x-show="error" x-cloak class="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
|
|
49
|
+
<div class="flex items-center gap-3">
|
|
50
|
+
<i data-lucide="alert-circle" class="w-5 h-5 text-red-600"></i>
|
|
51
|
+
<span class="text-sm text-red-800 font-medium" x-text="error"></span>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<!-- Step 1: Storage Configuration -->
|
|
56
|
+
<div x-show="step === 1" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 translate-x-4" x-transition:enter-end="opacity-100 translate-x-0">
|
|
57
|
+
<h2 class="text-lg font-bold text-gray-900 mb-1 flex items-center gap-2">
|
|
58
|
+
<i data-lucide="cloud" class="w-5 h-5 text-primary-500"></i>
|
|
59
|
+
Storage Configuration
|
|
60
|
+
</h2>
|
|
61
|
+
<p class="text-sm text-gray-500 mb-6">Configure your R2/S3 compatible storage</p>
|
|
62
|
+
|
|
63
|
+
<div class="space-y-5">
|
|
64
|
+
<div>
|
|
65
|
+
<label class="block text-sm font-semibold text-gray-700 mb-2">Endpoint URL *</label>
|
|
66
|
+
<input
|
|
67
|
+
type="url"
|
|
68
|
+
x-model="form.endpoint"
|
|
69
|
+
placeholder="https://your-account.r2.cloudflarestorage.com"
|
|
70
|
+
class="w-full bg-gray-50 border border-gray-200 rounded-lg px-4 py-3 text-gray-900 focus:ring-2 focus:ring-primary-500 focus:border-transparent outline-none transition-all font-medium"
|
|
71
|
+
>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="grid grid-cols-2 gap-4">
|
|
74
|
+
<div>
|
|
75
|
+
<label class="block text-sm font-semibold text-gray-700 mb-2">Bucket Name *</label>
|
|
76
|
+
<input
|
|
77
|
+
type="text"
|
|
78
|
+
x-model="form.bucket"
|
|
79
|
+
placeholder="my-backups"
|
|
80
|
+
class="w-full bg-gray-50 border border-gray-200 rounded-lg px-4 py-3 text-gray-900 focus:ring-2 focus:ring-primary-500 focus:border-transparent outline-none transition-all font-medium"
|
|
81
|
+
>
|
|
82
|
+
</div>
|
|
83
|
+
<div>
|
|
84
|
+
<label class="block text-sm font-semibold text-gray-700 mb-2">Prefix (Folder)</label>
|
|
85
|
+
<input
|
|
86
|
+
type="text"
|
|
87
|
+
x-model="form.prefix"
|
|
88
|
+
placeholder="backups/"
|
|
89
|
+
class="w-full bg-gray-50 border border-gray-200 rounded-lg px-4 py-3 text-gray-900 focus:ring-2 focus:ring-primary-500 focus:border-transparent outline-none transition-all font-medium"
|
|
90
|
+
>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
<div class="grid grid-cols-2 gap-4">
|
|
94
|
+
<div>
|
|
95
|
+
<label class="block text-sm font-semibold text-gray-700 mb-2">Access Key ID *</label>
|
|
96
|
+
<input
|
|
97
|
+
type="text"
|
|
98
|
+
x-model="form.accessKeyId"
|
|
99
|
+
placeholder="Your access key"
|
|
100
|
+
class="w-full bg-gray-50 border border-gray-200 rounded-lg px-4 py-3 text-gray-900 focus:ring-2 focus:ring-primary-500 focus:border-transparent outline-none transition-all font-medium"
|
|
101
|
+
>
|
|
102
|
+
</div>
|
|
103
|
+
<div>
|
|
104
|
+
<label class="block text-sm font-semibold text-gray-700 mb-2">Secret Access Key *</label>
|
|
105
|
+
<input
|
|
106
|
+
type="password"
|
|
107
|
+
x-model="form.secretAccessKey"
|
|
108
|
+
placeholder="Your secret key"
|
|
109
|
+
class="w-full bg-gray-50 border border-gray-200 rounded-lg px-4 py-3 text-gray-900 focus:ring-2 focus:ring-primary-500 focus:border-transparent outline-none transition-all font-medium"
|
|
110
|
+
>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<!-- Step 2: Backup Configuration -->
|
|
117
|
+
<div x-show="step === 2" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 translate-x-4" x-transition:enter-end="opacity-100 translate-x-0">
|
|
118
|
+
<h2 class="text-lg font-bold text-gray-900 mb-1 flex items-center gap-2">
|
|
119
|
+
<i data-lucide="hard-drive" class="w-5 h-5 text-primary-500"></i>
|
|
120
|
+
Backup Configuration
|
|
121
|
+
</h2>
|
|
122
|
+
<p class="text-sm text-gray-500 mb-6">Configure backup source and schedule</p>
|
|
123
|
+
|
|
124
|
+
<div class="space-y-5">
|
|
125
|
+
<div>
|
|
126
|
+
<label class="block text-sm font-semibold text-gray-700 mb-2">Source Directory</label>
|
|
127
|
+
<input
|
|
128
|
+
type="text"
|
|
129
|
+
value="${sourceDir}"
|
|
130
|
+
disabled
|
|
131
|
+
class="w-full bg-gray-100 border border-gray-200 rounded-lg px-4 py-3 text-gray-500 cursor-not-allowed font-medium"
|
|
132
|
+
>
|
|
133
|
+
<p class="text-xs text-gray-400 mt-1">Defined in server configuration</p>
|
|
134
|
+
</div>
|
|
135
|
+
<div>
|
|
136
|
+
<label class="block text-sm font-semibold text-gray-700 mb-2">Allowed Extensions</label>
|
|
137
|
+
<input
|
|
138
|
+
type="text"
|
|
139
|
+
x-model="form.extensions"
|
|
140
|
+
placeholder=".db, .sqlite, .json (leave empty for all files)"
|
|
141
|
+
class="w-full bg-gray-50 border border-gray-200 rounded-lg px-4 py-3 text-gray-900 focus:ring-2 focus:ring-primary-500 focus:border-transparent outline-none transition-all font-medium"
|
|
142
|
+
>
|
|
143
|
+
<p class="text-xs text-gray-400 mt-1">Comma-separated list of file extensions to backup</p>
|
|
144
|
+
</div>
|
|
145
|
+
<div>
|
|
146
|
+
<label class="block text-sm font-semibold text-gray-700 mb-2">Cron Schedule</label>
|
|
147
|
+
<input
|
|
148
|
+
type="text"
|
|
149
|
+
x-model="form.cronSchedule"
|
|
150
|
+
placeholder="0 0 * * * (daily at midnight)"
|
|
151
|
+
class="w-full bg-gray-50 border border-gray-200 rounded-lg px-4 py-3 text-gray-900 focus:ring-2 focus:ring-primary-500 focus:border-transparent outline-none transition-all font-medium"
|
|
152
|
+
>
|
|
153
|
+
<p class="text-xs text-gray-400 mt-1">
|
|
154
|
+
Format: Minute Hour Day Month DayOfWeek.
|
|
155
|
+
<a href="https://crontab.guru/" target="_blank" class="text-primary-500 hover:underline">Help</a>
|
|
156
|
+
</p>
|
|
157
|
+
</div>
|
|
158
|
+
<div class="flex items-center gap-3 p-4 bg-gray-50 rounded-lg">
|
|
159
|
+
<input
|
|
160
|
+
type="checkbox"
|
|
161
|
+
x-model="form.cronEnabled"
|
|
162
|
+
id="cronEnabled"
|
|
163
|
+
class="w-5 h-5 text-primary-500 rounded border-gray-300 focus:ring-primary-500"
|
|
164
|
+
>
|
|
165
|
+
<label for="cronEnabled" class="text-sm font-medium text-gray-700">Enable scheduled backups</label>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<!-- Step 3: Security Configuration -->
|
|
171
|
+
<div x-show="step === 3" x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 translate-x-4" x-transition:enter-end="opacity-100 translate-x-0">
|
|
172
|
+
<h2 class="text-lg font-bold text-gray-900 mb-1 flex items-center gap-2">
|
|
173
|
+
<i data-lucide="shield" class="w-5 h-5 text-primary-500"></i>
|
|
174
|
+
Security Configuration
|
|
175
|
+
</h2>
|
|
176
|
+
<p class="text-sm text-gray-500 mb-6">Set up your admin credentials</p>
|
|
177
|
+
|
|
178
|
+
<div class="space-y-5">
|
|
179
|
+
<div>
|
|
180
|
+
<label class="block text-sm font-semibold text-gray-700 mb-2">Admin Username *</label>
|
|
181
|
+
<div class="relative">
|
|
182
|
+
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
|
183
|
+
<i data-lucide="user" class="w-5 h-5 text-gray-400"></i>
|
|
184
|
+
</div>
|
|
185
|
+
<input
|
|
186
|
+
type="text"
|
|
187
|
+
x-model="form.username"
|
|
188
|
+
placeholder="admin"
|
|
189
|
+
class="w-full bg-gray-50 border border-gray-200 rounded-lg pl-12 pr-4 py-3 text-gray-900 focus:ring-2 focus:ring-primary-500 focus:border-transparent outline-none transition-all font-medium"
|
|
190
|
+
>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
<div>
|
|
194
|
+
<label class="block text-sm font-semibold text-gray-700 mb-2">Password *</label>
|
|
195
|
+
<div class="relative">
|
|
196
|
+
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
|
197
|
+
<i data-lucide="lock" class="w-5 h-5 text-gray-400"></i>
|
|
198
|
+
</div>
|
|
199
|
+
<input
|
|
200
|
+
type="password"
|
|
201
|
+
x-model="form.password"
|
|
202
|
+
placeholder="Enter a strong password"
|
|
203
|
+
class="w-full bg-gray-50 border border-gray-200 rounded-lg pl-12 pr-4 py-3 text-gray-900 focus:ring-2 focus:ring-primary-500 focus:border-transparent outline-none transition-all font-medium"
|
|
204
|
+
>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
<div>
|
|
208
|
+
<label class="block text-sm font-semibold text-gray-700 mb-2">Confirm Password *</label>
|
|
209
|
+
<div class="relative">
|
|
210
|
+
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
|
211
|
+
<i data-lucide="lock" class="w-5 h-5 text-gray-400"></i>
|
|
212
|
+
</div>
|
|
213
|
+
<input
|
|
214
|
+
type="password"
|
|
215
|
+
x-model="form.confirmPassword"
|
|
216
|
+
placeholder="Confirm your password"
|
|
217
|
+
class="w-full bg-gray-50 border border-gray-200 rounded-lg pl-12 pr-4 py-3 text-gray-900 focus:ring-2 focus:ring-primary-500 focus:border-transparent outline-none transition-all font-medium"
|
|
218
|
+
:class="form.confirmPassword && form.password !== form.confirmPassword ? 'border-red-300 focus:ring-red-500' : ''"
|
|
219
|
+
>
|
|
220
|
+
</div>
|
|
221
|
+
<p x-show="form.confirmPassword && form.password !== form.confirmPassword" class="text-xs text-red-500 mt-1">
|
|
222
|
+
Passwords do not match
|
|
223
|
+
</p>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<div class="p-4 bg-primary-50 rounded-lg border border-primary-100">
|
|
227
|
+
<div class="flex items-start gap-3">
|
|
228
|
+
<i data-lucide="info" class="w-5 h-5 text-primary-600 mt-0.5"></i>
|
|
229
|
+
<div class="text-sm text-primary-800">
|
|
230
|
+
<p class="font-medium mb-1">Two-Factor Authentication</p>
|
|
231
|
+
<p class="text-primary-700">You can enable 2FA later in the Security settings after completing the setup.</p>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
<!-- Navigation Buttons -->
|
|
239
|
+
<div class="flex justify-between mt-8 pt-6 border-t border-gray-100">
|
|
240
|
+
<button
|
|
241
|
+
x-show="step > 1"
|
|
242
|
+
@click="prevStep()"
|
|
243
|
+
class="px-6 py-3 text-gray-600 hover:text-gray-900 font-semibold transition-colors flex items-center gap-2"
|
|
244
|
+
>
|
|
245
|
+
<i data-lucide="arrow-left" class="w-4 h-4"></i>
|
|
246
|
+
Back
|
|
247
|
+
</button>
|
|
248
|
+
<div x-show="step === 1"></div>
|
|
249
|
+
|
|
250
|
+
<button
|
|
251
|
+
x-show="step < 3"
|
|
252
|
+
@click="nextStep()"
|
|
253
|
+
class="px-8 py-3 bg-primary-500 hover:bg-primary-600 text-white font-bold rounded-xl transition-all shadow-lg hover:shadow-xl flex items-center gap-2"
|
|
254
|
+
>
|
|
255
|
+
Continue
|
|
256
|
+
<i data-lucide="arrow-right" class="w-4 h-4"></i>
|
|
257
|
+
</button>
|
|
258
|
+
|
|
259
|
+
<button
|
|
260
|
+
x-show="step === 3"
|
|
261
|
+
@click="submitSetup()"
|
|
262
|
+
:disabled="loading || !canSubmit()"
|
|
263
|
+
class="px-8 py-3 bg-primary-500 hover:bg-primary-600 text-white font-bold rounded-xl transition-all shadow-lg hover:shadow-xl flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
264
|
+
>
|
|
265
|
+
<span x-show="!loading" class="flex items-center gap-2">
|
|
266
|
+
<i data-lucide="check-circle" class="w-5 h-5"></i>
|
|
267
|
+
Complete Setup
|
|
268
|
+
</span>
|
|
269
|
+
<span x-show="loading" class="flex items-center gap-2">
|
|
270
|
+
<i data-lucide="loader-2" class="w-5 h-5 animate-spin"></i>
|
|
271
|
+
Saving...
|
|
272
|
+
</span>
|
|
273
|
+
</button>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
`
|
|
@@ -24,7 +24,7 @@ export const SecuritySection = () => `
|
|
|
24
24
|
<div x-show="!totpEnabled && !showTotpSetup">
|
|
25
25
|
<button
|
|
26
26
|
@click="generateTotp()"
|
|
27
|
-
class="bg-
|
|
27
|
+
class="bg-primary-500 hover:bg-primary-600 text-white font-bold py-2.5 px-5 rounded-lg transition-all flex items-center gap-2"
|
|
28
28
|
>
|
|
29
29
|
<i data-lucide="plus" class="w-4 h-4"></i>
|
|
30
30
|
Enable 2FA
|
|
@@ -73,7 +73,7 @@ export const SecuritySection = () => `
|
|
|
73
73
|
pattern="[0-9]*"
|
|
74
74
|
maxlength="6"
|
|
75
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-
|
|
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-primary-500 focus:border-transparent outline-none transition-all font-medium tracking-widest text-center text-lg"
|
|
77
77
|
>
|
|
78
78
|
<button
|
|
79
79
|
@click="verifyTotp()"
|
|
@@ -131,7 +131,7 @@ export const SecuritySection = () => `
|
|
|
131
131
|
pattern="[0-9]*"
|
|
132
132
|
maxlength="6"
|
|
133
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-
|
|
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-primary-500 focus:border-transparent outline-none transition-all font-medium tracking-widest text-center text-lg"
|
|
135
135
|
>
|
|
136
136
|
<button
|
|
137
137
|
@click="disableTotp()"
|
|
@@ -12,16 +12,16 @@ export const SettingsTab = () => `
|
|
|
12
12
|
<div class="space-y-6">
|
|
13
13
|
<div>
|
|
14
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-
|
|
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-primary-500 focus:border-transparent outline-none transition-all font-medium">
|
|
16
16
|
</div>
|
|
17
17
|
<div class="grid grid-cols-2 gap-6">
|
|
18
18
|
<div>
|
|
19
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-
|
|
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-primary-500 focus:border-transparent outline-none transition-all font-medium">
|
|
21
21
|
</div>
|
|
22
22
|
<div>
|
|
23
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-
|
|
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-primary-500 focus:border-transparent outline-none transition-all font-medium">
|
|
25
25
|
</div>
|
|
26
26
|
</div>
|
|
27
27
|
<div>
|
|
@@ -31,13 +31,13 @@ export const SettingsTab = () => `
|
|
|
31
31
|
</div>
|
|
32
32
|
<div>
|
|
33
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-
|
|
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-primary-500 focus:border-transparent outline-none transition-all font-medium">
|
|
35
35
|
</div>
|
|
36
36
|
<div>
|
|
37
37
|
<label class="block text-sm font-semibold text-gray-700 mb-2">Cron Schedule</label>
|
|
38
38
|
<div class="flex gap-4 items-center">
|
|
39
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-
|
|
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-primary-500 focus:border-transparent outline-none transition-all font-medium">
|
|
41
41
|
<div class="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-gray-500">
|
|
42
42
|
<a href="https://crontab.guru/" target="_blank" class="underline hover:text-gray-800">Help</a>
|
|
43
43
|
</div>
|
|
@@ -68,17 +68,17 @@ export const SettingsTab = () => `
|
|
|
68
68
|
<div class="grid grid-cols-2 gap-6">
|
|
69
69
|
<div>
|
|
70
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-
|
|
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-primary-500 focus:border-transparent outline-none transition-all font-medium">
|
|
72
72
|
</div>
|
|
73
73
|
<div>
|
|
74
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-
|
|
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-primary-500 focus:border-transparent outline-none transition-all font-medium">
|
|
76
76
|
</div>
|
|
77
77
|
</div>
|
|
78
78
|
</div>
|
|
79
79
|
|
|
80
80
|
<div class="pt-4 flex justify-end">
|
|
81
|
-
<button type="submit" class="bg-
|
|
81
|
+
<button type="submit" class="bg-primary-500 hover:bg-primary-600 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
82
|
<i data-lucide="save" class="w-4 h-4"></i>
|
|
83
83
|
Save Changes
|
|
84
84
|
</button>
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding Alpine.js application script
|
|
3
|
+
* Handles multi-step form navigation and submission
|
|
4
|
+
* @param {{ sourceDir: string }} props
|
|
5
|
+
* @returns {string} Script tag with onboarding logic
|
|
6
|
+
*/
|
|
7
|
+
export const onboardingAppScript = ({ sourceDir }) => `
|
|
8
|
+
<script>
|
|
9
|
+
document.addEventListener('alpine:init', () => {
|
|
10
|
+
Alpine.data('onboardingApp', () => ({
|
|
11
|
+
step: 1,
|
|
12
|
+
loading: false,
|
|
13
|
+
error: '',
|
|
14
|
+
form: {
|
|
15
|
+
// Storage
|
|
16
|
+
endpoint: '',
|
|
17
|
+
bucket: '',
|
|
18
|
+
prefix: '',
|
|
19
|
+
accessKeyId: '',
|
|
20
|
+
secretAccessKey: '',
|
|
21
|
+
// Backup
|
|
22
|
+
extensions: '',
|
|
23
|
+
cronSchedule: '0 0 * * *',
|
|
24
|
+
cronEnabled: true,
|
|
25
|
+
// Security
|
|
26
|
+
username: '',
|
|
27
|
+
password: '',
|
|
28
|
+
confirmPassword: ''
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
init() {
|
|
32
|
+
this.$nextTick(() => lucide.createIcons());
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
validateStep1() {
|
|
36
|
+
if (!this.form.endpoint) {
|
|
37
|
+
this.error = 'Endpoint URL is required';
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
if (!this.form.bucket) {
|
|
41
|
+
this.error = 'Bucket name is required';
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
if (!this.form.accessKeyId) {
|
|
45
|
+
this.error = 'Access Key ID is required';
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
if (!this.form.secretAccessKey) {
|
|
49
|
+
this.error = 'Secret Access Key is required';
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
validateStep2() {
|
|
56
|
+
// Step 2 has no required fields
|
|
57
|
+
return true;
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
validateStep3() {
|
|
61
|
+
if (!this.form.username) {
|
|
62
|
+
this.error = 'Username is required';
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
if (!this.form.password) {
|
|
66
|
+
this.error = 'Password is required';
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
if (this.form.password.length < 8) {
|
|
70
|
+
this.error = 'Password must be at least 8 characters';
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
if (this.form.password !== this.form.confirmPassword) {
|
|
74
|
+
this.error = 'Passwords do not match';
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
nextStep() {
|
|
81
|
+
this.error = '';
|
|
82
|
+
|
|
83
|
+
if (this.step === 1 && !this.validateStep1()) return;
|
|
84
|
+
if (this.step === 2 && !this.validateStep2()) return;
|
|
85
|
+
|
|
86
|
+
this.step++;
|
|
87
|
+
this.$nextTick(() => lucide.createIcons());
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
prevStep() {
|
|
91
|
+
this.error = '';
|
|
92
|
+
this.step--;
|
|
93
|
+
this.$nextTick(() => lucide.createIcons());
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
canSubmit() {
|
|
97
|
+
return this.form.username &&
|
|
98
|
+
this.form.password &&
|
|
99
|
+
this.form.password === this.form.confirmPassword &&
|
|
100
|
+
this.form.password.length >= 8;
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
async submitSetup() {
|
|
104
|
+
this.error = '';
|
|
105
|
+
|
|
106
|
+
if (!this.validateStep3()) return;
|
|
107
|
+
|
|
108
|
+
this.loading = true;
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const response = await fetch('/backup/api/onboarding', {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
headers: { 'Content-Type': 'application/json' },
|
|
114
|
+
body: JSON.stringify({
|
|
115
|
+
endpoint: this.form.endpoint,
|
|
116
|
+
bucket: this.form.bucket,
|
|
117
|
+
prefix: this.form.prefix,
|
|
118
|
+
accessKeyId: this.form.accessKeyId,
|
|
119
|
+
secretAccessKey: this.form.secretAccessKey,
|
|
120
|
+
extensions: this.form.extensions,
|
|
121
|
+
cronSchedule: this.form.cronSchedule,
|
|
122
|
+
cronEnabled: this.form.cronEnabled,
|
|
123
|
+
username: this.form.username,
|
|
124
|
+
password: this.form.password
|
|
125
|
+
})
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const data = await response.json();
|
|
129
|
+
|
|
130
|
+
if (data.status === 'success') {
|
|
131
|
+
window.location.href = '/backup';
|
|
132
|
+
} else {
|
|
133
|
+
this.error = data.message || 'Setup failed. Please try again.';
|
|
134
|
+
}
|
|
135
|
+
} catch (e) {
|
|
136
|
+
this.error = 'Connection error. Please try again.';
|
|
137
|
+
} finally {
|
|
138
|
+
this.loading = false;
|
|
139
|
+
this.$nextTick(() => lucide.createIcons());
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}));
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
146
|
+
lucide.createIcons();
|
|
147
|
+
});
|
|
148
|
+
</script>
|
|
149
|
+
`
|