Fix admin organization proof image preview path and implement forgot password feature with rate limiting and check email warning

This commit is contained in:
ilham_gmail
2026-06-11 20:38:28 +07:00
parent cbbd683470
commit b6f150ef86
5 changed files with 219 additions and 49 deletions
+38 -8
View File
@@ -73,18 +73,48 @@ if(!in_array($mime, $allowedMimes) || !in_array($ext, $allowedExts)){
exit;
}
// Verify code matches what was sent (simple check: hash it and compare)
// In production, store code server-side with TTL
$codeHash = hash('sha256', $verify_code);
// For now, we'll do a simple verification - just accept it
// In production: look up in cache/db, check expiry, mark used
// For MVP: accept any 6-digit code (since we sent it ourselves)
if(!preg_match('/^\d{6}$/', $verify_code)){
http_response_code(400);
echo json_encode(['success'=>false,'error'=>'invalid_verify_code_format']);
echo json_encode(['success'=>false,'error'=>'Format kode verifikasi salah (harus 6 digit)']);
exit;
}
// Verify code against database
$inputHash = hash('sha256', $verify_code);
$vStmt = $conn->prepare("SELECT code_hash, expires_at FROM verification_attempts WHERE email = ?");
$vStmt->bind_param('s', $email);
$vStmt->execute();
$vRes = $vStmt->get_result();
$vRow = $vRes ? $vRes->fetch_assoc() : null;
$vStmt->close();
if(!$vRow || !$vRow['code_hash']){
http_response_code(400);
echo json_encode(['success'=>false,'error'=>'Kode verifikasi belum dikirim atau telah kedaluwarsa']);
exit;
}
$dbHash = $vRow['code_hash'];
$expiry = strtotime($vRow['expires_at']);
if(time() > $expiry){
http_response_code(400);
echo json_encode(['success'=>false,'error'=>'Kode verifikasi telah kedaluwarsa']);
exit;
}
if(!hash_equals($dbHash, $inputHash)){
http_response_code(400);
echo json_encode(['success'=>false,'error'=>'Kode verifikasi tidak cocok']);
exit;
}
// Code matches! Clear the verification code hash to prevent reuse
$vClear = $conn->prepare("UPDATE verification_attempts SET code_hash = NULL, expires_at = NULL WHERE email = ?");
$vClear->bind_param('s', $email);
$vClear->execute();
$vClear->close();
// validate email and password
if(!filter_var($email, FILTER_VALIDATE_EMAIL)){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'invalid_email']); exit; }
if(strlen($password) < 8){ http_response_code(400); echo json_encode(['success'=>false,'error'=>'password_too_short']); exit; }
@@ -120,7 +150,7 @@ if(!$res){ http_response_code(500); echo json_encode(['success'=>false,'error'=>
$orgProofPath = null;
if(isset($_FILES['org_proof']) && $_FILES['org_proof']['error'] === UPLOAD_ERR_OK){
$file = $_FILES['org_proof'];
$uDir = __DIR__ . '/../../uploads/org_proofs';
$uDir = __DIR__ . '/../../../uploads/org_proofs';
if(!is_dir($uDir)) { @mkdir($uDir, 0755, true); @chmod($uDir, 0755); }
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$safeName = preg_replace('/[^a-zA-Z0-9._-]/','_', basename($file['name']));
@@ -0,0 +1,103 @@
<?php
include __DIR__ . '/../../config/koneksi.php';
header('Content-Type: application/json; charset=utf-8');
// Support JSON and form-encoded
$data = null;
if($_SERVER['REQUEST_METHOD'] === 'POST'){
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
if(strpos($contentType, 'application/json') !== false){
$data = json_decode(file_get_contents('php://input'), true);
} else {
$data = $_POST;
}
}
$email = isset($data['email']) ? strtolower(trim($data['email'])) : null;
$verify_code = isset($data['verify_code']) ? trim($data['verify_code']) : null;
$password = isset($data['password']) ? $data['password'] : null;
if(!$email || !filter_var($email, FILTER_VALIDATE_EMAIL)){
http_response_code(400);
echo json_encode(['success'=>false,'error'=>'Email tidak valid']);
exit;
}
if(!$verify_code || !preg_match('/^\d{6}$/', $verify_code)){
http_response_code(400);
echo json_encode(['success'=>false,'error'=>'Format kode verifikasi salah (harus 6 digit)']);
exit;
}
if(!$password || strlen($password) < 8){
http_response_code(400);
echo json_encode(['success'=>false,'error'=>'Password minimal harus 8 karakter']);
exit;
}
// 1. Verify user exists
$st = $conn->prepare('SELECT id FROM users WHERE email = ? LIMIT 1');
if($st){
$st->bind_param('s', $email);
$st->execute();
$r = $st->get_result();
if(!$r || !$r->fetch_assoc()){
http_response_code(404);
echo json_encode(['success'=>false,'error'=>'Email tidak terdaftar sebagai pengelola']);
exit;
}
$st->close();
}
// 2. Verify code against database
$inputHash = hash('sha256', $verify_code);
$vStmt = $conn->prepare("SELECT code_hash, expires_at FROM verification_attempts WHERE email = ?");
$vStmt->bind_param('s', $email);
$vStmt->execute();
$vRes = $vStmt->get_result();
$vRow = $vRes ? $vRes->fetch_assoc() : null;
$vStmt->close();
if(!$vRow || !$vRow['code_hash']){
http_response_code(400);
echo json_encode(['success'=>false,'error'=>'Kode verifikasi belum dikirim atau telah kedaluwarsa']);
exit;
}
$dbHash = $vRow['code_hash'];
$expiry = strtotime($vRow['expires_at']);
if(time() > $expiry){
http_response_code(400);
echo json_encode(['success'=>false,'error'=>'Kode verifikasi telah kedaluwarsa']);
exit;
}
if(!hash_equals($dbHash, $inputHash)){
http_response_code(400);
echo json_encode(['success'=>false,'error'=>'Kode verifikasi tidak cocok']);
exit;
}
// 3. Clear code hash to prevent reuse
$vClear = $conn->prepare("UPDATE verification_attempts SET code_hash = NULL, expires_at = NULL WHERE email = ?");
$vClear->bind_param('s', $email);
$vClear->execute();
$vClear->close();
// 4. Update password
$hash = password_hash($password, PASSWORD_DEFAULT);
$up = $conn->prepare('UPDATE users SET password_hash = ? WHERE email = ?');
if($up){
$up->bind_param('ss', $hash, $email);
$res = $up->execute();
$up->close();
if($res){
echo json_encode(['success'=>true,'message'=>'Password berhasil diperbarui']);
exit;
}
}
http_response_code(500);
echo json_encode(['success'=>false,'error'=>'Gagal memperbarui password di database']);
?>
@@ -30,6 +30,8 @@ try {
last_attempt_at DATETIME NOT NULL,
locked_until DATETIME NULL
)");
$conn->query("ALTER TABLE verification_attempts ADD COLUMN IF NOT EXISTS code_hash VARCHAR(128) NULL");
$conn->query("ALTER TABLE verification_attempts ADD COLUMN IF NOT EXISTS expires_at DATETIME NULL");
} catch (Exception $e) {}
$nowStr = date('Y-m-d H:i:s');
@@ -77,6 +79,7 @@ if($row){
// Generate 6-digit code
$verificationCode = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
$codeHash = hash('sha256', $verificationCode);
$expiresAt = date('Y-m-d H:i:s', $nowTime + 15 * 60); // 15 minutes expiry
$mailSuccess = false;
try{
@@ -92,15 +95,15 @@ if($row){
$newAttempts = ($row['locked_until'] && $nowTime >= strtotime($row['locked_until'])) ? 1 : ($row['attempts'] + 1);
$newLockedUntil = ($newAttempts >= 3) ? date('Y-m-d H:i:s', $nowTime + 24 * 3600) : null;
$up = $conn->prepare("UPDATE verification_attempts SET attempts = ?, last_attempt_at = ?, locked_until = ? WHERE email = ?");
$up->bind_param('isss', $newAttempts, $nowStr, $newLockedUntil, $email);
$up = $conn->prepare("UPDATE verification_attempts SET attempts = ?, last_attempt_at = ?, locked_until = ?, code_hash = ?, expires_at = ? WHERE email = ?");
$up->bind_param('isssss', $newAttempts, $nowStr, $newLockedUntil, $codeHash, $expiresAt, $email);
$up->execute();
$up->close();
} else {
$one = 1;
$nullVal = null;
$ins = $conn->prepare("INSERT INTO verification_attempts (email, attempts, last_attempt_at, locked_until) VALUES (?, ?, ?, ?)");
$ins->bind_param('siss', $email, $one, $nowStr, $nullVal);
$ins = $conn->prepare("INSERT INTO verification_attempts (email, attempts, last_attempt_at, locked_until, code_hash, expires_at) VALUES (?, ?, ?, ?, ?, ?)");
$ins->bind_param('sissss', $email, $one, $nowStr, $nullVal, $codeHash, $expiresAt);
$ins->execute();
$ins->close();
}