FastAPI + PostgreSQL 16. KYC, issue sistemi, permission/group yönetimi, session yönetimi, API client auth (kışla kapısı), officials/persons CRUD. Migration 0001–0013 dahil.
144 lines
4.6 KiB
Python
144 lines
4.6 KiB
Python
import uuid
|
||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form
|
||
from fastapi.responses import Response
|
||
from psycopg import AsyncConnection
|
||
from mm_api.db import get_conn
|
||
from mm_api.dependencies import current_user
|
||
from pydantic import BaseModel
|
||
import mm_api.services.kyc as svc
|
||
import mm_api.services.permission as perm_svc
|
||
|
||
router = APIRouter(prefix="/kyc", tags=["kyc"])
|
||
|
||
ALLOWED_MIME = {"image/jpeg", "image/png"}
|
||
MAX_FILE_SIZE = 5 * 1024 * 1024 # 5 MB
|
||
|
||
|
||
class RejectRequest(BaseModel):
|
||
note: str
|
||
|
||
|
||
async def _require_kyc_perm(action: str, user: dict, conn: AsyncConnection):
|
||
if not await perm_svc.can(conn, user["id"], action, "kyc", is_admin=True):
|
||
raise HTTPException(403, "Yetersiz yetki")
|
||
|
||
|
||
def _validate_file(file: UploadFile):
|
||
if file.content_type not in ALLOWED_MIME:
|
||
raise HTTPException(400, f"Geçersiz dosya türü: {file.content_type}")
|
||
|
||
|
||
@router.post("/submit", status_code=201)
|
||
async def submit(
|
||
tc_kimlik: str = Form(..., min_length=11, max_length=11, pattern=r"^\d{11}$"),
|
||
id_front: UploadFile = File(...),
|
||
id_back: UploadFile = File(...),
|
||
selfie: UploadFile = File(...),
|
||
user: dict = Depends(current_user),
|
||
conn: AsyncConnection = Depends(get_conn),
|
||
):
|
||
for f in (id_front, id_back, selfie):
|
||
_validate_file(f)
|
||
|
||
req_uuid = str(uuid.uuid4())
|
||
|
||
front_data = await id_front.read()
|
||
back_data = await id_back.read()
|
||
selfie_data = await selfie.read()
|
||
|
||
for data, name in ((front_data, "id_front"), (back_data, "id_back"), (selfie_data, "selfie")):
|
||
if len(data) > MAX_FILE_SIZE:
|
||
raise HTTPException(400, f"{name} dosyası 5MB'dan büyük olamaz")
|
||
|
||
front_path = svc.save_file_encrypted(req_uuid, "id_front", front_data)
|
||
back_path = svc.save_file_encrypted(req_uuid, "id_back", back_data)
|
||
selfie_path = svc.save_file_encrypted(req_uuid, "selfie", selfie_data)
|
||
|
||
try:
|
||
return await svc.submit(conn, user["id"], tc_kimlik, front_path, back_path, selfie_path)
|
||
except ValueError as e:
|
||
svc.delete_request_files(req_uuid)
|
||
raise HTTPException(400, str(e))
|
||
|
||
|
||
@router.get("/pending")
|
||
async def list_pending(
|
||
user: dict = Depends(current_user),
|
||
conn: AsyncConnection = Depends(get_conn),
|
||
):
|
||
await _require_kyc_perm("view", user, conn)
|
||
return await svc.list_pending(conn)
|
||
|
||
|
||
@router.post("/{request_id}/view-token")
|
||
async def get_view_token(
|
||
request_id: int,
|
||
user: dict = Depends(current_user),
|
||
conn: AsyncConnection = Depends(get_conn),
|
||
):
|
||
await _require_kyc_perm("view", user, conn)
|
||
req = await svc.get_request(conn, request_id)
|
||
if not req:
|
||
raise HTTPException(404, "Başvuru bulunamadı")
|
||
token = await svc.create_view_token(conn, request_id, user["id"])
|
||
return {"view_token": token, "expires_in": 600}
|
||
|
||
|
||
@router.get("/view/{token}/{file_type}")
|
||
async def view_file(
|
||
token: str,
|
||
file_type: str,
|
||
conn: AsyncConnection = Depends(get_conn),
|
||
):
|
||
if file_type not in ("id_front", "id_back", "selfie"):
|
||
raise HTTPException(400, "Geçersiz dosya türü")
|
||
|
||
req = await svc.consume_view_token(conn, token)
|
||
if not req:
|
||
raise HTTPException(404, "Geçersiz veya süresi dolmuş token")
|
||
|
||
path_key = f"{file_type}_path"
|
||
data = svc.read_file_encrypted(req[path_key])
|
||
return Response(content=data, media_type="image/jpeg")
|
||
|
||
|
||
@router.post("/{request_id}/approve")
|
||
async def approve(
|
||
request_id: int,
|
||
user: dict = Depends(current_user),
|
||
conn: AsyncConnection = Depends(get_conn),
|
||
):
|
||
await _require_kyc_perm("approve", user, conn)
|
||
try:
|
||
return await svc.approve(conn, request_id, user["id"])
|
||
except ValueError as e:
|
||
raise HTTPException(400, str(e))
|
||
|
||
|
||
@router.post("/{request_id}/reject")
|
||
async def reject(
|
||
request_id: int,
|
||
data: RejectRequest,
|
||
user: dict = Depends(current_user),
|
||
conn: AsyncConnection = Depends(get_conn),
|
||
):
|
||
await _require_kyc_perm("reject", user, conn)
|
||
try:
|
||
return await svc.reject(conn, request_id, user["id"], data.note)
|
||
except ValueError as e:
|
||
raise HTTPException(400, str(e))
|
||
|
||
|
||
@router.get("/users/{user_id}/tc")
|
||
async def get_tc(
|
||
user_id: int,
|
||
user: dict = Depends(current_user),
|
||
conn: AsyncConnection = Depends(get_conn),
|
||
):
|
||
"""Adli soruşturma endpoint'i — TC kimlik nosunu döner."""
|
||
if not await perm_svc.can(conn, user["id"], "view", "kyc", is_admin=True):
|
||
raise HTTPException(403, "Yetersiz yetki")
|
||
tc = await svc.get_tc_kimlik(conn, user_id)
|
||
if tc is None:
|
||
raise HTTPException(404, "Kimlik bilgisi bulunamadı")
|
||
return {"user_id": user_id, "tc_kimlik": tc}
|