Skip to content

Commit 7f3315d

Browse files
committed
refactor: modularize backup service by extracting S3 client, core logic, scheduling, authentication, and UI into separate modules.
1 parent 9be03f4 commit 7f3315d

File tree

10 files changed

+1365
-903
lines changed

10 files changed

+1365
-903
lines changed

jsconfig.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"compilerOptions": {
3+
"jsx": "react",
4+
"jsxFactory": "Html.createElement",
5+
"jsxFragmentFactory": "Html.Fragment"
6+
}
7+
}

src/core/config.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { readFileSync, existsSync } from 'node:fs'
2+
import { writeFile } from 'node:fs/promises'
3+
4+
/**
5+
* Load configuration from a JSON file
6+
* @param {string} configPath - Path to the config file
7+
* @returns {Object} Loaded configuration or empty object
8+
*/
9+
export const loadConfig = configPath => {
10+
if (!existsSync(configPath)) {
11+
return {}
12+
}
13+
14+
try {
15+
const fileContent = readFileSync(configPath, 'utf-8')
16+
const config = JSON.parse(fileContent)
17+
console.log('Loaded backup config from', configPath)
18+
return config
19+
} catch (e) {
20+
console.error('Failed to load backup config:', e)
21+
return {}
22+
}
23+
}
24+
25+
/**
26+
* Save configuration to a JSON file
27+
* @param {string} configPath - Path to save the config
28+
* @param {Object} config - Configuration object to save
29+
*/
30+
export const saveConfig = async (configPath, config) => {
31+
try {
32+
await writeFile(configPath, JSON.stringify(config, null, 2))
33+
return true
34+
} catch (e) {
35+
console.error('Failed to save config:', e)
36+
return false
37+
}
38+
}
39+
40+
/**
41+
* Create a configuration manager
42+
* @param {Object} initialConfig - Initial configuration from code
43+
* @returns {Object} Configuration manager with get/set/save methods
44+
*/
45+
export const createConfigManager = initialConfig => {
46+
const configPath = initialConfig.configPath || './config.json'
47+
const savedConfig = loadConfig(configPath)
48+
49+
let config = { ...initialConfig, ...savedConfig }
50+
51+
return {
52+
get: () => config,
53+
getPath: () => configPath,
54+
55+
set: newConfig => {
56+
config = { ...config, ...newConfig }
57+
return config
58+
},
59+
60+
save: async () => {
61+
return saveConfig(configPath, config)
62+
},
63+
64+
update: async newConfig => {
65+
config = { ...config, ...newConfig }
66+
await saveConfig(configPath, config)
67+
return config
68+
},
69+
}
70+
}

