Merge remote-tracking branch 'origin/sub_laptop_1' into main_laptop_1
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
"""시스템 로그 기록·정리 한 곳 (2026-09-06 사용자 확정).
|
||||
|
||||
같은 INSERT 문이 여섯 자리에 흩어져 있었고, 대상(무엇에 한 일인가)과 접속 정보(어디서
|
||||
했는가)는 칸만 있고 값이 비어 있었다. 기록은 이 함수 하나로 모은다.
|
||||
|
||||
보관 기간은 `AUDIT_LOG_RETENTION_DAYS`(기본 365일) — 사고 추적에 1년이면 충분하다는
|
||||
사용자 판단(2026-09-06). 임시 보관함 정리 루프가 돌 때 함께 지운다.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any
|
||||
|
||||
from config.config_db import get_db_pool
|
||||
from config.config_system import AUDIT_LOG_RETENTION_DAYS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 브라우저 문자열은 길다 — 표 칸은 TEXT 지만 화면·로그가 감당할 만큼만 자른다.
|
||||
_USER_AGENT_MAX = 300
|
||||
|
||||
|
||||
def request_origin(request: Any | None) -> tuple[str | None, str | None]:
|
||||
"""요청에서 접속 주소와 브라우저 문자열을 꺼낸다. 없으면 (None, None).
|
||||
|
||||
프록시 뒤에서는 `X-Forwarded-For` 의 **첫 주소**가 실제 사용자다.
|
||||
"""
|
||||
if request is None:
|
||||
return None, None
|
||||
try:
|
||||
forwarded = request.headers.get("x-forwarded-for")
|
||||
address = forwarded.split(",")[0].strip() if forwarded else None
|
||||
if not address and request.client is not None:
|
||||
address = request.client.host
|
||||
agent = (request.headers.get("user-agent") or "")[:_USER_AGENT_MAX] or None
|
||||
return address, agent
|
||||
except Exception: # 기록이 본 작업을 막으면 안 된다.
|
||||
return None, None
|
||||
|
||||
|
||||
async def record_audit(
|
||||
cursor: Any,
|
||||
*,
|
||||
actor_id: int,
|
||||
action: str,
|
||||
resource_type: str | None = None,
|
||||
resource_ref: str | int | None = None,
|
||||
request: Any | None = None,
|
||||
) -> None:
|
||||
"""시스템 로그 한 줄을 적는다 — 호출부의 트랜잭션(cursor)에 얹는다.
|
||||
|
||||
`resource_ref` 는 프로젝트 UUID 처럼 문자열이어도 되고 숫자여도 된다. 숫자면 옛
|
||||
`resource_id` 칸에도 같이 넣어 예전 기록과 같은 모양을 지킨다.
|
||||
"""
|
||||
reference = None if resource_ref is None else str(resource_ref)
|
||||
numeric = int(resource_ref) if isinstance(resource_ref, int) else None
|
||||
address, agent = request_origin(request)
|
||||
await cursor.execute(
|
||||
"""INSERT INTO system_audit_logs
|
||||
(user_id, action, resource_type, resource_id, resource_ref, ip_address, user_agent)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s)""",
|
||||
(actor_id, action, resource_type, numeric, reference, address, agent),
|
||||
)
|
||||
|
||||
|
||||
async def purge_expired_audit_logs() -> int:
|
||||
"""보관 기간이 지난 시스템 로그를 지운다. 지운 줄 수를 돌려준다."""
|
||||
cutoff = datetime.now(timezone.utc) - timedelta(days=max(1, AUDIT_LOG_RETENTION_DAYS))
|
||||
pool = get_db_pool()
|
||||
try:
|
||||
async with pool.acquire() as connection, connection.cursor() as cursor:
|
||||
await cursor.execute("DELETE FROM system_audit_logs WHERE timestamp < %s", (cutoff,))
|
||||
removed = cursor.rowcount
|
||||
if removed:
|
||||
await connection.commit()
|
||||
logger.info(
|
||||
"시스템 로그 정리: %d건 삭제 (보관 %d일)", removed, AUDIT_LOG_RETENTION_DAYS
|
||||
)
|
||||
return removed
|
||||
except Exception:
|
||||
logger.exception("시스템 로그 정리 실패")
|
||||
return 0
|
||||
@@ -8,14 +8,16 @@
|
||||
|
||||
import logging
|
||||
import shutil
|
||||
from typing import Any
|
||||
|
||||
from common_util.common_util_audit import record_audit
|
||||
from common_util.common_util_storage import resolve_project_root_for_delete
|
||||
from config.config_db import get_db_pool
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def hard_delete_project(project_id: str, actor_id: int) -> bool:
|
||||
async def hard_delete_project(project_id: str, actor_id: int, request: Any | None = None) -> bool:
|
||||
"""프로젝트를 DB와 영구저장소에서 완전히 지운다. 되돌릴 수 없다.
|
||||
|
||||
자식 테이블은 나열하지 않는다 — `projects.id`를 참조하는 테이블이 전부
|
||||
@@ -60,10 +62,13 @@ async def hard_delete_project(project_id: str, actor_id: int) -> bool:
|
||||
await connection.rollback()
|
||||
return False
|
||||
# 감사 기록은 프로젝트가 사라진 뒤에도 남는다. resource_id는 FK가 없어 고아가 되지 않는다.
|
||||
await cursor.execute(
|
||||
"""INSERT INTO system_audit_logs (user_id, action, resource_type, resource_id)
|
||||
VALUES (%s, 'PROJECT_HARD_DELETE', 'project', NULL)""",
|
||||
(actor_id,),
|
||||
await record_audit(
|
||||
cursor,
|
||||
actor_id=actor_id,
|
||||
action="PROJECT_HARD_DELETE",
|
||||
resource_type="project",
|
||||
resource_ref=project_id,
|
||||
request=request,
|
||||
)
|
||||
await connection.commit()
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from B03_FileInput.B03_FileInput_Repository_Temp import (
|
||||
delete_temp_batch,
|
||||
list_expired_temp_batches,
|
||||
)
|
||||
from common_util.common_util_audit import purge_expired_audit_logs
|
||||
from common_util.common_util_storage import resolve_temp_batch_path, temp_upload_root
|
||||
from config.config_db import get_db_pool
|
||||
from config.config_system import (
|
||||
@@ -81,4 +82,6 @@ async def cleanup_expired_temp_uploads_loop() -> None:
|
||||
removed = await cleanup_expired_temp_uploads()
|
||||
if removed:
|
||||
logger.info("임시 보관함 정리 완료: %d건 삭제", removed)
|
||||
# 시스템 로그 보관 기간 정리도 같은 주기에 얹는다 — 루프를 따로 두지 않는다.
|
||||
await purge_expired_audit_logs()
|
||||
await asyncio.sleep(interval_seconds)
|
||||
|
||||
Reference in New Issue
Block a user