Fix admin organization proof image preview path and implement forgot password feature with rate limiting and check email warning
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user