Fix admin organization proof image preview path and implement forgot password feature with rate limiting and check email warning
This commit is contained in:
+7
-12
@@ -1,18 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WebGIS Portal - SIG UAS</title>
|
||||
<title>Tugas Akhir SIG</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>WebGIS Portal - Tugas Akhir SIG</h1>
|
||||
<p>Selamat datang di portal integrasi aplikasi Sistem Informasi Geografis (SIG) untuk UAS.</p>
|
||||
|
||||
<body>
|
||||
<h1>Tugas Akhir SIG</h1>
|
||||
<h2>Daftar Aplikasi:</h2>
|
||||
<ul>
|
||||
<li><a href="/poverty-mapping/">Aplikasi Poverty Mapping (Project Utama)</a></li>
|
||||
<li><a href="/tugas-spbu/">Aplikasi Tugas SPBU & Tata Ruang</a></li>
|
||||
<li><a href="/tugas-spbu/">Aplikasi Tugas SPBU</a></li>
|
||||
</ul>
|
||||
|
||||
<h2>Akun Demo Penilaian:</h2>
|
||||
@@ -22,7 +22,7 @@
|
||||
<th>Aplikasi</th>
|
||||
<th>Role</th>
|
||||
<th>Kredensial / Email</th>
|
||||
<th>Password / Token</th>
|
||||
<th>Password</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -38,12 +38,6 @@
|
||||
<td>manager@example.com</td>
|
||||
<td>password</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tugas SPBU</td>
|
||||
<td>Admin SPBU</td>
|
||||
<td>Input API Key di UI</td>
|
||||
<td>8f3b7e6a9c4d2f1a6b8c9d0e4f7a1b2c</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -53,4 +47,5 @@
|
||||
<p>© 2026 Portal WebGIS UAS.</p>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
+50
-11
@@ -530,11 +530,27 @@ function openAuthModal(mode){
|
||||
'<div style="font-size:12px;color:#64748b;margin-top:8px">Akun akan berstatus menunggu persetujuan admin setelah pendaftaran berhasil.</div>';
|
||||
if(switchArea){ switchArea.innerHTML = '<span>Sudah punya akun? <button type="button" id="auth-switch-to-login" class="data-link" style="background:transparent;border:0;padding:0;cursor:pointer;margin-left:4px">Masuk</button></span>'; }
|
||||
if(submitBtn) submitBtn.textContent = 'Daftar';
|
||||
} else {
|
||||
} else if(mode === 'login'){
|
||||
title.textContent = 'Masuk';
|
||||
fields.innerHTML = '<div class="auth-field"><label>Email<input name="email" type="email" required></label></div><div class="auth-field"><label>Password<input name="password" type="password" required></label></div>';
|
||||
fields.innerHTML = '<div class="auth-field"><label><span class="label-title">Email</span><input name="email" type="email" required></label></div>' +
|
||||
'<div class="auth-field"><label><span class="label-title">Password</span><input name="password" type="password" required></label></div>' +
|
||||
'<div style="text-align:right;margin-top:-6px;margin-bottom:12px">' +
|
||||
'<button type="button" id="auth-forgot-password-link" style="background:transparent;border:0;padding:0;color:#3b82f6;cursor:pointer;font-size:13px">Lupa Password?</button>' +
|
||||
'</div>';
|
||||
if(switchArea){ switchArea.innerHTML = '<span>Ingin daftar pengelola? <button type="button" id="auth-switch-to-register" class="data-link" style="background:transparent;border:0;padding:0;cursor:pointer;margin-left:4px">Daftar di sini</button></span>'; }
|
||||
if(submitBtn) submitBtn.textContent = 'Masuk';
|
||||
} else if(mode === 'forgot_password'){
|
||||
title.textContent = 'Lupa Password';
|
||||
fields.innerHTML =
|
||||
'<div class="auth-field"><label><span class="label-title">Email <span class="required-star">*</span></span><input name="email" type="email" required></label></div>'+
|
||||
'<div style="display:flex;gap:8px;margin-top:12px;margin-bottom:12px">' +
|
||||
'<button type="button" id="auth-send-forgot-code-btn" class="btn" style="flex:1;padding:10px;background:#3b82f6;color:white;border:none;border-radius:4px;cursor:pointer;font-size:14px">Kirim Kode Verifikasi</button>' +
|
||||
'</div>' +
|
||||
'<div class="auth-field"><label><span class="label-title">Kode Verifikasi <span class="required-star">*</span></span><input name="verify_code" type="text" placeholder="Masukkan 6 digit" maxlength="6" pattern="[0-9]{6}" required></label></div>'+
|
||||
'<div class="auth-field"><label><span class="label-title">Password Baru <span class="required-star">*</span></span><input name="password" type="password" required></label></div>'+
|
||||
'<div class="auth-field"><label><span class="label-title">Konfirmasi Password Baru <span class="required-star">*</span></span><input name="password_confirm" type="password" required></label></div>';
|
||||
if(switchArea){ switchArea.innerHTML = '<span>Kembali ke <button type="button" id="auth-switch-to-login" class="data-link" style="background:transparent;border:0;padding:0;cursor:pointer;margin-left:4px">Masuk</button></span>'; }
|
||||
if(submitBtn) submitBtn.textContent = 'Reset Password';
|
||||
}
|
||||
modal.classList.remove('hidden');
|
||||
modal.setAttribute('aria-hidden','false');
|
||||
@@ -542,8 +558,10 @@ function openAuthModal(mode){
|
||||
try{
|
||||
const switchToRegister = document.getElementById('auth-switch-to-register');
|
||||
const switchToLogin = document.getElementById('auth-switch-to-login');
|
||||
const forgotPasswordLink = document.getElementById('auth-forgot-password-link');
|
||||
if(switchToRegister) switchToRegister.onclick = function(){ openAuthModal('register'); };
|
||||
if(switchToLogin) switchToLogin.onclick = function(){ openAuthModal('login'); };
|
||||
if(forgotPasswordLink) forgotPasswordLink.onclick = function(){ openAuthModal('forgot_password'); };
|
||||
}catch(e){}
|
||||
if(mode === 'register'){
|
||||
const orgProof = document.getElementById('org_proof_input');
|
||||
@@ -567,15 +585,16 @@ function openAuthModal(mode){
|
||||
});
|
||||
}
|
||||
}
|
||||
const form = document.getElementById('auth-modal-form');
|
||||
form.onsubmit = async function(ev){
|
||||
ev.preventDefault();
|
||||
|
||||
// Regular form submission (register or login)
|
||||
const endpoint = mode === 'register' ? 'src/api/auth/register.php' : 'src/api/auth/login.php';
|
||||
let endpoint = 'src/api/auth/login.php';
|
||||
if(mode === 'register') endpoint = 'src/api/auth/register.php';
|
||||
if(mode === 'forgot_password') endpoint = 'src/api/auth/reset_password.php';
|
||||
|
||||
try{
|
||||
let resp, jr;
|
||||
if(mode === 'register'){
|
||||
if(mode === 'register' || mode === 'forgot_password'){
|
||||
// Validate password length and match
|
||||
const passwordInput = form.querySelector('input[name="password"]');
|
||||
const password = passwordInput ? passwordInput.value : '';
|
||||
@@ -591,6 +610,7 @@ function openAuthModal(mode){
|
||||
return;
|
||||
}
|
||||
|
||||
if(mode === 'register'){
|
||||
// client-side validation for org_proof: size <=5MB and allowed extensions
|
||||
const fileInput = form.querySelector('input[name="org_proof"]') || document.getElementById('org_proof_input');
|
||||
if(!fileInput){ showToast('Input bukti organisasi tidak ditemukan', 'error', 4000); return; }
|
||||
@@ -601,6 +621,7 @@ function openAuthModal(mode){
|
||||
const ext = (f.name || '').split('.').pop().toLowerCase();
|
||||
if(f.size > maxSize){ showToast('Ukuran file bukti organisasi maksimal 5MB', 'error', 5000); return; }
|
||||
if(!allowed.includes(ext)){ showToast('Jenis file tidak diperbolehkan. Hanya .png, .jpg, .jpeg, .svg, atau .pdf.', 'error', 5000); return; }
|
||||
}
|
||||
|
||||
// Check verify code
|
||||
const codeInput = form.querySelector('input[name="verify_code"]');
|
||||
@@ -610,6 +631,7 @@ function openAuthModal(mode){
|
||||
return;
|
||||
}
|
||||
|
||||
if(mode === 'register'){
|
||||
const fd = new FormData(form);
|
||||
resp = await fetch(endpoint, { method: 'POST', headers: buildHeaders(null), body: fd, credentials: 'same-origin' });
|
||||
jr = await resp.json().catch(()=>null);
|
||||
@@ -620,14 +642,25 @@ function openAuthModal(mode){
|
||||
resp = await fetch(endpoint, { method: 'POST', headers: buildHeaders(), body: JSON.stringify(payload), credentials: 'same-origin' });
|
||||
jr = await resp.json().catch(()=>null);
|
||||
}
|
||||
} else {
|
||||
const fd = new FormData(form);
|
||||
const payload = {};
|
||||
for(const [k,v] of fd.entries()) payload[k] = v;
|
||||
resp = await fetch(endpoint, { method: 'POST', headers: buildHeaders(), body: JSON.stringify(payload), credentials: 'same-origin' });
|
||||
jr = await resp.json().catch(()=>null);
|
||||
}
|
||||
if(!resp.ok || !jr || jr.success === false){
|
||||
showToast((jr && (jr.error || jr.status)) ? (jr.error || jr.status) : 'Gagal memproses respons dari server', 'error', 5000);
|
||||
showToast((jr && (jr.error || jr.status || jr.message)) ? (jr.error || jr.status || jr.message) : 'Gagal memproses respons dari server', 'error', 5000);
|
||||
return;
|
||||
}
|
||||
if(mode === 'register'){
|
||||
authRegisterState = { code_sent: false, code: null, email: null };
|
||||
closeAuthModal();
|
||||
showToast('Pendaftaran berhasil! Akun menunggu persetujuan admin.', 'success', 5000);
|
||||
} else if(mode === 'forgot_password'){
|
||||
authRegisterState = { code_sent: false, code: null, email: null };
|
||||
closeAuthModal();
|
||||
showToast('Reset password berhasil! Silakan masuk kembali.', 'success', 5000);
|
||||
} else {
|
||||
if(jr && jr.token){ syncAuthToken(jr.token); }
|
||||
if(jr && jr.user){ syncCurrentUser(jr.user); }
|
||||
@@ -639,9 +672,9 @@ function openAuthModal(mode){
|
||||
}catch(e){ console.error(e); showToast('Gagal memproses autentikasi', 'error', 5000); }
|
||||
};
|
||||
|
||||
// Setup event listeners for registration "Send Verification Code" button
|
||||
if(mode === 'register'){
|
||||
const sendCodeBtn = document.getElementById('auth-send-code-btn');
|
||||
// Setup event listeners for registration and forgot password "Send Verification Code" buttons
|
||||
const wireVerificationBtn = function(btnId) {
|
||||
const sendCodeBtn = document.getElementById(btnId);
|
||||
if(sendCodeBtn){
|
||||
sendCodeBtn.onclick = async function(ev){
|
||||
ev.preventDefault();
|
||||
@@ -724,7 +757,7 @@ function openAuthModal(mode){
|
||||
}
|
||||
}
|
||||
|
||||
showToast('Kode verifikasi dikirim ke email Anda!', 'success', 4000);
|
||||
showToast('Kode verifikasi terkirim ke email Anda! Silakan cek email Anda.', 'success', 6000);
|
||||
|
||||
// Cooldown countdown timer
|
||||
let cooldown = 30;
|
||||
@@ -749,6 +782,12 @@ function openAuthModal(mode){
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
if(mode === 'register'){
|
||||
wireVerificationBtn('auth-send-code-btn');
|
||||
} else if(mode === 'forgot_password'){
|
||||
wireVerificationBtn('auth-send-forgot-code-btn');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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