752 lines
38 KiB
PHP
752 lines
38 KiB
PHP
<?php
|
|
session_start();
|
|
include 'koneksi.php';
|
|
include 'run_background.php';
|
|
|
|
$token = isset($_GET['token']) ? trim($_GET['token']) : '';
|
|
if (empty($token)) {
|
|
header("Location: 404.php");
|
|
exit;
|
|
}
|
|
|
|
// Validasi Token
|
|
$manifestPath = __DIR__ . '/manifest_forgot_tokens.json';
|
|
if (!file_exists($manifestPath)) {
|
|
header("Location: 404.php");
|
|
exit;
|
|
}
|
|
|
|
$tokens = json_decode(file_get_contents($manifestPath), true) ?: [];
|
|
if (!isset($tokens[$token])) {
|
|
header("Location: 404.php");
|
|
exit;
|
|
}
|
|
|
|
$tokenData = $tokens[$token];
|
|
if (time() > $tokenData['expires_at']) {
|
|
unset($tokens[$token]);
|
|
file_put_contents($manifestPath, json_encode($tokens, JSON_PRETTY_PRINT));
|
|
header("Location: 404.php");
|
|
exit;
|
|
}
|
|
|
|
$id_occupant = $tokenData['id_occupant'];
|
|
|
|
// Add password columns to occupant if they don't exist yet
|
|
try { $pdo->exec("ALTER TABLE occupant ADD COLUMN password VARCHAR(255) NULL AFTER no_hp"); } catch(PDOException $e) {}
|
|
try { $pdo->exec("ALTER TABLE occupant ADD COLUMN password_enc VARCHAR(255) NULL AFTER password"); } catch(PDOException $e) {}
|
|
|
|
// Ambil data occupant
|
|
$stmt = $pdo->prepare("SELECT name, password_enc FROM occupant WHERE id_occupant = ?");
|
|
$stmt->execute([$id_occupant]);
|
|
$occupant = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$occupant) {
|
|
header("Location: 404.php");
|
|
exit;
|
|
}
|
|
|
|
// Proses jika form disubmit via AJAX
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
header('Content-Type: application/json');
|
|
$action = isset($_POST['action']) ? $_POST['action'] : '';
|
|
|
|
if ($action === 'verify_pin') {
|
|
$pin = isset($_POST['pin']) ? trim($_POST['pin']) : '';
|
|
if (empty($pin)) {
|
|
echo json_encode(["status" => "error", "message" => "PIN is required."]);
|
|
exit;
|
|
}
|
|
|
|
$stmt = $pdo->prepare("SELECT pin FROM occupant WHERE id_occupant = ?");
|
|
$stmt->execute([$id_occupant]);
|
|
$data = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if ($data && $data['pin'] === $pin) {
|
|
echo json_encode(["status" => "success", "message" => "PIN Valid. Please enter a new password."]);
|
|
} else {
|
|
echo json_encode(["status" => "error", "message" => "PIN is invalid!"]);
|
|
}
|
|
exit;
|
|
}
|
|
|
|
if ($action === 'reset_password') {
|
|
$password = isset($_POST['password']) ? $_POST['password'] : '';
|
|
$confirm_password = isset($_POST['confirm_password']) ? $_POST['confirm_password'] : '';
|
|
|
|
if (empty($password) || empty($confirm_password)) {
|
|
echo json_encode(["status" => "error", "message" => "All fields are required."]);
|
|
exit;
|
|
}
|
|
if ($password !== $confirm_password) {
|
|
echo json_encode(["status" => "error", "message" => "Passwords do not match."]);
|
|
exit;
|
|
}
|
|
|
|
$password_hash = password_hash($password, PASSWORD_DEFAULT);
|
|
|
|
try {
|
|
$stmt = $pdo->prepare("UPDATE occupant SET password = ?, password_enc = ? WHERE id_occupant = ?");
|
|
if ($stmt->execute([$password_hash, $password, $id_occupant])) {
|
|
|
|
// Edit Telegram Message to disable the inline button
|
|
if (isset($tokenData['msg_id'])) {
|
|
$botToken = "8184881871:AAFOz6uzIgxE7rk3WttcKKrr0DtcNGIt-Ho";
|
|
$chatId = $occupant['telegram_id'] ?? $tokenData['id_occupant'];
|
|
// We need actual telegram_id.
|
|
$tstmt = $pdo->prepare("SELECT telegram_id FROM occupant WHERE id_occupant = ?");
|
|
$tstmt->execute([$id_occupant]);
|
|
$tdata = $tstmt->fetch(PDO::FETCH_ASSOC);
|
|
if ($tdata && $tdata['telegram_id']) {
|
|
$chatId = $tdata['telegram_id'];
|
|
@file_get_contents("https://api.telegram.org/bot$botToken/deleteMessage?chat_id=$chatId&message_id={$tokenData['msg_id']}");
|
|
if (isset($tokenData['user_msg_id'])) {
|
|
@file_get_contents("https://api.telegram.org/bot$botToken/deleteMessage?chat_id=$chatId&message_id={$tokenData['user_msg_id']}");
|
|
}
|
|
|
|
// Send success message that auto-deletes after 5 seconds with live countdown
|
|
$successLabel = "✅ Data saved successfully";
|
|
$successText = "<b>" . $successLabel . "</b>";
|
|
$successPost = ['chat_id' => $chatId, 'text' => $successText, 'parse_mode' => 'HTML'];
|
|
$res = @file_get_contents("https://api.telegram.org/bot$botToken/sendMessage?" . http_build_query($successPost));
|
|
$resData2 = json_decode($res, true);
|
|
if (isset($resData2['result']['message_id'])) {
|
|
$successMsgId = $resData2['result']['message_id'];
|
|
runBackground(__DIR__ . '/auto_delete_quick.php', [$chatId, $successMsgId, $successLabel]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Hapus token karena sudah digunakan (single-use)
|
|
unset($tokens[$token]);
|
|
file_put_contents($manifestPath, json_encode($tokens, JSON_PRETTY_PRINT));
|
|
|
|
echo json_encode(["status" => "success", "message" => "Password updated successfully!"]);
|
|
} else {
|
|
echo json_encode(["status" => "error", "message" => "Failed to save password."]);
|
|
}
|
|
} catch (PDOException $e) {
|
|
echo json_encode(["status" => "error", "message" => "Database Error: " . $e->getMessage()]);
|
|
}
|
|
exit;
|
|
}
|
|
}
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, maximum-scale=1, user-scalable=0">
|
|
<title>Verifikasi PIN - Identia</title>
|
|
<!-- CSS & Plugins (mirip forgot_password.php / manage_occupant) -->
|
|
<link rel="stylesheet" href="vendors/feather/feather.css">
|
|
<link rel="stylesheet" href="vendors/ti-icons/css/themify-icons.css">
|
|
<link rel="stylesheet" href="vendors/css/vendor.bundle.base.css">
|
|
<link rel="stylesheet" href="css/vertical-layout-light/style.css">
|
|
<link rel="stylesheet" href="vendors/mdi/css/materialdesignicons.min.css">
|
|
<style>
|
|
.auth .brand-logo img { width: 140px; max-width: 100%; }
|
|
.auth-form-light {
|
|
background: #ffffff;
|
|
border-radius: 24px;
|
|
padding: 40px 30px;
|
|
text-align: center;
|
|
position: relative;
|
|
z-index: 2;
|
|
margin-top: -30px;
|
|
border: 1px solid rgba(226, 232, 240, 0.8);
|
|
}
|
|
.pin-input-group {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: 10px;
|
|
margin-bottom: 5px;
|
|
}
|
|
.pin-box {
|
|
width: 45px;
|
|
height: 55px;
|
|
text-align: center;
|
|
font-size: 24px;
|
|
border: none;
|
|
border-bottom: 2px solid #ccc;
|
|
background: transparent;
|
|
outline: none;
|
|
transition: border-color 0.3s;
|
|
}
|
|
.pin-box:focus { border-bottom-color: #4B49AC; }
|
|
.custom-alert {
|
|
position: fixed; top: 20px; right: 20px; background: #fff;
|
|
border-left: 4px solid #fcd34d;
|
|
padding: 15px 20px; border-radius: 4px; z-index: 9999;
|
|
display: none; align-items: center; gap: 12px; min-width: 300px;
|
|
max-width: 400px; animation: slideIn 0.3s ease-out;
|
|
}
|
|
.custom-alert.error { border-left-color: #ef4444; }
|
|
.custom-alert.success { border-left-color: #10b981; }
|
|
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
|
|
@keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }
|
|
.custom-alert-icon { font-size: 24px; }
|
|
.custom-alert.error .custom-alert-icon { color: #ef4444; }
|
|
.custom-alert.success .custom-alert-icon { color: #10b981; }
|
|
.custom-alert.warning .custom-alert-icon { color: #f59e0b; }
|
|
.custom-alert-content { flex: 1; text-align: left; }
|
|
.custom-alert-title { font-weight: bold; font-size: 14px; margin-bottom: 2px; color: #333; }
|
|
.custom-alert-msg { font-size: 13px; color: #666; line-height: 1.4; }
|
|
.custom-alert-close { cursor: pointer; color: #999; font-size: 18px; }
|
|
.custom-alert-close:hover { color: #333; }
|
|
.info-alert { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 8px; display: flex; align-items: center; gap: 8px; padding: 8px 12px; font-size: 13px; color: #1e293b; margin-bottom: 1rem; }
|
|
.info-alert-icon { width: 22px; height: 22px; border-radius: 50%; background: #1d4ed8; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
|
.info-alert-icon i { color: #e0ecff; font-size: 14px; line-height: 1; }
|
|
body, .container-scroller, .page-body-wrapper, .content-wrapper { background-color: #f8f9fa !important; }
|
|
@media (max-width: 480px) {
|
|
.custom-alert { top: 10px; right: 10px; left: 10px; min-width: unset; max-width: unset; }
|
|
.pin-box { width: 40px; height: 50px; font-size: 20px; }
|
|
}
|
|
.success-checkmark { text-align: center; padding: 30px 10px; animation: scaleIn 0.5s ease; }
|
|
@keyframes scaleIn { 0% { transform: scale(0.5); opacity: 0; } 50% { transform: scale(1.1); } 100% { transform: scale(1); opacity: 1; } }
|
|
.checkmark-circle { width: 80px; height: 80px; border-radius: 50%; background: #10b981; display: inline-flex; justify-content: center; align-items: center; margin-bottom: 20px; margin-left: auto; margin-right: auto; }
|
|
.checkmark-circle i { color: white; font-size: 40px; line-height: 1; }
|
|
/* Session Timer Pill */
|
|
.session-timer-pill {
|
|
position: fixed; top: 12px; right: 12px; z-index: 10000;
|
|
display: flex; align-items: center; gap: 6px;
|
|
padding: 6px 14px; border-radius: 30px;
|
|
background: rgba(255,255,255,0.95); border: 1px solid #e2e8f0;
|
|
font-size: 12px; font-weight: 700; color: #64748b;
|
|
backdrop-filter: blur(8px); box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
|
transition: all 0.3s ease;
|
|
}
|
|
.session-timer-pill i { font-size: 16px; color: #f59e0b; }
|
|
.session-timer-pill.urgent { background: #fef2f2; border-color: #fecaca; color: #dc2626; }
|
|
.session-timer-pill.urgent i { color: #dc2626; }
|
|
/* Session Expired Overlay */
|
|
.session-expired-overlay {
|
|
display: none; position: fixed; inset: 0; z-index: 99999;
|
|
background: #f8f9fa;
|
|
flex-direction: column; align-items: center; justify-content: center;
|
|
text-align: center; padding: 40px 30px;
|
|
animation: fadeInOverlay 0.4s ease;
|
|
}
|
|
@keyframes fadeInOverlay { from { opacity: 0; } to { opacity: 1; } }
|
|
.expired-icon {
|
|
width: 80px; height: 80px; border-radius: 50%;
|
|
background: #fef2f2; border: 1px solid #fecaca;
|
|
display: flex; align-items: center; justify-content: center;
|
|
margin-bottom: 20px;
|
|
}
|
|
.expired-icon i { font-size: 40px; color: #ef4444; }
|
|
.session-expired-overlay h3 { font-size: 22px; font-weight: 800; color: #1e293b; margin-bottom: 8px; }
|
|
.session-expired-overlay p { font-size: 14px; color: #64748b; line-height: 1.6; max-width: 300px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container-scroller">
|
|
<div class="container-fluid page-body-wrapper full-page-wrapper">
|
|
<div class="content-wrapper d-flex align-items-center auth px-0">
|
|
<div class="row w-100 mx-0">
|
|
<div class="col-lg-4 mx-auto">
|
|
<div class="auth-form-light text-left py-5 px-4 px-sm-5">
|
|
<div class="brand-logo text-center mb-4">
|
|
<img src="images/ko.png" alt="logo" style="width: 180px;">
|
|
</div>
|
|
|
|
<div id="inCardAlert" style="display: none; background: #fef2f2; border: 1px solid #fecaca; border-radius: 8px; padding: 10px 14px; margin-bottom: 16px; animation: slideIn 0.3s ease-out;">
|
|
<div style="display: flex; align-items: center; gap: 8px;">
|
|
<i class="mdi mdi-close-circle" style="color: #ef4444; font-size: 20px; flex-shrink: 0;"></i>
|
|
<span id="inCardAlertMsg" style="font-size: 13px; color: #991b1b; font-weight: 500;"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="step1-verify">
|
|
<h4 class="font-weight-bold text-center mb-2">Identity Verification</h4>
|
|
<p class="font-weight-light text-center text-muted mb-4" style="font-size: 14px;">Enter the 6-Digit PIN for the account <br><strong><?= htmlspecialchars($occupant['name']) ?></strong></p>
|
|
|
|
<form id="verifyForm">
|
|
<input type="hidden" name="action" value="verify_pin">
|
|
<input type="hidden" name="pin" id="fullPin" value="">
|
|
|
|
<div class="form-group mb-4">
|
|
<div class="pin-input-group" id="pinContainer">
|
|
<input type="tel" class="pin-box" maxlength="1" pattern="[0-9]*" inputmode="numeric" id="pin1" autocomplete="off" autofocus>
|
|
<input type="tel" class="pin-box" maxlength="1" pattern="[0-9]*" inputmode="numeric" id="pin2" autocomplete="off" disabled>
|
|
<input type="tel" class="pin-box" maxlength="1" pattern="[0-9]*" inputmode="numeric" id="pin3" autocomplete="off" disabled>
|
|
<input type="tel" class="pin-box" maxlength="1" pattern="[0-9]*" inputmode="numeric" id="pin4" autocomplete="off" disabled>
|
|
<input type="tel" class="pin-box" maxlength="1" pattern="[0-9]*" inputmode="numeric" id="pin5" autocomplete="off" disabled>
|
|
<input type="tel" class="pin-box" maxlength="1" pattern="[0-9]*" inputmode="numeric" id="pin6" autocomplete="off" disabled>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4">
|
|
<button type="submit" id="btnVerify" class="btn btn-block btn-primary btn-lg font-weight-medium auth-form-btn" style="border-radius: 8px;">VERIFY PIN</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div id="step2-password" style="display: none;">
|
|
<h4 class="font-weight-bold text-center mb-2">New Password</h4>
|
|
<p class="font-weight-light text-center text-muted mb-4" style="font-size: 14px;">Enter the new password for your account</p>
|
|
|
|
<!-- Old Password Display -->
|
|
<div class="info-alert mb-4" style="background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 10px; padding: 14px 16px; display: block;">
|
|
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
|
<div class="info-alert-icon" style="background: #1d4ed8; width: 22px; height: 22px; border-radius: 50%; display: flex; align-items: center; justify-content: center;"><i class="mdi mdi-lock-open-outline" style="color: #e0ecff; font-size: 14px;"></i></div>
|
|
<span style="font-weight: 700; font-size: 13px; color: #1e2223;">Current Password</span>
|
|
</div>
|
|
<p style="font-size: 11px; color: #1e2223; margin-bottom: 10px; font-weight: 400; line-height: 1.4;">Your old password is shown below. Please enter and confirm your new password to update it.</p>
|
|
<div style="background: #fff; border: 1px solid #e5e7eb; border-radius: 8px; padding: 10px 14px; font-family: 'Courier New', monospace; font-size: 15px; font-weight: 600; color: #1e2223; letter-spacing: 1px; word-break: break-all;" id="oldPasswordDisplay">
|
|
<?= htmlspecialchars($occupant['password_enc'] ?? '(not available)') ?>
|
|
</div>
|
|
</div>
|
|
|
|
<form id="passwordForm">
|
|
<input type="hidden" name="action" value="reset_password">
|
|
|
|
<div class="form-group mb-3">
|
|
<div class="input-group">
|
|
<div class="input-group-prepend">
|
|
<span class="input-group-text bg-transparent border-right-0" style="border-radius: 8px 0 0 8px;">
|
|
|
|
</span>
|
|
</div>
|
|
<input type="password" class="form-control form-control-lg border-left-0 border-right-0" name="password" id="newPassword" placeholder="New Password" required>
|
|
<div class="input-group-append">
|
|
<span class="input-group-text bg-transparent border-left-0 eye-toggle" data-target="newPassword" style="border-radius: 0 8px 8px 0; cursor: pointer;">
|
|
<i class="mdi mdi-eye-off text-muted"></i>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="progress mt-1" style="height: 5px; display: none;" id="pwStrengthMeterReg">
|
|
<div class="progress-bar" id="pwStrengthBarReg" role="progressbar" style="width: 0%;"></div>
|
|
</div>
|
|
<small id="pwStrengthTextReg" class="form-text mt-1 mb-1 font-weight-bold" style="font-size: 11px;"></small>
|
|
<small class="text-muted d-block mt-0" style="font-size: 10px; line-height: 1.2;">*Min. 8 characters, uppercase, lowercase, numbers & symbols.</small>
|
|
</div>
|
|
|
|
<div class="form-group mb-4">
|
|
<div class="input-group">
|
|
<div class="input-group-prepend">
|
|
<span class="input-group-text bg-transparent border-right-0" style="border-radius: 8px 0 0 8px;">
|
|
|
|
</span>
|
|
</div>
|
|
<input type="password" class="form-control form-control-lg border-left-0 border-right-0" name="confirm_password" id="confirmPassword" placeholder="Confirm Password" required>
|
|
<div class="input-group-append">
|
|
<span class="input-group-text bg-transparent border-left-0 eye-toggle" data-target="confirmPassword" style="border-radius: 0 8px 8px 0; cursor: pointer;">
|
|
<i class="mdi mdi-eye-off text-muted"></i>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4">
|
|
<button type="submit" id="btnUpdate" class="btn btn-block btn-primary btn-lg font-weight-medium auth-form-btn" style="border-radius: 8px;">SAVE PASSWORD</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div id="step3-success" class="success-checkmark" style="display: none;">
|
|
<div class="checkmark-circle">
|
|
<i class="mdi mdi-check"></i>
|
|
</div>
|
|
<h4 class="font-weight-bold text-success mb-2">Success!</h4>
|
|
<p class="text-muted" style="font-size: 14px;">Your password has been updated.</p>
|
|
<hr class="my-4">
|
|
<p class="text-muted mb-4" style="font-size: 13px;">This page will automatically close in <span id="countdownText" class="font-weight-bold text-primary">10</span> seconds to keep your session secure.</p>
|
|
<button type="button" onclick="Telegram.WebApp.close(); window.close()" class="btn btn-block btn-outline-primary fw-bold" style="border-radius: 8px;">Close Now</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Custom Alert Container -->
|
|
<div id="customAlert" class="custom-alert warning" style="display: none;">
|
|
<i id="customAlertIcon" class="mdi mdi-alert-circle custom-alert-icon"></i>
|
|
<div class="custom-alert-content">
|
|
<div id="customAlertTitle" class="custom-alert-title">Notice</div>
|
|
<div id="customAlertMsg" class="custom-alert-msg">Warning message here.</div>
|
|
</div>
|
|
<i class="mdi mdi-close custom-alert-close" onclick="closeCustomAlert()"></i>
|
|
</div>
|
|
|
|
<!-- Session Timer Pill -->
|
|
<div class="session-timer-pill" id="sessionTimerPill">
|
|
<i class="mdi mdi-timer-outline"></i>
|
|
<span id="sessionTimerText">5:00</span>
|
|
</div>
|
|
|
|
<!-- Session Expired Overlay -->
|
|
<div class="session-expired-overlay" id="sessionExpiredOverlay">
|
|
<div class="expired-icon"><i class="mdi mdi-timer-off-outline"></i></div>
|
|
<h3>Session Expired</h3>
|
|
<p>Your 5-minute access window has ended. Please use the <b>/forgot_password</b> command in Telegram to request a new session.</p>
|
|
</div>
|
|
|
|
<script src="vendors/js/vendor.bundle.base.js"></script>
|
|
<script src="js/hoverable-collapse.js"></script>
|
|
<script src="js/template.js"></script>
|
|
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
|
<script>
|
|
// Session expiry timestamp from server
|
|
var SESSION_EXPIRES_AT = <?= (int)$tokenData['expires_at'] ?>;
|
|
|
|
// Init Telegram WebApp
|
|
window.Telegram.WebApp.ready();
|
|
window.Telegram.WebApp.expand();
|
|
|
|
let customAlertTimeout;
|
|
let closeAlertTimeout;
|
|
|
|
function showCustomAlert(title, message, type='warning') {
|
|
const alertBox = document.getElementById('customAlert');
|
|
const icon = document.getElementById('customAlertIcon');
|
|
|
|
clearTimeout(customAlertTimeout);
|
|
clearTimeout(closeAlertTimeout);
|
|
|
|
alertBox.className = 'custom-alert ' + type;
|
|
if(type === 'error') icon.className = 'mdi mdi-close-circle custom-alert-icon';
|
|
else if(type === 'success') icon.className = 'mdi mdi-check-circle custom-alert-icon';
|
|
else icon.className = 'mdi mdi-alert-circle custom-alert-icon';
|
|
|
|
document.getElementById('customAlertTitle').innerText = title;
|
|
document.getElementById('customAlertMsg').innerText = message;
|
|
|
|
alertBox.style.display = 'flex';
|
|
alertBox.style.animation = 'slideIn 0.3s ease-out';
|
|
|
|
customAlertTimeout = setTimeout(() => closeCustomAlert(), 5000);
|
|
}
|
|
|
|
function closeCustomAlert() {
|
|
const alertBox = document.getElementById('customAlert');
|
|
alertBox.style.animation = 'fadeOut 0.3s ease-out forwards';
|
|
clearTimeout(customAlertTimeout);
|
|
clearTimeout(closeAlertTimeout);
|
|
closeAlertTimeout = setTimeout(() => {
|
|
alertBox.style.display = 'none';
|
|
}, 300);
|
|
}
|
|
|
|
// Logic Input PIN (mirip di forgot_password.php)
|
|
const pinInputs = document.querySelectorAll('.pin-box');
|
|
|
|
pinInputs.forEach((input, index) => {
|
|
input.addEventListener('input', function(e) {
|
|
// Allow only numbers
|
|
this.value = this.value.replace(/[^0-9]/g, '');
|
|
|
|
if (this.value !== '') { // Jika diisi
|
|
this.classList.remove('input-error');
|
|
if (index < pinInputs.length - 1) {
|
|
pinInputs[index + 1].disabled = false;
|
|
pinInputs[index + 1].focus();
|
|
} else if (index === pinInputs.length - 1) {
|
|
// All filled, auto submit form or focus submit button
|
|
document.getElementById('btnVerify').focus();
|
|
}
|
|
}
|
|
updateFullPin();
|
|
});
|
|
|
|
input.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Backspace' && this.value === '') {
|
|
if (index > 0) {
|
|
pinInputs[index - 1].focus();
|
|
pinInputs[index - 1].value = '';
|
|
this.disabled = true; // disable current
|
|
this.classList.remove('input-error');
|
|
}
|
|
}
|
|
updateFullPin();
|
|
});
|
|
|
|
// Handle paste event
|
|
input.addEventListener('paste', function(e) {
|
|
e.preventDefault();
|
|
const pastedData = (e.clipboardData || window.clipboardData).getData('text');
|
|
const numbers = pastedData.replace(/[^0-9]/g, '').substring(0, 6);
|
|
|
|
if (numbers.length > 0) {
|
|
for (let i = 0; i < pinInputs.length; i++) {
|
|
if (i < numbers.length) {
|
|
pinInputs[i].value = numbers[i];
|
|
pinInputs[i].disabled = false;
|
|
pinInputs[i].classList.remove('input-error');
|
|
} else {
|
|
if (i > numbers.length) {
|
|
pinInputs[i].disabled = true;
|
|
}
|
|
pinInputs[i].value = '';
|
|
}
|
|
}
|
|
if (numbers.length < 6) {
|
|
pinInputs[numbers.length].disabled = false;
|
|
pinInputs[numbers.length].focus();
|
|
} else {
|
|
document.getElementById('btnVerify').focus();
|
|
}
|
|
updateFullPin();
|
|
}
|
|
});
|
|
});
|
|
|
|
function updateFullPin() {
|
|
let pinStr = '';
|
|
pinInputs.forEach(inp => { pinStr += inp.value; });
|
|
document.getElementById('fullPin').value = pinStr;
|
|
}
|
|
|
|
// Handle Submit Verify PIN
|
|
document.getElementById('verifyForm').addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
closeCustomAlert();
|
|
|
|
updateFullPin();
|
|
const pinVal = document.getElementById('fullPin').value;
|
|
|
|
if (pinVal.length < 6) {
|
|
showCustomAlert('Incomplete Form', 'Please complete all 6 digits of your PIN.', 'warning');
|
|
pinInputs.forEach(inp => {
|
|
if(inp.value === '') inp.classList.add('input-error');
|
|
});
|
|
return;
|
|
}
|
|
|
|
const btn = document.getElementById('btnVerify');
|
|
const originalText = btn.innerHTML;
|
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm mr-2" role="status" aria-hidden="true"></span>Verifying...';
|
|
btn.disabled = true;
|
|
|
|
pinInputs.forEach(inp => inp.disabled = true);
|
|
|
|
const formData = new FormData(this);
|
|
|
|
fetch('', {
|
|
method: 'POST',
|
|
body: formData
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
pinInputs.forEach(inp => inp.classList.add('input-success'));
|
|
// Hide in-card alert if visible
|
|
document.getElementById('inCardAlert').style.display = 'none';
|
|
setTimeout(() => {
|
|
document.getElementById('step1-verify').style.display = 'none';
|
|
document.getElementById('step2-password').style.display = 'block';
|
|
}, 500);
|
|
} else {
|
|
// Show error inside card
|
|
const inCardAlert = document.getElementById('inCardAlert');
|
|
document.getElementById('inCardAlertMsg').textContent = data.message || 'PIN is invalid!';
|
|
inCardAlert.style.display = 'block';
|
|
inCardAlert.style.animation = 'none';
|
|
setTimeout(() => { inCardAlert.style.animation = 'slideIn 0.3s ease-out'; }, 10);
|
|
|
|
pinInputs.forEach(inp => {
|
|
inp.classList.add('input-error');
|
|
inp.disabled = false;
|
|
});
|
|
pinInputs[0].focus();
|
|
btn.innerHTML = originalText;
|
|
btn.disabled = false;
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showCustomAlert('Network Error', 'A connection error occurred.', 'error');
|
|
btn.innerHTML = originalText;
|
|
btn.disabled = false;
|
|
pinInputs.forEach(inp => {
|
|
inp.disabled = false;
|
|
if(inp.value !== '') inp.disabled = true;
|
|
});
|
|
});
|
|
});
|
|
|
|
// Handle Submit Reset Password
|
|
document.getElementById('passwordForm').addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
closeCustomAlert();
|
|
|
|
const pwd = document.getElementById('newPassword').value;
|
|
const confirmPwd = document.getElementById('confirmPassword').value;
|
|
|
|
if(pwd === '' || confirmPwd === '') {
|
|
showCustomAlert('Incomplete Form', 'All fields are required.', 'warning');
|
|
return;
|
|
}
|
|
|
|
const pwValid = (pwd.length >= 8 && /[a-z]/.test(pwd) && /[A-Z]/.test(pwd) && /\d/.test(pwd) && /[^A-Za-z0-9]/.test(pwd));
|
|
if (!pwValid) {
|
|
showCustomAlert("Warning", "Password does not meet criteria (min. 8 chars, uppercase, lowercase, numbers, and symbols)!", "warning");
|
|
document.getElementById('newPassword').focus();
|
|
return;
|
|
}
|
|
|
|
if(pwd !== confirmPwd) {
|
|
showCustomAlert('Failed', 'Passwords do not match.', 'error');
|
|
return;
|
|
}
|
|
|
|
const btn = document.getElementById('btnUpdate');
|
|
const originalText = btn.innerHTML;
|
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm mr-2" role="status" aria-hidden="true"></span>Saving...';
|
|
btn.disabled = true;
|
|
|
|
const formData = new FormData(this);
|
|
|
|
fetch('', {
|
|
method: 'POST',
|
|
body: formData
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
document.getElementById('step2-password').style.display = 'none';
|
|
document.getElementById('step3-success').style.display = 'block';
|
|
|
|
if (window.Telegram && window.Telegram.WebApp && window.Telegram.WebApp.HapticFeedback) {
|
|
window.Telegram.WebApp.HapticFeedback.notificationOccurred('success');
|
|
}
|
|
|
|
// Auto close after 10 seconds
|
|
let count = 10;
|
|
const cText = document.getElementById('countdownText');
|
|
const cInterval = setInterval(() => {
|
|
count--;
|
|
cText.innerText = count;
|
|
if(count <= 0) {
|
|
clearInterval(cInterval);
|
|
if(window.Telegram && window.Telegram.WebApp) {
|
|
window.Telegram.WebApp.close();
|
|
}
|
|
window.close();
|
|
}
|
|
}, 1000);
|
|
|
|
} else {
|
|
showCustomAlert('Failed', data.message, 'error');
|
|
btn.innerHTML = originalText;
|
|
btn.disabled = false;
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showCustomAlert('Network Error', 'A connection error occurred.', 'error');
|
|
btn.innerHTML = originalText;
|
|
btn.disabled = false;
|
|
});
|
|
});
|
|
|
|
// Eye Toggle Logic
|
|
var toggles = document.querySelectorAll('.eye-toggle');
|
|
toggles.forEach(function(toggle) {
|
|
toggle.addEventListener('click', function() {
|
|
var targetId = this.getAttribute('data-target');
|
|
var input = document.getElementById(targetId);
|
|
var icon = this.querySelector('i');
|
|
if (input.type === 'password') {
|
|
input.type = 'text';
|
|
icon.classList.remove('mdi-eye-off');
|
|
icon.classList.add('mdi-eye');
|
|
} else {
|
|
input.type = 'password';
|
|
icon.classList.remove('mdi-eye');
|
|
icon.classList.add('mdi-eye-off');
|
|
}
|
|
});
|
|
});
|
|
|
|
// Password Strength Logic
|
|
document.getElementById('newPassword').addEventListener('input', function() {
|
|
var pass = this.value;
|
|
var meter = document.getElementById('pwStrengthMeterReg');
|
|
var bar = document.getElementById('pwStrengthBarReg');
|
|
var text = document.getElementById('pwStrengthTextReg');
|
|
|
|
if(pass.length > 0) {
|
|
meter.style.display = 'flex';
|
|
var hasLower = /[a-z]/.test(pass);
|
|
var hasUpper = /[A-Z]/.test(pass);
|
|
var hasNumber = /\d/.test(pass);
|
|
var hasSymbol = /[^A-Za-z0-9]/.test(pass);
|
|
var typesCount = (hasLower?1:0) + (hasUpper?1:0) + (hasNumber?1:0) + (hasSymbol?1:0);
|
|
|
|
if (pass.length >= 8 && typesCount === 4) {
|
|
bar.style.width = '100%'; bar.style.backgroundColor = '#10b981'; // Kuat
|
|
text.innerText = 'Strong Password'; text.style.color = '#10b981';
|
|
} else if (pass.length >= 6 && typesCount >= 2) {
|
|
bar.style.width = '66%'; bar.style.backgroundColor = '#f59e0b'; // Sedang
|
|
text.innerText = 'Medium Password'; text.style.color = '#f59e0b';
|
|
} else {
|
|
bar.style.width = '33%'; bar.style.backgroundColor = '#ef4444'; // Lemah
|
|
text.innerText = 'Weak Password'; text.style.color = '#ef4444';
|
|
}
|
|
} else {
|
|
meter.style.display = 'none';
|
|
text.innerText = '';
|
|
}
|
|
});
|
|
|
|
// ─── SESSION EXPIRY TIMER ───
|
|
(function() {
|
|
var timerPill = document.getElementById('sessionTimerPill');
|
|
var timerText = document.getElementById('sessionTimerText');
|
|
var expiredOverlay = document.getElementById('sessionExpiredOverlay');
|
|
var sessionCompleted = false;
|
|
|
|
function updateSessionTimer() {
|
|
if (sessionCompleted) return;
|
|
var now = Math.floor(Date.now() / 1000);
|
|
var remaining = SESSION_EXPIRES_AT - now;
|
|
|
|
if (remaining <= 0) {
|
|
// Session expired - close directly
|
|
if (window.Telegram && window.Telegram.WebApp) {
|
|
window.Telegram.WebApp.close();
|
|
}
|
|
window.close();
|
|
return;
|
|
}
|
|
|
|
var mins = Math.floor(remaining / 60);
|
|
var secs = remaining % 60;
|
|
timerText.textContent = mins + ':' + (secs < 10 ? '0' : '') + secs;
|
|
|
|
// Urgent style when < 60 seconds
|
|
if (remaining <= 60) {
|
|
timerPill.classList.add('urgent');
|
|
} else {
|
|
timerPill.classList.remove('urgent');
|
|
}
|
|
|
|
setTimeout(updateSessionTimer, 1000);
|
|
}
|
|
|
|
// Mark session as completed when user successfully resets password (so timer won't interfere)
|
|
var origStep3 = document.getElementById('step3-success');
|
|
var observer = new MutationObserver(function(mutations) {
|
|
mutations.forEach(function(m) {
|
|
if (origStep3.style.display !== 'none') {
|
|
sessionCompleted = true;
|
|
timerPill.style.display = 'none';
|
|
}
|
|
});
|
|
});
|
|
observer.observe(origStep3, { attributes: true, attributeFilter: ['style'] });
|
|
|
|
updateSessionTimer();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|