TKK_E32230273/forgot.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>