diff --git a/backup.sh b/backup.sh
new file mode 100644
index 0000000..e1f8f87
--- /dev/null
+++ b/backup.sh
@@ -0,0 +1,63 @@
+#!/bin/bash
+
+# Configuration
+BACKUP_DIR="/volume1/backups/nas-tools"
+DATE=$(date +%Y%m%d_%H%M%S)
+RETENTION_DAYS=7
+
+# Ensure backup directory exists
+mkdir -p "$BACKUP_DIR"
+
+log() {
+ echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
+}
+
+log "Starting backup..."
+
+# 1. Database Dumps
+log "Dumping databases..."
+
+# Gitea DB
+docker exec gitea-db pg_dump -U gitea gitea > "$BACKUP_DIR/gitea_db_$DATE.sql"
+if [ $? -eq 0 ]; then
+ log "Gitea DB dumped successfully."
+else
+ log "Error dumping Gitea DB!"
+fi
+
+# Immich DB
+docker exec immich-db pg_dump -U postgres immich > "$BACKUP_DIR/immich_db_$DATE.sql"
+if [ $? -eq 0 ]; then
+ log "Immich DB dumped successfully."
+else
+ log "Error dumping Immich DB!"
+fi
+
+# 2. File Backups (Configs & Data)
+# Exclude heavy media files if handled separately, but for now backing up key configs
+log "Compressing configuration files..."
+
+# Dashboard & Gitea & Immich configs
+# Assuming /volume1/docker structure
+tar -czf "$BACKUP_DIR/configs_$DATE.tar.gz" \
+ -C /volume1/docker \
+ nas-dashboard/backend/config.py \
+ nas-dashboard/.env \
+ gitea/conf \
+ immich/.env \
+ --exclude='gitea/data' \
+ --exclude='immich/upload' \
+ --exclude='immich/thumbs'
+
+if [ $? -eq 0 ]; then
+ log "Configs compressed successfully."
+else
+ log "Error compressing configs!"
+fi
+
+# 3. Cleanup Old Backups
+log "Cleaning up backups older than $RETENTION_DAYS days..."
+find "$BACKUP_DIR" -type f -name "*_db_*.sql" -mtime +$RETENTION_DAYS -delete
+find "$BACKUP_DIR" -type f -name "configs_*.tar.gz" -mtime +$RETENTION_DAYS -delete
+
+log "Backup completed."
diff --git a/dashboard/backend/auth.py b/dashboard/backend/auth.py
index 6dc6b40..eef5d5e 100644
--- a/dashboard/backend/auth.py
+++ b/dashboard/backend/auth.py
@@ -60,27 +60,39 @@ async def get_current_user_ws(token: str):
raise credentials_exception
return username
-# TOTP Persistenceturn username
-
# TOTP Persistence
AUTH_FILE = config.VOLUME_ROOT + "/docker/nas-dashboard/auth.json"
+import json, os
-def load_totp_secret():
+def _load_auth_data():
try:
- import json
if os.path.exists(AUTH_FILE):
with open(AUTH_FILE, "r") as f:
- data = json.load(f)
- return data.get("totp_secret", "")
+ return json.load(f)
except Exception:
pass
- return config.TOTP_SECRET
+ return {}
-def save_totp_secret(secret: str):
- import json
+def _save_auth_data(data):
os.makedirs(os.path.dirname(AUTH_FILE), exist_ok=True)
with open(AUTH_FILE, "w") as f:
- json.dump({"totp_secret": secret}, f)
+ json.dump(data, f)
+
+def load_totp_secret():
+ return _load_auth_data().get("totp_secret", "") or config.TOTP_SECRET
+
+def save_totp_secret(secret: str):
+ data = _load_auth_data()
+ data["totp_secret"] = secret
+ _save_auth_data(data)
+
+def load_password_hash():
+ return _load_auth_data().get("password_hash", "") or config.ADMIN_PASSWORD_HASH
+
+def save_password_hash(hashed: str):
+ data = _load_auth_data()
+ data["password_hash"] = hashed
+ _save_auth_data(data)
def verify_totp(token: str, secret: str = None):
if secret is None:
diff --git a/dashboard/backend/routers/auth.py b/dashboard/backend/routers/auth.py
index e79043b..0a1d536 100644
--- a/dashboard/backend/routers/auth.py
+++ b/dashboard/backend/routers/auth.py
@@ -28,7 +28,7 @@ class Verify2FARequest(BaseModel):
@router.post("/login", response_model=Token)
async def login(creds: LoginRequest):
# Verify username/password
- if creds.username != config.ADMIN_USER or not auth.verify_password(creds.password, config.ADMIN_PASSWORD_HASH):
+ if creds.username != config.ADMIN_USER or not auth.verify_password(creds.password, auth.load_password_hash()):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
@@ -84,3 +84,16 @@ async def verify_2fa_setup(request: Verify2FARequest, current_user: str = Depend
async def disable_2fa(current_user: str = Depends(auth.get_current_user)):
auth.save_totp_secret("")
return {"message": "2FA disabled"}
+
+class ChangePasswordRequest(BaseModel):
+ current_password: str
+ new_password: str
+
+@router.post("/change-password")
+async def change_password(req: ChangePasswordRequest, current_user: str = Depends(auth.get_current_user)):
+ if not auth.verify_password(req.current_password, auth.load_password_hash()):
+ raise HTTPException(status_code=400, detail="Current password is incorrect")
+ if len(req.new_password) < 8:
+ raise HTTPException(status_code=400, detail="Password must be at least 8 characters")
+ auth.save_password_hash(auth.get_password_hash(req.new_password))
+ return {"message": "Password changed successfully"}
diff --git a/dashboard/backend/routers/terminal.py b/dashboard/backend/routers/terminal.py
index 8bf202f..c858198 100644
--- a/dashboard/backend/routers/terminal.py
+++ b/dashboard/backend/routers/terminal.py
@@ -16,7 +16,7 @@ async def ws_endpoint(websocket: WebSocket, user: str = Depends(auth.get_current
return
- await ws.accept()
+ await websocket.accept()
master_fd, slave_fd = pty.openpty()
pid = os.fork()
@@ -40,7 +40,7 @@ async def ws_endpoint(websocket: WebSocket, user: str = Depends(auth.get_current
data = os.read(master_fd, 4096)
if not data:
break
- await ws.send_bytes(data)
+ await websocket.send_bytes(data)
except (OSError, WebSocketDisconnect):
pass
@@ -53,7 +53,7 @@ async def ws_endpoint(websocket: WebSocket, user: str = Depends(auth.get_current
try:
while True:
- msg = await ws.receive()
+ msg = await websocket.receive()
if "bytes" in msg and msg["bytes"]:
data = msg["bytes"]
# Handle resize: first byte 0x01 means resize message
diff --git a/dashboard/frontend/src/App.svelte b/dashboard/frontend/src/App.svelte
index 67face2..d936f00 100644
--- a/dashboard/frontend/src/App.svelte
+++ b/dashboard/frontend/src/App.svelte
@@ -5,6 +5,7 @@
import Gitea from "./routes/Gitea.svelte";
import Files from "./routes/Files.svelte";
import Terminal from "./routes/Terminal.svelte";
+ import Settings from "./routes/Settings.svelte";
import Login from "./routes/Login.svelte";
import { onMount } from "svelte";
import { getToken, checkAuth, setToken } from "./lib/api.js";
@@ -82,6 +83,8 @@
Account and security settings
+