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.
224 lines
7.2 KiB
Python
224 lines
7.2 KiB
Python
import hashlib
|
||
import os
|
||
import secrets
|
||
import shutil
|
||
from datetime import datetime, timezone, timedelta
|
||
from pathlib import Path
|
||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||
from psycopg import AsyncConnection
|
||
|
||
UPLOAD_DIR = Path(os.getenv("UPLOAD_DIR", "/opt/mm/uploads/kyc"))
|
||
VIEW_TOKEN_TTL = timedelta(minutes=10)
|
||
|
||
# AES-256-GCM — 32 byte key, hex encoded in env
|
||
_raw_key = bytes.fromhex(os.getenv("KYC_ENCRYPTION_KEY", "0" * 64))
|
||
_aesgcm = AESGCM(_raw_key)
|
||
|
||
|
||
# --- Şifreleme yardımcıları ---
|
||
|
||
def _encrypt(data: bytes) -> bytes:
|
||
nonce = secrets.token_bytes(12)
|
||
return nonce + _aesgcm.encrypt(nonce, data, None)
|
||
|
||
|
||
def _decrypt(data: bytes) -> bytes:
|
||
return _aesgcm.decrypt(data[:12], data[12:], None)
|
||
|
||
|
||
def _hash_tc(tc: str) -> str:
|
||
return hashlib.sha256(tc.encode()).hexdigest()
|
||
|
||
|
||
# --- Dosya işlemleri ---
|
||
|
||
def _request_dir(request_uuid: str) -> Path:
|
||
path = UPLOAD_DIR / request_uuid
|
||
path.mkdir(parents=True, exist_ok=True)
|
||
return path
|
||
|
||
|
||
def save_file_encrypted(request_uuid: str, filename: str, data: bytes) -> str:
|
||
"""Dosyayı şifreli kaydeder, göreli path döner."""
|
||
enc = _encrypt(data)
|
||
path = _request_dir(request_uuid) / (filename + ".enc")
|
||
path.write_bytes(enc)
|
||
return str(path)
|
||
|
||
|
||
def read_file_encrypted(path: str) -> bytes:
|
||
return _decrypt(Path(path).read_bytes())
|
||
|
||
|
||
def delete_request_files(request_uuid: str):
|
||
d = UPLOAD_DIR / request_uuid
|
||
if d.exists():
|
||
shutil.rmtree(d)
|
||
|
||
|
||
# --- DB işlemleri ---
|
||
|
||
async def submit(
|
||
conn: AsyncConnection,
|
||
user_id: int,
|
||
tc_kimlik: str,
|
||
front_path: str,
|
||
back_path: str,
|
||
selfie_path: str,
|
||
) -> dict:
|
||
# Aynı TC ile başka kayıtlı kullanıcı var mı?
|
||
tc_hash = _hash_tc(tc_kimlik)
|
||
existing = await (await conn.execute(
|
||
"SELECT id FROM users WHERE tc_kimlik_hash = %s AND id != %s", (tc_hash, user_id)
|
||
)).fetchone()
|
||
if existing:
|
||
raise ValueError("Bu kimlik numarası başka bir hesaba kayıtlı")
|
||
|
||
# Bekleyen başvuru var mı?
|
||
pending = await (await conn.execute(
|
||
"SELECT id FROM verification_requests WHERE user_id = %s AND status = 'pending'", (user_id,)
|
||
)).fetchone()
|
||
if pending:
|
||
raise ValueError("Bekleyen bir başvurunuz zaten mevcut")
|
||
|
||
tc_enc = _encrypt(tc_kimlik.encode())
|
||
|
||
await conn.execute(
|
||
"UPDATE users SET tc_kimlik_enc = %s, tc_kimlik_hash = %s, kyc_status = 'pending' WHERE id = %s",
|
||
(tc_enc, tc_hash, user_id)
|
||
)
|
||
row = await (await conn.execute(
|
||
"""INSERT INTO verification_requests (user_id, id_front_path, id_back_path, selfie_path)
|
||
VALUES (%s, %s, %s, %s) RETURNING id, submitted_at""",
|
||
(user_id, front_path, back_path, selfie_path)
|
||
)).fetchone()
|
||
await conn.commit()
|
||
return {"id": row[0], "status": "pending", "submitted_at": row[1]}
|
||
|
||
|
||
async def list_pending(conn: AsyncConnection) -> list[dict]:
|
||
rows = await (await conn.execute(
|
||
"""SELECT vr.id, vr.user_id, u.email, vr.submitted_at, vr.rejection_count
|
||
FROM verification_requests vr
|
||
JOIN users u ON u.id = vr.user_id
|
||
WHERE vr.status = 'pending'
|
||
ORDER BY vr.submitted_at ASC"""
|
||
)).fetchall()
|
||
return [
|
||
{"id": r[0], "user_id": r[1], "email": r[2], "submitted_at": r[3], "rejection_count": r[4]}
|
||
for r in rows
|
||
]
|
||
|
||
|
||
async def get_request(conn: AsyncConnection, request_id: int) -> dict | None:
|
||
row = await (await conn.execute(
|
||
"""SELECT vr.id, vr.user_id, u.email, vr.status, vr.submitted_at,
|
||
vr.id_front_path, vr.id_back_path, vr.selfie_path,
|
||
vr.review_note, vr.reviewed_at, vr.rejection_count
|
||
FROM verification_requests vr
|
||
JOIN users u ON u.id = vr.user_id
|
||
WHERE vr.id = %s""",
|
||
(request_id,)
|
||
)).fetchone()
|
||
if not row:
|
||
return None
|
||
return {
|
||
"id": row[0], "user_id": row[1], "email": row[2], "status": row[3],
|
||
"submitted_at": row[4], "id_front_path": row[5], "id_back_path": row[6],
|
||
"selfie_path": row[7], "review_note": row[8], "reviewed_at": row[9],
|
||
"rejection_count": row[10],
|
||
}
|
||
|
||
|
||
async def create_view_token(conn: AsyncConnection, request_id: int, admin_id: int) -> str:
|
||
token = secrets.token_urlsafe(32)
|
||
expires_at = datetime.now(timezone.utc) + VIEW_TOKEN_TTL
|
||
await conn.execute(
|
||
"INSERT INTO kyc_view_tokens (token, request_id, created_by, expires_at) VALUES (%s, %s, %s, %s)",
|
||
(token, request_id, admin_id, expires_at)
|
||
)
|
||
await conn.commit()
|
||
return token
|
||
|
||
|
||
async def consume_view_token(conn: AsyncConnection, token: str) -> dict | None:
|
||
"""Token'ı tüketir (tek kullanımlık), geçerliyse request bilgilerini döner."""
|
||
row = await (await conn.execute(
|
||
"""SELECT request_id, expires_at, used_at
|
||
FROM kyc_view_tokens WHERE token = %s""",
|
||
(token,)
|
||
)).fetchone()
|
||
if not row:
|
||
return None
|
||
request_id, expires_at, used_at = row
|
||
if used_at is not None:
|
||
return None
|
||
if expires_at < datetime.now(timezone.utc):
|
||
return None
|
||
await conn.execute(
|
||
"UPDATE kyc_view_tokens SET used_at = NOW() WHERE token = %s", (token,)
|
||
)
|
||
await conn.commit()
|
||
return await get_request(conn, request_id)
|
||
|
||
|
||
async def approve(conn: AsyncConnection, request_id: int, reviewed_by: int) -> dict:
|
||
req = await get_request(conn, request_id)
|
||
if not req:
|
||
raise ValueError("Başvuru bulunamadı")
|
||
if req["status"] != "pending":
|
||
raise ValueError("Bu başvuru zaten işlenmiş")
|
||
|
||
await conn.execute(
|
||
"""UPDATE verification_requests
|
||
SET status = 'approved', reviewed_by = %s, reviewed_at = NOW()
|
||
WHERE id = %s""",
|
||
(reviewed_by, request_id)
|
||
)
|
||
await conn.execute(
|
||
"UPDATE users SET kyc_status = 'verified', kyc_verified_at = NOW() WHERE id = %s",
|
||
(req["user_id"],)
|
||
)
|
||
await conn.commit()
|
||
|
||
delete_request_files(_request_uuid(req))
|
||
return {"ok": True}
|
||
|
||
|
||
async def reject(conn: AsyncConnection, request_id: int, reviewed_by: int, note: str) -> dict:
|
||
req = await get_request(conn, request_id)
|
||
if not req:
|
||
raise ValueError("Başvuru bulunamadı")
|
||
if req["status"] != "pending":
|
||
raise ValueError("Bu başvuru zaten işlenmiş")
|
||
|
||
await conn.execute(
|
||
"""UPDATE verification_requests
|
||
SET status = 'rejected', reviewed_by = %s, reviewed_at = NOW(),
|
||
review_note = %s, rejection_count = rejection_count + 1
|
||
WHERE id = %s""",
|
||
(reviewed_by, note, request_id)
|
||
)
|
||
await conn.execute(
|
||
"UPDATE users SET kyc_status = 'rejected' WHERE id = %s",
|
||
(req["user_id"],)
|
||
)
|
||
await conn.commit()
|
||
|
||
delete_request_files(_request_uuid(req))
|
||
return {"ok": True}
|
||
|
||
|
||
async def get_tc_kimlik(conn: AsyncConnection, user_id: int) -> str | None:
|
||
"""Adli soruşturma: kullanıcının TC kimlik nosunu döner."""
|
||
row = await (await conn.execute(
|
||
"SELECT tc_kimlik_enc FROM users WHERE id = %s", (user_id,)
|
||
)).fetchone()
|
||
if not row or not row[0]:
|
||
return None
|
||
return _decrypt(bytes(row[0])).decode()
|
||
|
||
|
||
def _request_uuid(req: dict) -> str:
|
||
# Dosya path'inden UUID klasör adını çıkar
|
||
return Path(req["id_front_path"]).parent.name
|