src/core/scheduler.js

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { CronJob } from 'cron'
2+
3+
/**
4+
* Create a backup scheduler
5+
* @param {Function} onBackup - Callback to execute on scheduled backup
6+
* @returns {Object} Scheduler with setup/stop/getStatus methods
7+
*/
8+
export const createScheduler = onBackup => {
9+
let backupJob = null
10+
11+
return {
12+
/**
13+
* Setup or restart the cron scheduler
14+
* @param {string} cronSchedule - Cron expression
15+
* @param {boolean} enabled - Whether scheduling is enabled
16+
*/
17+
setup: (cronSchedule, enabled = true) => {
18+
// Stop existing job if any
19+
if (backupJob) {
20+
backupJob.stop()
21+
backupJob = null
22+
}
23+
24+
if (!cronSchedule || enabled === false) {
25+
return
26+
}
27+
28+
console.log(`Setting up backup cron: ${cronSchedule}`)
29+
30+
try {
31+
backupJob = new CronJob(
32+
cronSchedule,
33+
async () => {
34+
console.log('Running scheduled backup...')
35+
try {
36+
// Generate timestamp for the backup
37+
const now = new Date()
38+
const timestamp =
39+
now.getFullYear() +
40+
'-' +
41+
String(now.getMonth() + 1).padStart(2, '0') +
42+
'-' +
43+
String(now.getDate()).padStart(2, '0') +
44+
'_' +
45+
String(now.getHours()).padStart(2, '0') +
46+
'-' +
47+
String(now.getMinutes()).padStart(2, '0') +
48+
'-' +
49+
String(now.getSeconds()).padStart(2, '0')
50+
51+
await onBackup(timestamp)
52+
console.log('Scheduled backup completed')
53+
} catch (e) {
54+
console.error('Scheduled backup failed:', e)
55+
}
56+
},
57+
null,
58+
true // start immediately
59+
)
60+
} catch (e) {
61+
console.error('Invalid cron schedule:', e.message)
62+
}
63+
},
64+
65+
/**
66+
* Stop the scheduler
67+
*/
68+
stop: () => {
69+
if (backupJob) {
70+
backupJob.stop()
71+
backupJob = null
72+
}
73+
},
74+
75+
/**
76+
* Get current scheduler status
77+
* @param {boolean} cronEnabled - Whether cron is enabled in config
78+
* @returns {Object} Status with isRunning and nextRun
79+
*/
80+
getStatus: cronEnabled => {
81+
const isRunning = !!backupJob && cronEnabled !== false
82+
let nextRun = null
83+
84+
if (isRunning && backupJob) {
85+
try {
86+
const nextDate = backupJob.nextDate()
87+
if (nextDate) {
88+
// cron returns Luxon DateTime, convert to JS Date
89+
nextRun = nextDate.toJSDate().toISOString()
90+
}
91+
} catch (e) {
92+
console.error('Error getting next date', e)
93+
}
94+
}
95+
96+
return { isRunning, nextRun }
97+
},
98+
}
99+
}

src/core/session.js

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/**
2+
* Session Management Module
3+
* Handles secure session creation, validation, and cleanup
4+
*/
5+
6+
/**
7+
* Generate a secure random session token
8+
* @returns {string} 64-character hex token
9+
*/
10+
export const generateSessionToken = () => {
11+
const array = new Uint8Array(32)
12+
crypto.getRandomValues(array)
13+
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('')
14+
}
15+
16+
/**
17+
* Create a session manager instance
18+
* @returns {Object} Session manager with create/get/delete methods
19+
*/
20+
export const createSessionManager = () => {
21+
const sessions = new Map()
22+
23+
return {
24+
/**
25+
* Create a new session for a user
26+
* @param {string} username - Username for the session
27+
* @param {number} sessionDuration - Duration in milliseconds (default: 24h)
28+
* @returns {Object} Session token and expiration
29+
*/
30+
create: (username, sessionDuration = 24 * 60 * 60 * 1000) => {
31+
const token = generateSessionToken()
32+
const expiresAt = Date.now() + sessionDuration
33+
sessions.set(token, { username, expiresAt })
34+
return { token, expiresAt }
35+
},
36+
37+
/**
38+
* Get and validate a session by token
39+
* @param {string} token - Session token
40+
* @returns {Object|null} Session data or null if invalid/expired
41+
*/
42+
get: token => {
43+
if (!token) return null
44+
45+
const session = sessions.get(token)
46+
if (!session) return null
47+
48+
if (Date.now() > session.expiresAt) {
49+
sessions.delete(token)
50+
return null
51+
}
52+
53+
return session
54+
},
55+
56+
/**
57+
* Delete a session
58+
* @param {string} token - Session token to delete
59+
*/
60+
delete: token => {
61+
if (token) {
62+
sessions.delete(token)
63+
}
64+
},
65+
66+
/**
67+
* Get the number of active sessions
68+
* @returns {number} Count of active sessions
69+
*/
70+
count: () => sessions.size,
71+
72+
/**
73+
* Clean up expired sessions
74+
* @returns {number} Number of sessions cleaned
75+
*/
76+
cleanup: () => {
77+
const now = Date.now()
78+
let cleaned = 0
79+
80+
for (const [token, session] of sessions) {
81+
if (now > session.expiresAt) {
82+
sessions.delete(token)
83+
cleaned++
84+
}
85+
}
86+
87+
return cleaned
88+
},
89+
}
90+
}

0 commit comments

Comments
 (0)