Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ BACKUP_DIR=/config/backups
TZ=UTC
DISK_PATH=/mnt/storage
DISK_PATH_2=/mnt/backup
USERS_FILE_PATH=/config/users.json
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,22 @@ these settings automatically using `python-dotenv`.
| `ENV_FILE_PATH` | `/config/.env` | Path to environment file |
| `BACKUP_DIR` | `/config/backups` | Backup directory for configurations |

### User Accounts
User credentials are stored in a simple JSON file. By default the application
loads `users.json` from the project root, but this can be overridden with the
`USERS_FILE_PATH` environment variable.

To add or update a user, edit `users.json` and provide a hashed password
generated with `werkzeug.security.generate_password_hash`. Example:

```json
{
"holly": "<hashed>"
}
```

After creating or updating the file, restart the application.

### Storage Configuration
The dashboard automatically discovers and monitors:
- **Data Drives**: USB drives and additional storage
Expand Down
2 changes: 2 additions & 0 deletions pihealth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ def create_app():
logging.basicConfig(level=getattr(logging, log_level, logging.INFO))

app = Flask(__name__, static_folder='static')
# Secret key for session management
app.secret_key = os.getenv("SECRET_KEY", "pihealth-secret-key")

# Initialize Docker client if available
try:
Expand Down
58 changes: 55 additions & 3 deletions pihealth/auth_routes.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,57 @@
from flask import Blueprint
"""Simple authentication routes for the dashboard."""

auth_bp = Blueprint('auth_bp', __name__)
import json
import os
from flask import Blueprint, request, session, jsonify
from werkzeug.security import check_password_hash


auth_bp = Blueprint("auth_bp", __name__)

# Path to the JSON file storing user credentials. Can be overridden with the
# USERS_FILE_PATH environment variable.
USERS_FILE = os.getenv(
"USERS_FILE_PATH", os.path.join(os.path.dirname(os.path.dirname(__file__)), "users.json")
)


def _load_users():
"""Load the user database from disk."""
if os.path.exists(USERS_FILE):
with open(USERS_FILE, "r", encoding="utf-8") as f:
return json.load(f)
return {}


@auth_bp.route("/api/login", methods=["POST"])
def login():
"""Validate credentials and create a session."""
data = request.get_json() or {}
username = (data.get("username") or "").strip()
password = (data.get("password") or "").strip()

if not username or not password:
return jsonify({"status": "error", "message": "Missing credentials"}), 400

users = _load_users()
password_hash = users.get(username)
if password_hash and check_password_hash(password_hash, password):
session["username"] = username
return jsonify({"status": "success"})

return jsonify({"status": "error", "message": "Invalid credentials"}), 401


@auth_bp.route("/api/logout", methods=["POST"])
def logout():
"""Clear the user's session."""
session.pop("username", None)
return jsonify({"status": "success"})


@auth_bp.route("/api/me", methods=["GET"])
def whoami():
"""Return the currently logged in user, if any."""
username = session.get("username")
return jsonify({"logged_in": bool(username), "username": username})

# Placeholder for future authentication routes
7 changes: 6 additions & 1 deletion pihealth/system_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@

from . import docker_client, docker_available, container_updates

system_bp = Blueprint('system_bp', __name__)
# Serve static HTML files from the project-level static directory
system_bp = Blueprint(
"system_bp",
__name__,
static_folder=os.path.join(os.path.dirname(__file__), "..", "static"),
)


def calculate_cpu_usage(cpu_line):
Expand Down
46 changes: 22 additions & 24 deletions static/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -114,36 +114,34 @@ <h1 class="text-4xl font-bold text-blue-200 font-serif mb-2" style="text-shadow:
</div>

<script>
// Simple credentials for demonstration
const USERNAME = "holly";
const PASSWORD = "pass123";

function validateLogin() {
async function validateLogin() {
const username = document.getElementById("username").value.trim();
const password = document.getElementById("password").value.trim();
const errorEl = document.getElementById("login-error");
const loginCard = document.querySelector(".login-card");

if (username === USERNAME && password === PASSWORD) {
// Store login status in session storage
sessionStorage.setItem("loggedIn", "true");
// Redirect to the dashboard root
window.location.href = "/";
} else {
// Show error message and shake animation
errorEl.classList.remove("hidden");
loginCard.classList.add("shake-animation");

// Remove shake animation after it completes
setTimeout(() => {
loginCard.classList.remove("shake-animation");
}, 500);

// Hide error after 5 seconds
setTimeout(() => {
errorEl.classList.add("hidden");
}, 5000);
try {
const resp = await fetch("/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password })
});

if (resp.ok) {
// Store login status in session storage
sessionStorage.setItem("loggedIn", "true");
window.location.href = "/";
return;
}
} catch (err) {
console.error("Login failed", err);
}

// Show error message and shake animation on failure
errorEl.classList.remove("hidden");
loginCard.classList.add("shake-animation");
setTimeout(() => loginCard.classList.remove("shake-animation"), 500);
setTimeout(() => errorEl.classList.add("hidden"), 5000);
}
</script>
</body>
Expand Down
4 changes: 4 additions & 0 deletions users.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"holly": "scrypt:32768:8:1$k2QmclcvV0Bz6uTR$83d26e85456c25b316141201207aede5fb8d8c0ab5e5a381b7771dbe0413cbbfd40ba5e9eeff03137baef2c7daefd0f86fd848195b9950c230cbae060b22f922",
"admin": "scrypt:32768:8:1$OH9F3zN81tTMvzfE$10afeaca09d5975622b9e20d4eba996cc8d750de790facb98822c51508e75f9ff3df89dd356bf27161a14a08c423b9478f89e375d57d8ecebda2be604412a168"
}