Spaces:
Running
Running
Added audit logs to be logged into database for staff ease
Browse files
backend/app/api/auth_routes.py
CHANGED
|
@@ -10,9 +10,9 @@ from app.services.email_service import send_reset_password_email, send_verificat
|
|
| 10 |
router = APIRouter(prefix="/auth", tags=["Auth"])
|
| 11 |
|
| 12 |
@router.post("/login")
|
| 13 |
-
def login(data: LoginRequest, db: Session = Depends(get_db)):
|
| 14 |
try:
|
| 15 |
-
token = login_user(db, data.email, data.password)
|
| 16 |
if not token:
|
| 17 |
raise HTTPException(status_code=401, detail="Invalid Credentials")
|
| 18 |
return {"access_token": token}
|
|
@@ -77,8 +77,8 @@ def request_reset(
|
|
| 77 |
return {"message": "If that email exists, a reset link has been sent."}
|
| 78 |
|
| 79 |
@router.post("/reset-password")
|
| 80 |
-
def reset_password(data: PasswordResetConfirm, db: Session = Depends(get_db)):
|
| 81 |
-
success = confirm_password_reset(db, data.token, data.new_password)
|
| 82 |
if not success:
|
| 83 |
raise HTTPException(
|
| 84 |
status_code=status.HTTP_400_BAD_REQUEST,
|
|
|
|
| 10 |
router = APIRouter(prefix="/auth", tags=["Auth"])
|
| 11 |
|
| 12 |
@router.post("/login")
|
| 13 |
+
def login(data: LoginRequest, request: Request, db: Session = Depends(get_db)):
|
| 14 |
try:
|
| 15 |
+
token = login_user(db, data.email, data.password, request)
|
| 16 |
if not token:
|
| 17 |
raise HTTPException(status_code=401, detail="Invalid Credentials")
|
| 18 |
return {"access_token": token}
|
|
|
|
| 77 |
return {"message": "If that email exists, a reset link has been sent."}
|
| 78 |
|
| 79 |
@router.post("/reset-password")
|
| 80 |
+
def reset_password(data: PasswordResetConfirm, request: Request, db: Session = Depends(get_db)):
|
| 81 |
+
success = confirm_password_reset(db, data.token, data.new_password, request)
|
| 82 |
if not success:
|
| 83 |
raise HTTPException(
|
| 84 |
status_code=status.HTTP_400_BAD_REQUEST,
|
backend/app/models/audit_model.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import Column, Integer, String, DateTime, JSON, ForeignKey
|
| 2 |
+
from app.db.database import Base
|
| 3 |
+
from datetime import datetime, UTC
|
| 4 |
+
|
| 5 |
+
class AuditLog(Base):
|
| 6 |
+
__tablename__ = "audit_logs"
|
| 7 |
+
|
| 8 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 9 |
+
user_id = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
|
| 10 |
+
action = Column(String, index=True, nullable=False)
|
| 11 |
+
timestamp = Column(DateTime, default=datetime.now(UTC), index=True)
|
| 12 |
+
ip = Column(String, nullable=True)
|
| 13 |
+
metadata_info = Column(JSON, nullable=True)
|
backend/app/services/audit_service.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy.orm import Session
|
| 2 |
+
from app.models.audit_model import AuditLog
|
| 3 |
+
from fastapi import Request
|
| 4 |
+
from app.core.request_id import get_request_id
|
| 5 |
+
|
| 6 |
+
def log_audit_event(
|
| 7 |
+
db: Session,
|
| 8 |
+
action: str,
|
| 9 |
+
user_id: int | None = None,
|
| 10 |
+
request: Request | None = None,
|
| 11 |
+
details: dict | None = None
|
| 12 |
+
):
|
| 13 |
+
ip_address = None
|
| 14 |
+
if request and request.client:
|
| 15 |
+
ip_address = request.client.host
|
| 16 |
+
|
| 17 |
+
# ip_address = request.headers.get("X-Forwarded-For", request.client.host)
|
| 18 |
+
meta = details or {}
|
| 19 |
+
meta["request_id"] = get_request_id()
|
| 20 |
+
|
| 21 |
+
audit_entry = AuditLog(
|
| 22 |
+
user_id=user_id,
|
| 23 |
+
action=action,
|
| 24 |
+
ip=ip_address,
|
| 25 |
+
metadata_info=meta
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
db.add(audit_entry)
|
| 29 |
+
db.commit()
|
backend/app/services/auth_service.py
CHANGED
|
@@ -4,17 +4,21 @@ from sqlalchemy.orm import Session
|
|
| 4 |
from app.models.user_model import User
|
| 5 |
from app.utils.security import verify_password, hash_password
|
| 6 |
from app.utils.jwt_handler import create_access_token
|
|
|
|
|
|
|
| 7 |
|
| 8 |
-
def login_user(db: Session, email: str, password: str):
|
| 9 |
user = db.query(User).filter(User.email == email).first()
|
| 10 |
|
| 11 |
if not user:
|
|
|
|
| 12 |
return None
|
| 13 |
|
| 14 |
if user.locked_until:
|
| 15 |
if datetime.now(UTC).replace(tzinfo=None) < user.locked_until:
|
| 16 |
time_left = user.locked_until - datetime.now(UTC).replace(tzinfo=None)
|
| 17 |
minutes_left = int(time_left.total_seconds() / 60) + 1
|
|
|
|
| 18 |
raise ValueError(f"locked_{minutes_left}")
|
| 19 |
else:
|
| 20 |
user.locked_until = None
|
|
@@ -27,10 +31,12 @@ def login_user(db: Session, email: str, password: str):
|
|
| 27 |
if user.failed_attempts >= 5:
|
| 28 |
user.locked_until = datetime.now(UTC).replace(tzinfo=None) + timedelta(minutes=15)
|
| 29 |
db.commit()
|
|
|
|
| 30 |
raise ValueError("locked_15")
|
| 31 |
db.commit()
|
| 32 |
|
| 33 |
attempts_left = 5 - user.failed_attempts
|
|
|
|
| 34 |
raise ValueError(f"attempt_{attempts_left}")
|
| 35 |
|
| 36 |
user.failed_attempts = 0
|
|
@@ -38,9 +44,11 @@ def login_user(db: Session, email: str, password: str):
|
|
| 38 |
db.commit()
|
| 39 |
|
| 40 |
if not user.is_verified:
|
|
|
|
| 41 |
raise ValueError("unverified")
|
| 42 |
|
| 43 |
token = create_access_token({"user_id": user.id})
|
|
|
|
| 44 |
return token
|
| 45 |
|
| 46 |
def request_password_reset(db: Session, email: str) -> str | None:
|
|
@@ -58,7 +66,7 @@ def request_password_reset(db: Session, email: str) -> str | None:
|
|
| 58 |
|
| 59 |
return raw_token
|
| 60 |
|
| 61 |
-
def confirm_password_reset(db: Session, token: str, new_password: str) -> bool:
|
| 62 |
active_reset_users = db.query(User).filter(User.reset_token_hash.isnot(None)).all()
|
| 63 |
|
| 64 |
target_user = None
|
|
@@ -79,6 +87,7 @@ def confirm_password_reset(db: Session, token: str, new_password: str) -> bool:
|
|
| 79 |
|
| 80 |
db.commit()
|
| 81 |
|
|
|
|
| 82 |
return True
|
| 83 |
|
| 84 |
def confirm_email_verification(db: Session, token: str) -> bool:
|
|
|
|
| 4 |
from app.models.user_model import User
|
| 5 |
from app.utils.security import verify_password, hash_password
|
| 6 |
from app.utils.jwt_handler import create_access_token
|
| 7 |
+
from fastapi import Request
|
| 8 |
+
from app.services.audit_service import log_audit_event
|
| 9 |
|
| 10 |
+
def login_user(db: Session, email: str, password: str, request: Request):
|
| 11 |
user = db.query(User).filter(User.email == email).first()
|
| 12 |
|
| 13 |
if not user:
|
| 14 |
+
log_audit_event(db, "login_failed", None, request, {"email_attempted": email, "reason": "user_not_found"})
|
| 15 |
return None
|
| 16 |
|
| 17 |
if user.locked_until:
|
| 18 |
if datetime.now(UTC).replace(tzinfo=None) < user.locked_until:
|
| 19 |
time_left = user.locked_until - datetime.now(UTC).replace(tzinfo=None)
|
| 20 |
minutes_left = int(time_left.total_seconds() / 60) + 1
|
| 21 |
+
log_audit_event(db, "login_blocked", user.id, request, {"reason": "account_locked", "minutes_left": minutes_left})
|
| 22 |
raise ValueError(f"locked_{minutes_left}")
|
| 23 |
else:
|
| 24 |
user.locked_until = None
|
|
|
|
| 31 |
if user.failed_attempts >= 5:
|
| 32 |
user.locked_until = datetime.now(UTC).replace(tzinfo=None) + timedelta(minutes=15)
|
| 33 |
db.commit()
|
| 34 |
+
log_audit_event(db, "account_locked", user.id, request, {"reason": "max_failed_attempts"})
|
| 35 |
raise ValueError("locked_15")
|
| 36 |
db.commit()
|
| 37 |
|
| 38 |
attempts_left = 5 - user.failed_attempts
|
| 39 |
+
log_audit_event(db, "login_failed", user.id, request, {"reason": "bad_password", "attempts_left": attempts_left})
|
| 40 |
raise ValueError(f"attempt_{attempts_left}")
|
| 41 |
|
| 42 |
user.failed_attempts = 0
|
|
|
|
| 44 |
db.commit()
|
| 45 |
|
| 46 |
if not user.is_verified:
|
| 47 |
+
log_audit_event(db, "login_blocked", user.id, request, {"reason": "unverified_email"})
|
| 48 |
raise ValueError("unverified")
|
| 49 |
|
| 50 |
token = create_access_token({"user_id": user.id})
|
| 51 |
+
log_audit_event(db, "login_success", user.id, request, {"provider": "local"})
|
| 52 |
return token
|
| 53 |
|
| 54 |
def request_password_reset(db: Session, email: str) -> str | None:
|
|
|
|
| 66 |
|
| 67 |
return raw_token
|
| 68 |
|
| 69 |
+
def confirm_password_reset(db: Session, token: str, new_password: str, request: Request) -> bool:
|
| 70 |
active_reset_users = db.query(User).filter(User.reset_token_hash.isnot(None)).all()
|
| 71 |
|
| 72 |
target_user = None
|
|
|
|
| 87 |
|
| 88 |
db.commit()
|
| 89 |
|
| 90 |
+
log_audit_event(db, "password_reset", target_user.id, request)
|
| 91 |
return True
|
| 92 |
|
| 93 |
def confirm_email_verification(db: Session, token: str) -> bool:
|