"""Autentikasi: registrasi (UC-01) & login dengan password hashing. Memakai hashlib + salt (PBKDF2) dari pustaka standar agar tidak menambah dependency eksternal. Logika validasi dipisah agar dapat diuji langsung. """ from __future__ import annotations import hashlib import hmac import os import re from dataclasses import dataclass from src import db from src.constants import ROLE_MASYARAKAT _EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$") _PBKDF2_ROUNDS = 200_000 def hash_password(password: str) -> str: """Hash password dengan PBKDF2-HMAC-SHA256 + salt acak. Format: salt$hash (hex).""" salt = os.urandom(16) dk = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, _PBKDF2_ROUNDS) return f"{salt.hex()}${dk.hex()}" def verify_password(password: str, stored: str) -> bool: try: salt_hex, hash_hex = stored.split("$", 1) except ValueError: return False salt = bytes.fromhex(salt_hex) dk = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, _PBKDF2_ROUNDS) return hmac.compare_digest(dk.hex(), hash_hex) @dataclass class AuthResult: ok: bool message: str user_id: int | None = None role: str | None = None nama: str | None = None def validate_registration(nama, email, telepon, password, konfirmasi) -> str | None: """Validasi field pendaftaran. Kembalikan pesan error, atau None bila valid.""" if not all([nama, email, telepon, password, konfirmasi]): return "Semua field wajib diisi." if not _EMAIL_RE.match(email): return "Format email tidak valid." if not telepon.isdigit() or not (8 <= len(telepon) <= 15): return "Nomor telepon harus 8-15 digit angka." if len(password) < 6: return "Password minimal 6 karakter." if password != konfirmasi: return "Password dan konfirmasi tidak cocok." return None def register(conn, nama, email, telepon, password, konfirmasi, role=ROLE_MASYARAKAT) -> AuthResult: error = validate_registration(nama, email, telepon, password, konfirmasi) if error: return AuthResult(False, error) if db.email_exists(conn, email): return AuthResult(False, "Email sudah terdaftar.") # UC-01 eksepsi A.2 if db.telepon_exists(conn, telepon): return AuthResult(False, "Nomor sudah terdaftar.") # UC-01 eksepsi A.3 user_id = db.create_user(conn, nama.strip(), email.strip().lower(), telepon.strip(), hash_password(password), role) return AuthResult(True, "Pendaftaran berhasil. Silakan masuk.", user_id=user_id, role=role, nama=nama) def login(conn, email, password) -> AuthResult: if not email or not password: return AuthResult(False, "Email dan password wajib diisi.") user = db.get_user_by_email(conn, email.strip().lower()) if user is None or not verify_password(password, user["password_hash"]): return AuthResult(False, "Email atau password salah.") return AuthResult(True, "Login berhasil.", user_id=user["id"], role=user["role"], nama=user["nama"])