Compare commits
10 Commits
66da318100
...
4476152587
| Author | SHA1 | Date | |
|---|---|---|---|
| 4476152587 | |||
| 077d120586 | |||
| f59fd5a24a | |||
| 56bad098a3 | |||
| da9796755a | |||
| 4ad0081a02 | |||
| cb3e18fc64 | |||
| 4f52db834e | |||
| 1e0044c260 | |||
| 3d281d570e |
@@ -11,3 +11,6 @@
|
|||||||
.vscode/
|
.vscode/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
|
# uploaded files
|
||||||
|
**/uploads/
|
||||||
|
|||||||
+12
@@ -9,6 +9,7 @@ DROP TABLE IF EXISTS bantuan_detail;
|
|||||||
DROP TABLE IF EXISTS lokasi;
|
DROP TABLE IF EXISTS lokasi;
|
||||||
DROP TABLE IF EXISTS users;
|
DROP TABLE IF EXISTS users;
|
||||||
DROP TABLE IF EXISTS migrations;
|
DROP TABLE IF EXISTS migrations;
|
||||||
|
DROP TABLE IF EXISTS verification_attempts;
|
||||||
SET FOREIGN_KEY_CHECKS = 1;
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
|
|
||||||
CREATE TABLE users (
|
CREATE TABLE users (
|
||||||
@@ -32,6 +33,7 @@ CREATE TABLE users (
|
|||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
approved_by INT UNSIGNED DEFAULT NULL,
|
approved_by INT UNSIGNED DEFAULT NULL,
|
||||||
approved_at DATETIME DEFAULT NULL,
|
approved_at DATETIME DEFAULT NULL,
|
||||||
|
rejection_reason TEXT DEFAULT NULL,
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
UNIQUE KEY uq_users_email (email),
|
UNIQUE KEY uq_users_email (email),
|
||||||
KEY idx_users_status (status),
|
KEY idx_users_status (status),
|
||||||
@@ -40,6 +42,16 @@ CREATE TABLE users (
|
|||||||
CONSTRAINT fk_users_approved_by FOREIGN KEY (approved_by) REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
CONSTRAINT fk_users_approved_by FOREIGN KEY (approved_by) REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE verification_attempts (
|
||||||
|
email VARCHAR(255) NOT NULL,
|
||||||
|
attempts INT NOT NULL DEFAULT 0,
|
||||||
|
last_attempt_at DATETIME NOT NULL,
|
||||||
|
locked_until DATETIME DEFAULT NULL,
|
||||||
|
code_hash VARCHAR(128) DEFAULT NULL,
|
||||||
|
expires_at DATETIME DEFAULT NULL,
|
||||||
|
PRIMARY KEY (email)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
-- Insert Admin and Manager demo accounts (Password: password)
|
-- Insert Admin and Manager demo accounts (Password: password)
|
||||||
INSERT INTO users (email, password_hash, name, organization, role, status, email_verified)
|
INSERT INTO users (email, password_hash, name, organization, role, status, email_verified)
|
||||||
VALUES
|
VALUES
|
||||||
|
|||||||
+125
-48
@@ -102,8 +102,8 @@
|
|||||||
.layer-legend .legend-marker{display:inline-flex;align-items:flex-end;justify-content:center;width:16px;height:22px;flex:0 0 16px}
|
.layer-legend .legend-marker{display:inline-flex;align-items:flex-end;justify-content:center;width:16px;height:22px;flex:0 0 16px}
|
||||||
.layer-legend .legend-marker svg{display:block;width:16px;height:22px}
|
.layer-legend .legend-marker svg{display:block;width:16px;height:22px}
|
||||||
.layer-legend .legend-text{white-space:nowrap}
|
.layer-legend .legend-text{white-space:nowrap}
|
||||||
.btn-primary{background:#2563eb;color:#fff;padding:8px 12px;border:0;border-radius:8px}
|
.btn-primary{background:#2563eb;color:#fff;padding:8px 12px;border:0;border-radius:8px;cursor:pointer}
|
||||||
.btn-secondary{background:#f3f4f6;color:#111;padding:8px 12px;border:0;border-radius:8px}
|
.btn-secondary{background:#f3f4f6;color:#111;padding:8px 12px;border:0;border-radius:8px;cursor:pointer}
|
||||||
.modal-actions{margin-top:14px;text-align:right}
|
.modal-actions{margin-top:14px;text-align:right}
|
||||||
/* file name display next to file input */
|
/* file name display next to file input */
|
||||||
.file-name{display:inline-block;margin-left:8px;color:#374151;font-size:13px}
|
.file-name{display:inline-block;margin-left:8px;color:#374151;font-size:13px}
|
||||||
@@ -125,9 +125,9 @@
|
|||||||
.modal-content{background:#fff;padding:18px;border-radius:10px;min-width:320px;max-width:520px;width:420px;max-height:80vh;overflow:auto}
|
.modal-content{background:#fff;padding:18px;border-radius:10px;min-width:320px;max-width:520px;width:420px;max-height:80vh;overflow:auto}
|
||||||
.modal-content label{display:block;margin-top:8px}
|
.modal-content label{display:block;margin-top:8px}
|
||||||
.modal-actions{margin-top:12px;text-align:right}
|
.modal-actions{margin-top:12px;text-align:right}
|
||||||
.btn-primary{background:#1976d2;color:#fff;padding:8px 12px;border:0;border-radius:6px}
|
.btn-primary{background:#1976d2;color:#fff;padding:8px 12px;border:0;border-radius:6px;cursor:pointer}
|
||||||
.btn-secondary{background:#eee;padding:8px 12px;border:0;border-radius:6px}
|
.btn-secondary{background:#eee;padding:8px 12px;border:0;border-radius:6px;cursor:pointer}
|
||||||
.btn-destructive{background:#d32f2f;color:#fff;padding:8px 12px;border:0;border-radius:6px}
|
.btn-destructive{background:#d32f2f;color:#fff;padding:8px 12px;border:0;border-radius:6px;cursor:pointer}
|
||||||
.btn-destructive:hover{opacity:0.95}
|
.btn-destructive:hover{opacity:0.95}
|
||||||
.leaflet-popup-content{margin:14px 16px}
|
.leaflet-popup-content{margin:14px 16px}
|
||||||
.leaflet-popup-content-wrapper{border-radius:16px}
|
.leaflet-popup-content-wrapper{border-radius:16px}
|
||||||
@@ -241,6 +241,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="admin-reject-modal" class="modal hidden" role="dialog" aria-hidden="true" style="z-index:11000">
|
||||||
|
<div class="modal-content" style="max-width:480px;width:min(480px,90vw)">
|
||||||
|
<h3>Alasan Penolakan</h3>
|
||||||
|
<div style="font-size:13px;color:#475569;margin-bottom:12px">Silakan masukkan alasan penolakan untuk pengelola ini. Alasan akan dikirimkan ke email pendaftar.</div>
|
||||||
|
<div style="margin-bottom:16px">
|
||||||
|
<textarea id="admin-reject-reason-input" style="width:100%;height:100px;padding:8px;border:1px solid #cbd5e1;border-radius:6px;box-sizing:border-box" placeholder="Tulis alasan di sini..."></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions" style="display:flex;justify-content:flex-end;gap:8px">
|
||||||
|
<button type="button" id="admin-reject-cancel" class="btn-secondary">Batal</button>
|
||||||
|
<button type="button" id="admin-reject-submit" class="btn-destructive">Tolak Pendaftaran</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Email Verification Modal -->
|
<!-- Email Verification Modal -->
|
||||||
|
|
||||||
<!-- Image viewer modal (separate from form modal) -->
|
<!-- Image viewer modal (separate from form modal) -->
|
||||||
@@ -504,6 +518,10 @@ function openAuthModal(mode){
|
|||||||
const submitBtn = document.getElementById('auth-modal-submit');
|
const submitBtn = document.getElementById('auth-modal-submit');
|
||||||
const modal = document.getElementById('auth-modal');
|
const modal = document.getElementById('auth-modal');
|
||||||
|
|
||||||
|
if(submitBtn) {
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
// Reset registration verification state
|
// Reset registration verification state
|
||||||
if(mode === 'login'){
|
if(mode === 'login'){
|
||||||
authRegisterState = { code_sent: false, code: null, email: null };
|
authRegisterState = { code_sent: false, code: null, email: null };
|
||||||
@@ -585,14 +603,23 @@ function openAuthModal(mode){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
form.onsubmit = async function(ev){
|
const form = document.getElementById('auth-modal-form');
|
||||||
|
if(form) form.onsubmit = async function(ev){
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
|
const submitBtn = form.querySelector('button[type="submit"]') || document.getElementById('auth-modal-submit');
|
||||||
|
const originalText = submitBtn ? submitBtn.textContent : 'Kirim';
|
||||||
|
|
||||||
|
if(submitBtn) {
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.textContent = 'Memproses...';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
let endpoint = 'src/api/auth/login.php';
|
let endpoint = 'src/api/auth/login.php';
|
||||||
if(mode === 'register') endpoint = 'src/api/auth/register.php';
|
if(mode === 'register') endpoint = 'src/api/auth/register.php';
|
||||||
if(mode === 'forgot_password') endpoint = 'src/api/auth/reset_password.php';
|
if(mode === 'forgot_password') endpoint = 'src/api/auth/reset_password.php';
|
||||||
|
|
||||||
try{
|
|
||||||
let resp, jr;
|
let resp, jr;
|
||||||
if(mode === 'register' || mode === 'forgot_password'){
|
if(mode === 'register' || mode === 'forgot_password'){
|
||||||
// Validate password length and match
|
// Validate password length and match
|
||||||
@@ -603,24 +630,42 @@ function openAuthModal(mode){
|
|||||||
|
|
||||||
if(password.length < 8){
|
if(password.length < 8){
|
||||||
showToast('Password minimal harus 8 karakter', 'error', 5000);
|
showToast('Password minimal harus 8 karakter', 'error', 5000);
|
||||||
|
if(submitBtn) { submitBtn.disabled = false; submitBtn.textContent = originalText; }
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(password !== passwordConfirm){
|
if(password !== passwordConfirm){
|
||||||
showToast('Konfirmasi password tidak cocok', 'error', 5000);
|
showToast('Konfirmasi password tidak cocok', 'error', 5000);
|
||||||
|
if(submitBtn) { submitBtn.disabled = false; submitBtn.textContent = originalText; }
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(mode === 'register'){
|
if(mode === 'register'){
|
||||||
// client-side validation for org_proof: size <=5MB and allowed extensions
|
// client-side validation for org_proof: size <=5MB and allowed extensions
|
||||||
const fileInput = form.querySelector('input[name="org_proof"]') || document.getElementById('org_proof_input');
|
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){
|
||||||
if(!(fileInput.files && fileInput.files.length > 0)){ showToast('Mohon unggah bukti organisasi (foto/pdf).', 'error', 5000); return; }
|
showToast('Input bukti organisasi tidak ditemukan', 'error', 4000);
|
||||||
|
if(submitBtn) { submitBtn.disabled = false; submitBtn.textContent = originalText; }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(!(fileInput.files && fileInput.files.length > 0)){
|
||||||
|
showToast('Mohon unggah bukti organisasi (foto/pdf).', 'error', 5000);
|
||||||
|
if(submitBtn) { submitBtn.disabled = false; submitBtn.textContent = originalText; }
|
||||||
|
return;
|
||||||
|
}
|
||||||
const f = fileInput.files[0];
|
const f = fileInput.files[0];
|
||||||
const maxSize = 5 * 1024 * 1024;
|
const maxSize = 5 * 1024 * 1024;
|
||||||
const allowed = ['jpg','jpeg','png','svg','pdf'];
|
const allowed = ['jpg','jpeg','png','svg','pdf'];
|
||||||
const ext = (f.name || '').split('.').pop().toLowerCase();
|
const ext = (f.name || '').split('.').pop().toLowerCase();
|
||||||
if(f.size > maxSize){ showToast('Ukuran file bukti organisasi maksimal 5MB', 'error', 5000); return; }
|
if(f.size > maxSize){
|
||||||
if(!allowed.includes(ext)){ showToast('Jenis file tidak diperbolehkan. Hanya .png, .jpg, .jpeg, .svg, atau .pdf.', 'error', 5000); return; }
|
showToast('Ukuran file bukti organisasi maksimal 5MB', 'error', 5000);
|
||||||
|
if(submitBtn) { submitBtn.disabled = false; submitBtn.textContent = originalText; }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(!allowed.includes(ext)){
|
||||||
|
showToast('Jenis file tidak diperbolehkan. Hanya .png, .jpg, .jpeg, .svg, atau .pdf.', 'error', 5000);
|
||||||
|
if(submitBtn) { submitBtn.disabled = false; submitBtn.textContent = originalText; }
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check verify code
|
// Check verify code
|
||||||
@@ -628,6 +673,7 @@ function openAuthModal(mode){
|
|||||||
const code = codeInput ? codeInput.value.trim() : '';
|
const code = codeInput ? codeInput.value.trim() : '';
|
||||||
if(!code || code.length !== 6 || !/^\d{6}$/.test(code)){
|
if(!code || code.length !== 6 || !/^\d{6}$/.test(code)){
|
||||||
showToast('Masukkan kode verifikasi 6 digit yang valid', 'error', 4000);
|
showToast('Masukkan kode verifikasi 6 digit yang valid', 'error', 4000);
|
||||||
|
if(submitBtn) { submitBtn.disabled = false; submitBtn.textContent = originalText; }
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -651,6 +697,7 @@ function openAuthModal(mode){
|
|||||||
}
|
}
|
||||||
if(!resp.ok || !jr || jr.success === false){
|
if(!resp.ok || !jr || jr.success === false){
|
||||||
showToast((jr && (jr.error || jr.status || jr.message)) ? (jr.error || jr.status || jr.message) : '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);
|
||||||
|
if(submitBtn) { submitBtn.disabled = false; submitBtn.textContent = originalText; }
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(mode === 'register'){
|
if(mode === 'register'){
|
||||||
@@ -669,7 +716,11 @@ function openAuthModal(mode){
|
|||||||
showToast('Berhasil masuk', 'success', 3000);
|
showToast('Berhasil masuk', 'success', 3000);
|
||||||
loadData();
|
loadData();
|
||||||
}
|
}
|
||||||
}catch(e){ console.error(e); showToast('Gagal memproses autentikasi', 'error', 5000); }
|
}catch(e){
|
||||||
|
console.error(e);
|
||||||
|
showToast('Gagal memproses autentikasi', 'error', 5000);
|
||||||
|
if(submitBtn) { submitBtn.disabled = false; submitBtn.textContent = originalText; }
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Setup event listeners for registration and forgot password "Send Verification Code" buttons
|
// Setup event listeners for registration and forgot password "Send Verification Code" buttons
|
||||||
@@ -686,23 +737,15 @@ function openAuthModal(mode){
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client-side rate limiting check
|
// Client-side cooldown check
|
||||||
const limitKey = 'webgis_limit_' + btoa(email);
|
const limitKey = 'webgis_limit_' + btoa(email);
|
||||||
let limitInfo = { attempts: 0, lastTime: 0, lockedUntil: 0 };
|
let limitInfo = { lastTime: 0 };
|
||||||
try{
|
try{
|
||||||
const cached = localStorage.getItem(limitKey);
|
const cached = localStorage.getItem(limitKey);
|
||||||
if(cached) limitInfo = JSON.parse(cached);
|
if(cached) limitInfo = JSON.parse(cached);
|
||||||
}catch(e){}
|
}catch(e){}
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if(limitInfo.lockedUntil && now < limitInfo.lockedUntil){
|
|
||||||
const diff = limitInfo.lockedUntil - now;
|
|
||||||
const hours = Math.floor(diff / (3600 * 1000));
|
|
||||||
const mins = Math.ceil((diff % (3600 * 1000)) / (60 * 1000));
|
|
||||||
showToast(`Batas pengiriman kode tercapai. Anda harus menunggu ${hours} jam ${mins} menit lagi.`, 'error', 5000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(limitInfo.lastTime && now - limitInfo.lastTime < 30 * 1000){
|
if(limitInfo.lastTime && now - limitInfo.lastTime < 30 * 1000){
|
||||||
const remaining = Math.ceil((30 * 1000 - (now - limitInfo.lastTime)) / 1000);
|
const remaining = Math.ceil((30 * 1000 - (now - limitInfo.lastTime)) / 1000);
|
||||||
showToast(`Silakan tunggu ${remaining} detik sebelum mengirim kembali.`, 'error', 3000);
|
showToast(`Silakan tunggu ${remaining} detik sebelum mengirim kembali.`, 'error', 3000);
|
||||||
@@ -724,26 +767,13 @@ function openAuthModal(mode){
|
|||||||
if(!resp.ok || (jr && jr.success === false)){
|
if(!resp.ok || (jr && jr.success === false)){
|
||||||
const errMessage = jr && jr.message ? jr.message : (jr && jr.error ? jr.error : 'Gagal mengirim kode');
|
const errMessage = jr && jr.message ? jr.message : (jr && jr.error ? jr.error : 'Gagal mengirim kode');
|
||||||
showToast(errMessage, 'error', 5000);
|
showToast(errMessage, 'error', 5000);
|
||||||
|
|
||||||
// If locked on server side, sync client side state
|
|
||||||
if(jr && jr.error === 'locked_24h' && jr.locked_until){
|
|
||||||
limitInfo.lockedUntil = jr.locked_until;
|
|
||||||
try{ localStorage.setItem(limitKey, JSON.stringify(limitInfo)); }catch(e){}
|
|
||||||
}
|
|
||||||
|
|
||||||
sendCodeBtn.disabled = false;
|
sendCodeBtn.disabled = false;
|
||||||
sendCodeBtn.textContent = 'Kirim Kode Verifikasi';
|
sendCodeBtn.textContent = 'Kirim Kode Verifikasi';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update client side rate limiting on success
|
// Update client side cooldown info on success
|
||||||
limitInfo.attempts = (limitInfo.lockedUntil && now >= limitInfo.lockedUntil) ? 1 : (limitInfo.attempts + 1);
|
|
||||||
limitInfo.lastTime = now;
|
limitInfo.lastTime = now;
|
||||||
if(limitInfo.attempts >= 3){
|
|
||||||
limitInfo.lockedUntil = now + 24 * 3600 * 1000;
|
|
||||||
} else {
|
|
||||||
limitInfo.lockedUntil = 0;
|
|
||||||
}
|
|
||||||
try{ localStorage.setItem(limitKey, JSON.stringify(limitInfo)); }catch(e){}
|
try{ localStorage.setItem(limitKey, JSON.stringify(limitInfo)); }catch(e){}
|
||||||
|
|
||||||
authRegisterState.code_sent = true;
|
authRegisterState.code_sent = true;
|
||||||
@@ -755,9 +785,9 @@ function openAuthModal(mode){
|
|||||||
codeInput.value = jr.debug_code;
|
codeInput.value = jr.debug_code;
|
||||||
showToast('Gagal mengirim email. Menggunakan kode pengujian: ' + jr.debug_code, 'warning', 8000);
|
showToast('Gagal mengirim email. Menggunakan kode pengujian: ' + jr.debug_code, 'warning', 8000);
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
|
||||||
showToast('Kode verifikasi terkirim ke email Anda! Silakan cek email Anda.', 'success', 6000);
|
showToast('Kode verifikasi terkirim ke email Anda! Silakan cek email Anda.', 'success', 6000);
|
||||||
|
}
|
||||||
|
|
||||||
// Cooldown countdown timer
|
// Cooldown countdown timer
|
||||||
let cooldown = 30;
|
let cooldown = 30;
|
||||||
@@ -798,7 +828,14 @@ function closeAuthModal(){
|
|||||||
modal.setAttribute('aria-hidden','true');
|
modal.setAttribute('aria-hidden','true');
|
||||||
// Reset form states
|
// Reset form states
|
||||||
const form = document.getElementById('auth-modal-form');
|
const form = document.getElementById('auth-modal-form');
|
||||||
if(form) form.reset();
|
if(form) {
|
||||||
|
form.reset();
|
||||||
|
const submitBtn = form.querySelector('button[type="submit"]') || document.getElementById('auth-modal-submit');
|
||||||
|
if(submitBtn) {
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.textContent = 'Kirim';
|
||||||
|
}
|
||||||
|
}
|
||||||
authRegisterState = { code_sent: false, code: null, email: null };
|
authRegisterState = { code_sent: false, code: null, email: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -895,22 +932,61 @@ async function approvePendingUser(id){
|
|||||||
}catch(e){ console.error(e); showToast('Gagal menyetujui user', 'error', 4000); }
|
}catch(e){ console.error(e); showToast('Gagal menyetujui user', 'error', 4000); }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function rejectPendingUser(id){
|
function closeRejectModal(){
|
||||||
const reason = prompt("Masukkan alasan penolakan:");
|
const modal = document.getElementById('admin-reject-modal');
|
||||||
if (reason === null) return; // user cancelled
|
if(modal) {
|
||||||
const trimmedReason = reason.trim();
|
modal.classList.add('hidden');
|
||||||
if (!trimmedReason) {
|
modal.setAttribute('aria-hidden','true');
|
||||||
showToast("Alasan penolakan wajib diisi!", "error", 4000);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function rejectPendingUser(id){
|
||||||
|
const modal = document.getElementById('admin-reject-modal');
|
||||||
|
const input = document.getElementById('admin-reject-reason-input');
|
||||||
|
const submitBtn = document.getElementById('admin-reject-submit');
|
||||||
|
const cancelBtn = document.getElementById('admin-reject-cancel');
|
||||||
|
if(!modal || !input || !submitBtn || !cancelBtn) return;
|
||||||
|
|
||||||
|
input.value = '';
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.textContent = 'Tolak Pendaftaran';
|
||||||
|
|
||||||
|
cancelBtn.onclick = function(){
|
||||||
|
closeRejectModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
submitBtn.onclick = async function(){
|
||||||
|
const reason = input.value.trim();
|
||||||
|
if(!reason){
|
||||||
|
showToast('Alasan penolakan wajib diisi!', 'error', 4000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.textContent = 'Memproses...';
|
||||||
|
|
||||||
try{
|
try{
|
||||||
const resp = await fetch('src/api/admin/reject_user.php', { method: 'POST', headers: buildHeaders(), body: JSON.stringify({ id: id, reason: trimmedReason }), credentials: 'same-origin' });
|
const resp = await fetch('src/api/admin/reject_user.php', { method: 'POST', headers: buildHeaders(), body: JSON.stringify({ id: id, reason: reason }), credentials: 'same-origin' });
|
||||||
const jr = await resp.json().catch(()=>null);
|
const jr = await resp.json().catch(()=>null);
|
||||||
if(!resp.ok || !jr || !jr.success){ showToast('Gagal menolak user', 'error', 4000); return; }
|
if(!resp.ok || !jr || !jr.success){
|
||||||
|
showToast('Gagal menolak user', 'error', 4000);
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.textContent = 'Tolak Pendaftaran';
|
||||||
|
return;
|
||||||
|
}
|
||||||
showToast('User ditolak', 'success', 3000);
|
showToast('User ditolak', 'success', 3000);
|
||||||
|
closeRejectModal();
|
||||||
closePendingManagerDetail();
|
closePendingManagerDetail();
|
||||||
openAdminDashboard();
|
openAdminDashboard();
|
||||||
}catch(e){ console.error(e); showToast('Gagal menolak user', 'error', 4000); }
|
}catch(e){
|
||||||
|
console.error(e);
|
||||||
|
showToast('Gagal menolak user', 'error', 4000);
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.textContent = 'Tolak Pendaftaran';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
modal.setAttribute('aria-hidden','false');
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeAdminDashboard(){
|
function closeAdminDashboard(){
|
||||||
@@ -2388,6 +2464,7 @@ try{
|
|||||||
document.getElementById('auth-modal').addEventListener('click', function(ev){ if(ev.target === this) closeAuthModal(); });
|
document.getElementById('auth-modal').addEventListener('click', function(ev){ if(ev.target === this) closeAuthModal(); });
|
||||||
document.getElementById('admin-modal').addEventListener('click', function(ev){ if(ev.target === this) closeAdminDashboard(); });
|
document.getElementById('admin-modal').addEventListener('click', function(ev){ if(ev.target === this) closeAdminDashboard(); });
|
||||||
document.getElementById('admin-detail-modal').addEventListener('click', function(ev){ if(ev.target === this) closePendingManagerDetail(); });
|
document.getElementById('admin-detail-modal').addEventListener('click', function(ev){ if(ev.target === this) closePendingManagerDetail(); });
|
||||||
|
document.getElementById('admin-reject-modal').addEventListener('click', function(ev){ if(ev.target === this) closeRejectModal(); });
|
||||||
|
|
||||||
// Admin dashboard wiring is handled by openAdminDashboard() above.
|
// Admin dashboard wiring is handled by openAdminDashboard() above.
|
||||||
}catch(e){}
|
}catch(e){}
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ require_role('admin');
|
|||||||
|
|
||||||
// Ensure column exists
|
// Ensure column exists
|
||||||
try {
|
try {
|
||||||
$conn->query("ALTER TABLE users ADD COLUMN IF NOT EXISTS rejection_reason TEXT NULL");
|
$check = $conn->query("SHOW COLUMNS FROM users LIKE 'rejection_reason'");
|
||||||
|
if($check && $check->num_rows === 0){
|
||||||
|
$conn->query("ALTER TABLE users ADD COLUMN rejection_reason TEXT NULL");
|
||||||
|
}
|
||||||
} catch(Exception $e){}
|
} catch(Exception $e){}
|
||||||
|
|
||||||
$data = json_decode(file_get_contents('php://input'), true);
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
@@ -33,11 +36,10 @@ if($uStmt){
|
|||||||
$uStmt->close();
|
$uStmt->close();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Perform the update
|
// 2. Perform the delete
|
||||||
$st = $conn->prepare('UPDATE users SET status = "rejected", rejection_reason = ?, approved_by = ?, approved_at = NOW() WHERE id = ?');
|
$st = $conn->prepare('DELETE FROM users WHERE id = ?');
|
||||||
if(!$st){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
|
if(!$st){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
|
||||||
$adminId = intval(get_current_user_info()['id']);
|
$st->bind_param('i', $id);
|
||||||
$st->bind_param('sii', $reason, $adminId, $id);
|
|
||||||
$res = $st->execute();
|
$res = $st->execute();
|
||||||
$st->close();
|
$st->close();
|
||||||
if(!$res){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
|
if(!$res){ http_response_code(500); echo json_encode(['success'=>false,'error'=>$conn->error]); exit; }
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ require_once __DIR__ . '/../../../vendor/autoload.php';
|
|||||||
$dotenvPath = __DIR__ . '/../../../';
|
$dotenvPath = __DIR__ . '/../../../';
|
||||||
if(file_exists($dotenvPath . '.env')){
|
if(file_exists($dotenvPath . '.env')){
|
||||||
try{
|
try{
|
||||||
$dotenv = Dotenv\Dotenv::createImmutable($dotenvPath);
|
$dotenv = Dotenv\Dotenv::createUnsafeMutable($dotenvPath);
|
||||||
$dotenv->safeLoad();
|
$dotenv->safeLoad();
|
||||||
}catch(Exception $e){}
|
}catch(Exception $e){}
|
||||||
}
|
}
|
||||||
@@ -34,12 +34,21 @@ function send_mail_phpmailer($to, $subject, $body, $isHtml = false){
|
|||||||
|
|
||||||
if($smtpHost){
|
if($smtpHost){
|
||||||
$mail->isSMTP();
|
$mail->isSMTP();
|
||||||
$mail->Host = $smtpHost;
|
$mail->Host = gethostbyname($smtpHost); // Force IPv4
|
||||||
$mail->SMTPAuth = true;
|
$mail->SMTPAuth = true;
|
||||||
$mail->Username = $smtpUser;
|
$mail->Username = $smtpUser;
|
||||||
$mail->Password = $smtpPass;
|
$mail->Password = $smtpPass;
|
||||||
$mail->Port = intval($smtpPort);
|
$mail->Port = intval($smtpPort);
|
||||||
|
|
||||||
|
// Bypass SSL verification issues with IP hostname in Docker
|
||||||
|
$mail->SMTPOptions = array(
|
||||||
|
'ssl' => array(
|
||||||
|
'verify_peer' => false,
|
||||||
|
'verify_peer_name' => false,
|
||||||
|
'allow_self_signed' => true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
if(strtolower($smtpEnc) === 'ssl'){
|
if(strtolower($smtpEnc) === 'ssl'){
|
||||||
$mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
|
$mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
|
||||||
} elseif(strtolower($smtpEnc) === 'tls'){
|
} elseif(strtolower($smtpEnc) === 'tls'){
|
||||||
|
|||||||
@@ -28,10 +28,10 @@ try {
|
|||||||
email VARCHAR(255) PRIMARY KEY,
|
email VARCHAR(255) PRIMARY KEY,
|
||||||
attempts INT NOT NULL DEFAULT 0,
|
attempts INT NOT NULL DEFAULT 0,
|
||||||
last_attempt_at DATETIME NOT NULL,
|
last_attempt_at DATETIME NOT NULL,
|
||||||
locked_until DATETIME NULL
|
locked_until DATETIME NULL,
|
||||||
|
code_hash VARCHAR(128) NULL,
|
||||||
|
expires_at 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) {}
|
} catch (Exception $e) {}
|
||||||
|
|
||||||
$nowStr = date('Y-m-d H:i:s');
|
$nowStr = date('Y-m-d H:i:s');
|
||||||
@@ -92,18 +92,21 @@ try{
|
|||||||
|
|
||||||
// Update/Insert attempts on database
|
// Update/Insert attempts on database
|
||||||
if($row){
|
if($row){
|
||||||
|
$newAttempts = $row['attempts'];
|
||||||
|
if($mailSuccess){
|
||||||
$newAttempts = ($row['locked_until'] && $nowTime >= strtotime($row['locked_until'])) ? 1 : ($row['attempts'] + 1);
|
$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;
|
}
|
||||||
|
$newLockedUntil = ($newAttempts >= 3) ? date('Y-m-d H:i:s', $nowTime + 24 * 3600) : $row['locked_until'];
|
||||||
|
|
||||||
$up = $conn->prepare("UPDATE verification_attempts SET attempts = ?, last_attempt_at = ?, locked_until = ?, code_hash = ?, expires_at = ? WHERE 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->bind_param('isssss', $newAttempts, $nowStr, $newLockedUntil, $codeHash, $expiresAt, $email);
|
||||||
$up->execute();
|
$up->execute();
|
||||||
$up->close();
|
$up->close();
|
||||||
} else {
|
} else {
|
||||||
$one = 1;
|
$attemptsVal = $mailSuccess ? 1 : 0;
|
||||||
$nullVal = null;
|
$nullVal = null;
|
||||||
$ins = $conn->prepare("INSERT INTO verification_attempts (email, attempts, last_attempt_at, locked_until, code_hash, expires_at) VALUES (?, ?, ?, ?, ?, ?)");
|
$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->bind_param('sissss', $email, $attemptsVal, $nowStr, $nullVal, $codeHash, $expiresAt);
|
||||||
$ins->execute();
|
$ins->execute();
|
||||||
$ins->close();
|
$ins->close();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
ini_set('display_errors', '0');
|
||||||
|
error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
|
||||||
include_once __DIR__ . '/config.php';
|
include_once __DIR__ . '/config.php';
|
||||||
|
|
||||||
$dbHost = getenv('DB_HOST') ?: 'localhost';
|
$dbHost = getenv('DB_HOST') ?: 'localhost';
|
||||||
|
|||||||
Reference in New Issue
Block a user