diff --git a/landing-page/index.html b/landing-page/index.html index 89cb884..2b5c2fc 100644 --- a/landing-page/index.html +++ b/landing-page/index.html @@ -1,18 +1,18 @@ + - WebGIS Portal - SIG UAS + Tugas Akhir SIG + -

WebGIS Portal - Tugas Akhir SIG

-

Selamat datang di portal integrasi aplikasi Sistem Informasi Geografis (SIG) untuk UAS.

- +

Tugas Akhir SIG

Daftar Aplikasi:

Akun Demo Penilaian:

@@ -22,7 +22,7 @@ Aplikasi Role Kredensial / Email - Password / Token + Password @@ -38,12 +38,6 @@ manager@example.com password - - Tugas SPBU - Admin SPBU - Input API Key di UI - 8f3b7e6a9c4d2f1a6b8c9d0e4f7a1b2c - @@ -53,4 +47,5 @@

© 2026 Portal WebGIS UAS.

- + + \ No newline at end of file diff --git a/poverty-mapping/index.html b/poverty-mapping/index.html index 9c42d13..94b9fe4 100644 --- a/poverty-mapping/index.html +++ b/poverty-mapping/index.html @@ -530,11 +530,27 @@ function openAuthModal(mode){ '
Akun akan berstatus menunggu persetujuan admin setelah pendaftaran berhasil.
'; if(switchArea){ switchArea.innerHTML = 'Sudah punya akun? '; } if(submitBtn) submitBtn.textContent = 'Daftar'; - } else { + } else if(mode === 'login'){ title.textContent = 'Masuk'; - fields.innerHTML = '
'; + fields.innerHTML = '
' + + '
' + + '
' + + '' + + '
'; if(switchArea){ switchArea.innerHTML = 'Ingin daftar pengelola? '; } if(submitBtn) submitBtn.textContent = 'Masuk'; + } else if(mode === 'forgot_password'){ + title.textContent = 'Lupa Password'; + fields.innerHTML = + '
'+ + '
' + + '' + + '
' + + '
'+ + '
'+ + '
'; + if(switchArea){ switchArea.innerHTML = 'Kembali ke '; } + 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,16 +610,18 @@ function openAuthModal(mode){ return; } - // 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; } - if(!(fileInput.files && fileInput.files.length > 0)){ showToast('Mohon unggah bukti organisasi (foto/pdf).', 'error', 5000); return; } - const f = fileInput.files[0]; - const maxSize = 5 * 1024 * 1024; - const allowed = ['jpg','jpeg','png','svg','pdf']; - 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; } + 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; } + if(!(fileInput.files && fileInput.files.length > 0)){ showToast('Mohon unggah bukti organisasi (foto/pdf).', 'error', 5000); return; } + const f = fileInput.files[0]; + const maxSize = 5 * 1024 * 1024; + const allowed = ['jpg','jpeg','png','svg','pdf']; + 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,9 +631,17 @@ function openAuthModal(mode){ return; } - 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); + 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); + } 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); + } } else { const fd = new FormData(form); const payload = {}; @@ -621,13 +650,17 @@ function openAuthModal(mode){ 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'); } } diff --git a/poverty-mapping/src/api/auth/register.php b/poverty-mapping/src/api/auth/register.php index a9d2f77..135224e 100644 --- a/poverty-mapping/src/api/auth/register.php +++ b/poverty-mapping/src/api/auth/register.php @@ -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'])); diff --git a/poverty-mapping/src/api/auth/reset_password.php b/poverty-mapping/src/api/auth/reset_password.php new file mode 100644 index 0000000..83742e1 --- /dev/null +++ b/poverty-mapping/src/api/auth/reset_password.php @@ -0,0 +1,103 @@ +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']); +?> diff --git a/poverty-mapping/src/api/auth/send_verification_code.php b/poverty-mapping/src/api/auth/send_verification_code.php index 6b06387..3f9780d 100644 --- a/poverty-mapping/src/api/auth/send_verification_code.php +++ b/poverty-mapping/src/api/auth/send_verification_code.php @@ -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(); }