Skip to content

Commit 0a5bbb5

Browse files
committed
feat: add multi-step onboarding wizard for initial setup and remove static configuration files.
1 parent 6b69a18 commit 0a5bbb5

File tree

11 files changed

+574
-56
lines changed

11 files changed

+574
-56
lines changed

.DS_Store

-8 KB
Binary file not shown.

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,4 +138,5 @@ dist
138138
vite.config.js.timestamp-*
139139
vite.config.ts.timestamp-*
140140

141-
bun.lock
141+
bun.lock
142+
.DS_Store

config.json

Lines changed: 0 additions & 18 deletions
This file was deleted.

demo/.env.example

Lines changed: 0 additions & 13 deletions
This file was deleted.

demo/config.json

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
{
2+
"endpoint": "https://fb81cb0efe496349aa882e65d6704c6f.r2.cloudflarestorage.com",
23
"bucket": "2025-12-20",
4+
"prefix": "",
35
"accessKeyId": "135b46a7ebe62a798198865c64eee178",
46
"secretAccessKey": "3ae633898d81a7aeb0ce1179a92095ab836bb3a60f601cdd43d4e2b6b6152646",
5-
"endpoint": "https://fb81cb0efe496349aa882e65d6704c6f.r2.cloudflarestorage.com",
6-
"sourceDir": "./data",
7-
"prefix": "",
87
"extensions": [
98
".json"
109
],
11-
"cronSchedule": "0 * * * *",
12-
"cronEnabled": false,
10+
"cronSchedule": "0 0 * * *",
11+
"cronEnabled": true,
1312
"auth": {
1413
"username": "admin",
15-
"password": "admin123",
16-
"totpSecret": "DQMRAQL6ANCSI2BJ"
14+
"password": "q1w2e3r4t5"
1715
}
1816
}

demo/index.js

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,8 @@ const write = (file, data) => writeFileSync(file, JSON.stringify(data, null, 2))
2020
const app = new Elysia()
2121
.use(
2222
r2Backup({
23-
bucket: process.env.R2_BUCKET || 'demo-bucket',
24-
accessKeyId: process.env.R2_ACCESS_KEY_ID || '',
25-
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY || '',
26-
endpoint: process.env.R2_ENDPOINT || 'https://example.r2.cloudflarestorage.com',
2723
sourceDir: './data',
28-
prefix: 'backups/',
29-
extensions: ['.json'],
30-
cronSchedule: '0 * * * *',
31-
cronEnabled: false,
32-
// Authentication - provide username and password to enable auth
33-
auth: {
34-
username: process.env.BACKUP_USERNAME,
35-
password: process.env.BACKUP_PASSWORD,
36-
totpSecret: process.env.BACKUP_TOTP_SECRET, // Optional: base32 secret for 2FA
37-
},
24+
configPath: './config.json',
3825
})
3926
)
4027
.get(

src/.DS_Store

2 KB
Binary file not shown.

src/index.js

Lines changed: 117 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { html } from '@elysiajs/html'
1414
// Import page components
1515
import { LoginPage } from './views/LoginPage.js'
1616
import { DashboardPage } from './views/DashboardPage.js'
17+
import { OnboardingPage } from './views/OnboardingPage.js'
1718

1819
// Session Management
1920
const sessions = new Map()
@@ -93,6 +94,19 @@ export const r2Backup = initialConfig => app => {
9394
let config = { ...initialConfig, ...savedConfig }
9495
let backupJob = null
9596

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+
96110
const getS3Client = () => {
97111
console.log('S3 Config:', {
98112
bucket: config.bucket,
@@ -275,14 +289,27 @@ export const r2Backup = initialConfig => app => {
275289
return app.use(html()).group('/backup', app => {
276290
// Authentication Middleware
277291
const authMiddleware = context => {
292+
// Skip auth entirely if no valid config (needs onboarding)
293+
if (!hasValidConfig()) {
294+
return
295+
}
296+
278297
if (!config.auth || !config.auth.username || !config.auth.password) {
279298
return
280299
}
281300

282301
const path = context.path
283302

284-
// Skip auth for login, logout, and static assets
285-
if (path === '/backup/login' || path === '/backup/auth/login' || path === '/backup/auth/logout' || path === '/backup/favicon.ico' || path === '/backup/logo.png') {
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+
) {
286313
return
287314
}
288315

@@ -620,8 +647,95 @@ export const r2Backup = initialConfig => app => {
620647
})
621648
})
622649

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+
623730
// UI: Dashboard
624-
.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+
625739
const jobStatus = getJobStatus()
626740
const hasAuth = !!(config.auth && config.auth.username && config.auth.password)
627741
return DashboardPage({ config, jobStatus, hasAuth })

src/views/OnboardingPage.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
`

0 commit comments

Comments
 (0)