implement activity logging system for user actions and system events

This commit is contained in:
CPTN Cosmo 2026-06-19 03:35:51 +02:00
parent 81f7031555
commit 88e38535fc
5 changed files with 958 additions and 500 deletions

117
main.py
View file

@ -90,6 +90,16 @@ def init_db():
)
''')
# Create activity_logs table
c.execute('''
CREATE TABLE IF NOT EXISTS activity_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp REAL NOT NULL,
level TEXT NOT NULL,
message TEXT NOT NULL
)
''')
# Now check for column migrations
c.execute("PRAGMA table_info(instances)")
columns = [col[1] for col in c.fetchall()]
@ -135,7 +145,20 @@ def init_db():
conn.commit()
conn.close()
def log_activity(level: str, message: str):
try:
conn = get_db()
c = conn.cursor()
c.execute('INSERT INTO activity_logs (timestamp, level, message) VALUES (?, ?, ?)',
(time.time(), level, message))
conn.commit()
conn.close()
except Exception as e:
print(f"Failed to log activity: {e}")
init_db()
log_activity("INFO", "Database initialized / application startup.")
def is_authenticated(request: Request):
if not request.session.get("user_id"):
@ -164,14 +187,20 @@ async def login(request: Request):
request.session["user_id"] = user['id']
request.session["username"] = user['username']
request.session["is_admin"] = bool(user['is_admin'])
log_activity("INFO", f"User '{user['username']}' logged in successfully.")
return {"status": "ok", "user": {"username": user['username'], "is_admin": bool(user['is_admin'])}}
log_activity("WARNING", f"Failed login attempt for username '{username}'.")
raise HTTPException(status_code=401, detail="Invalid username or password")
@app.get("/api/logout")
async def logout(request: Request):
username = request.session.get("username", "Unknown")
request.session.clear()
log_activity("INFO", f"User '{username}' logged out.")
return {"status": "ok"}
@app.get("/api/auth/status")
async def auth_status(request: Request):
if request.session.get("user_id"):
@ -213,6 +242,8 @@ async def create_user(request: Request):
(username, hashed, is_admin))
conn.commit()
conn.close()
admin_username = request.session.get("username", "Admin")
log_activity("INFO", f"Admin '{admin_username}' created user '{username}' (Admin: {bool(is_admin)}).")
except sqlite3.IntegrityError:
raise HTTPException(status_code=400, detail="Username already exists")
return {"status": "ok"}
@ -223,11 +254,18 @@ def delete_user(id: int, request: Request):
raise HTTPException(status_code=400, detail="Cannot delete yourself")
conn = get_db()
c = conn.cursor()
c.execute('SELECT username FROM users WHERE id=?', (id,))
target_user_row = c.fetchone()
target_username = target_user_row[0] if target_user_row else f"ID {id}"
c.execute('DELETE FROM users WHERE id=?', (id,))
# Also delete their instances? Let's say yes for cleanliness.
c.execute('DELETE FROM instances WHERE user_id=?', (id,))
conn.commit()
conn.close()
admin_username = request.session.get("username", "Admin")
log_activity("INFO", f"Admin '{admin_username}' deleted user '{target_username}' and all associated instances.")
return {"status": "ok"}
@app.put("/api/users/{id}/password", dependencies=[Depends(is_authenticated), Depends(is_admin)])
@ -239,11 +277,19 @@ async def admin_reset_password(id: int, request: Request):
hashed = get_password_hash(new_password)
conn = get_db()
c = conn.cursor()
c.execute('SELECT username FROM users WHERE id=?', (id,))
target_user_row = c.fetchone()
target_username = target_user_row[0] if target_user_row else f"ID {id}"
c.execute('UPDATE users SET hashed_password = ? WHERE id = ?', (hashed, id))
conn.commit()
conn.close()
admin_username = request.session.get("username", "Admin")
log_activity("INFO", f"Admin '{admin_username}' reset password for user '{target_username}'.")
return {"status": "ok"}
# Own Password Change (All Users)
@app.put("/api/users/me/password", dependencies=[Depends(is_authenticated)])
async def change_own_password(request: Request):
@ -252,12 +298,15 @@ async def change_own_password(request: Request):
if not new_password:
raise HTTPException(status_code=400, detail="New password required")
user_id = request.session.get("user_id")
username = request.session.get("username")
hashed = get_password_hash(new_password)
conn = get_db()
c = conn.cursor()
c.execute('UPDATE users SET hashed_password = ? WHERE id = ?', (hashed, user_id))
conn.commit()
conn.close()
log_activity("INFO", f"User '{username}' updated their password.")
return {"status": "ok"}
@app.put("/api/users/me/username", dependencies=[Depends(is_authenticated)])
@ -267,16 +316,20 @@ async def change_own_username(request: Request):
if not new_username:
raise HTTPException(status_code=400, detail="New username required")
user_id = request.session.get("user_id")
old_username = request.session.get("username")
try:
conn = get_db()
c = conn.cursor()
c.execute('UPDATE users SET username = ? WHERE id = ?', (new_username, user_id))
conn.commit()
conn.close()
request.session["username"] = new_username
log_activity("INFO", f"User '{old_username}' updated their username to '{new_username}'.")
except sqlite3.IntegrityError:
raise HTTPException(status_code=400, detail="Username already exists")
return {"status": "ok"}
class OTPSecretCreate(BaseModel):
name: str
secret: str
@ -324,6 +377,7 @@ def get_otp_secrets(request: Request):
@app.post("/api/otp-secrets", dependencies=[Depends(is_authenticated)])
def create_otp_secret(secret_data: OTPSecretCreate, request: Request):
user_id = request.session.get("user_id")
username = request.session.get("username")
cleaned = clean_secret(secret_data.secret)
encrypted = fernet.encrypt(cleaned.encode()).decode()
conn = get_db()
@ -334,19 +388,23 @@ def create_otp_secret(secret_data: OTPSecretCreate, request: Request):
''', (secret_data.name, encrypted, user_id))
conn.commit()
conn.close()
log_activity("INFO", f"User '{username}' created OTP secret '{secret_data.name}'.")
return {"status": "ok"}
@app.put("/api/otp-secrets/{id}", dependencies=[Depends(is_authenticated)])
def update_otp_secret(id: int, secret_data: OTPSecretUpdate, request: Request):
user_id = request.session.get("user_id")
username = request.session.get("username")
conn = get_db()
c = conn.cursor()
# Verify ownership
c.execute('SELECT id FROM otp_secrets WHERE id=? AND user_id=?', (id, user_id))
if not c.fetchone():
c.execute('SELECT name FROM otp_secrets WHERE id=? AND user_id=?', (id, user_id))
row = c.fetchone()
if not row:
conn.close()
raise HTTPException(status_code=404, detail="OTP Secret not found")
old_name = row[0]
if secret_data.secret:
cleaned = clean_secret(secret_data.secret)
@ -355,11 +413,13 @@ def update_otp_secret(id: int, secret_data: OTPSecretUpdate, request: Request):
UPDATE otp_secrets SET name=?, encrypted_secret=?
WHERE id=? AND user_id=?
''', (secret_data.name, encrypted, id, user_id))
log_activity("INFO", f"User '{username}' updated OTP secret '{old_name}' (name updated to '{secret_data.name}' and key value changed).")
else:
c.execute('''
UPDATE otp_secrets SET name=?
WHERE id=? AND user_id=?
''', (secret_data.name, id, user_id))
log_activity("INFO", f"User '{username}' updated OTP secret name '{old_name}' to '{secret_data.name}'.")
conn.commit()
conn.close()
@ -368,9 +428,18 @@ def update_otp_secret(id: int, secret_data: OTPSecretUpdate, request: Request):
@app.delete("/api/otp-secrets/{id}", dependencies=[Depends(is_authenticated)])
def delete_otp_secret(id: int, request: Request):
user_id = request.session.get("user_id")
username = request.session.get("username")
conn = get_db()
c = conn.cursor()
# Verify ownership
c.execute('SELECT name FROM otp_secrets WHERE id=? AND user_id=?', (id, user_id))
row = c.fetchone()
if not row:
conn.close()
raise HTTPException(status_code=404, detail="OTP Secret not found")
secret_name = row[0]
# Check if used by any instances
c.execute('SELECT COUNT(*) FROM instances WHERE otp_secret_id=?', (id,))
if c.fetchone()[0] > 0:
@ -380,6 +449,7 @@ def delete_otp_secret(id: int, request: Request):
c.execute('DELETE FROM otp_secrets WHERE id=? AND user_id=?', (id, user_id))
conn.commit()
conn.close()
log_activity("INFO", f"User '{username}' deleted OTP secret '{secret_name}'.")
return {"status": "ok"}
async def poll_instances():
@ -419,13 +489,17 @@ async def poll_instances():
if resp.status_code == 200:
status_msg = "Sent"
log_activity("INFO", f"Polled instance '{inst['name']}' ({ip}:{port}) - OTP sent successfully.")
else:
status_msg = f"Failed: {resp.status_code}"
log_activity("WARNING", f"Polled instance '{inst['name']}' ({ip}:{port}) - Failed: HTTP status {resp.status_code}.")
except (httpx.ConnectError, httpx.TimeoutException):
status_msg = "Offline"
log_activity("INFO", f"Polled instance '{inst['name']}' ({ip}:{port}) - Offline (no response on port {port}).")
except Exception as e:
status_msg = f"Error: {str(e)}"
log_activity("ERROR", f"Polled instance '{inst['name']}' ({ip}:{port}) - Error: {str(e)}.")
conn = get_db()
c = conn.cursor()
@ -464,6 +538,7 @@ def get_instances(request: Request):
@app.post("/api/instances", dependencies=[Depends(is_authenticated)])
def create_instance(inst: InstanceCreate, request: Request):
user_id = request.session.get("user_id")
username = request.session.get("username")
conn = get_db()
c = conn.cursor()
c.execute('''
@ -472,19 +547,23 @@ def create_instance(inst: InstanceCreate, request: Request):
''', (inst.name, inst.ip, inst.port, inst.otp_secret_id, user_id, ""))
conn.commit()
conn.close()
log_activity("INFO", f"User '{username}' created instance '{inst.name}' ({inst.ip}:{inst.port}).")
return {"status": "ok"}
@app.put("/api/instances/{id}", dependencies=[Depends(is_authenticated)])
def update_instance(id: int, inst: InstanceUpdate, request: Request):
user_id = request.session.get("user_id")
username = request.session.get("username")
conn = get_db()
c = conn.cursor()
# Verify ownership
c.execute('SELECT id FROM instances WHERE id=? AND user_id=?', (id, user_id))
if not c.fetchone():
c.execute('SELECT name FROM instances WHERE id=? AND user_id=?', (id, user_id))
row = c.fetchone()
if not row:
conn.close()
raise HTTPException(status_code=404, detail="Instance not found")
old_name = row[0]
c.execute('''
UPDATE instances SET name=?, ip=?, port=?, otp_secret_id=?
@ -493,22 +572,44 @@ def update_instance(id: int, inst: InstanceUpdate, request: Request):
conn.commit()
conn.close()
log_activity("INFO", f"User '{username}' updated instance '{old_name}' -> '{inst.name}' ({inst.ip}:{inst.port}).")
return {"status": "ok"}
@app.get("/api/config", dependencies=[Depends(is_authenticated)])
def get_config():
return {"firewall_host_ip": FIREWALL_HOST_IP}
@app.delete("/api/instances/{id}", dependencies=[Depends(is_authenticated)])
def delete_instance(id: int, request: Request):
user_id = request.session.get("user_id")
username = request.session.get("username")
conn = get_db()
c = conn.cursor()
c.execute('SELECT name FROM instances WHERE id=? AND user_id=?', (id, user_id))
row = c.fetchone()
if not row:
conn.close()
raise HTTPException(status_code=404, detail="Instance not found")
inst_name = row[0]
c.execute('DELETE FROM instances WHERE id=? AND user_id=?', (id, user_id))
conn.commit()
conn.close()
log_activity("INFO", f"User '{username}' deleted instance '{inst_name}'.")
return {"status": "ok"}
@app.get("/api/config")
def get_config():
return {"firewall_host_ip": FIREWALL_HOST_IP}
@app.get("/api/logs", dependencies=[Depends(is_authenticated)])
def get_logs():
conn = get_db()
conn.row_factory = sqlite3.Row
c = conn.cursor()
c.execute('SELECT id, timestamp, level, message FROM activity_logs ORDER BY id DESC LIMIT 100')
logs = [dict(row) for row in c.fetchall()]
conn.close()
return logs
# Mount root and static
@app.get("/")
def read_root():