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