implement activity logging system for user actions and system events
This commit is contained in:
parent
81f7031555
commit
88e38535fc
5 changed files with 958 additions and 500 deletions
117
main.py
117
main.py
|
|
@ -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():
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue