1300 lines
60 KiB
PHP
1300 lines
60 KiB
PHP
<?php
|
|
session_start();
|
|
include 'koneksi.php';
|
|
|
|
$error = '';
|
|
$success = '';
|
|
$step = 1; // 1: Verify username & PIN, 2: Reset Password
|
|
|
|
// Handle form submission
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
if (isset($_POST['action']) && $_POST['action'] === 'reset_password') {
|
|
if (!isset($_SESSION['reset_username'])) {
|
|
header("Location: forgot_password.php");
|
|
exit;
|
|
}
|
|
|
|
$password = $_POST['password'] ?? '';
|
|
$confirm_password = $_POST['confirm_password'] ?? '';
|
|
|
|
if (empty($password) || empty($confirm_password)) {
|
|
$error = "All fields are required.";
|
|
$step = 2;
|
|
} elseif ($password !== $confirm_password) {
|
|
$error = "Password and Confirm Password do not match.";
|
|
$step = 2;
|
|
} else {
|
|
$username = $_SESSION['reset_username'];
|
|
$password_hash = password_hash($password, PASSWORD_DEFAULT);
|
|
$password_enc = $password;
|
|
|
|
try {
|
|
$stmt = $pdo->prepare("UPDATE account SET password = ?, password_enc = ? WHERE username = ?");
|
|
if ($stmt->execute([$password_hash, $password_enc, $username])) {
|
|
unset($_SESSION['reset_username']);
|
|
$success = "Password changed successfully. Redirecting to login page...";
|
|
} else {
|
|
$error = "Failed to change password.";
|
|
$step = 2;
|
|
}
|
|
} catch (PDOException $e) {
|
|
$error = "Database Error.";
|
|
$step = 2;
|
|
}
|
|
}
|
|
} elseif (isset($_POST['action']) && $_POST['action'] === 'verify_pin') {
|
|
$username = $_POST['username'] ?? '';
|
|
$pin = $_POST['pin'] ?? '';
|
|
|
|
if (empty($username) || empty($pin) || strlen($pin) < 6) {
|
|
$error = "Username and 6-digit PIN are required.";
|
|
} else {
|
|
try {
|
|
$stmt = $pdo->prepare("SELECT * FROM account WHERE username = ? AND pin = ?");
|
|
$stmt->execute([$username, $pin]);
|
|
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if ($user) {
|
|
$_SESSION['reset_username'] = $username;
|
|
$_SESSION['reset_old_password'] = isset($user['password_enc']) ? $user['password_enc'] : '(not available)';
|
|
$step = 2;
|
|
} else {
|
|
$error = "Incorrect username or PIN.";
|
|
}
|
|
} catch (PDOException $e) {
|
|
$error = "Database Error.";
|
|
}
|
|
}
|
|
}
|
|
} elseif (isset($_SESSION['reset_username'])) {
|
|
// If they refresh on step 2
|
|
$step = 2;
|
|
}
|
|
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
|
<title>Forgot Password - Identia Admin</title>
|
|
<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="shortcut icon" href="images/myn.png" />
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
|
|
<link rel="stylesheet"
|
|
href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css">
|
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet">
|
|
|
|
<style>
|
|
.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;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
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; }
|
|
|
|
/* Style for the custom tooltip to look native */
|
|
.custom-tooltip-wrapper {
|
|
position: relative;
|
|
display: block;
|
|
}
|
|
.custom-tooltip {
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
background-color: white;
|
|
color: #333;
|
|
padding: 8px 15px;
|
|
border-radius: 4px;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
font-size: 14px;
|
|
white-space: nowrap;
|
|
z-index: 1000;
|
|
margin-top: 8px;
|
|
border: 1px solid #ddd;
|
|
display: none; /* hidden by default */
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.custom-tooltip::before {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: 100%;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
border-width: 8px;
|
|
border-style: solid;
|
|
border-color: transparent transparent #ddd transparent;
|
|
}
|
|
.custom-tooltip::after {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: 100%;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
border-width: 7px;
|
|
border-style: solid;
|
|
border-color: transparent transparent white transparent;
|
|
}
|
|
.custom-tooltip .error-icon {
|
|
background-color: #ff9800;
|
|
color: white;
|
|
width: 20px;
|
|
height: 20px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-radius: 3px;
|
|
font-weight: bold;
|
|
font-family: serif;
|
|
}
|
|
if (isset($_POST['action']) && $_POST['action'] === 'reset_password') {
|
|
if (!isset($_SESSION['reset_username'])) {
|
|
header("Location: forgot_password.php");
|
|
exit;
|
|
}
|
|
|
|
$password = $_POST['password'] ?? '';
|
|
$confirm_password = $_POST['confirm_password'] ?? '';
|
|
|
|
if (empty($password) || empty($confirm_password)) {
|
|
$error = "All fields are required.";
|
|
$step = 2;
|
|
} elseif ($password !== $confirm_password) {
|
|
$error = "Password and Confirm Password do not match.";
|
|
$step = 2;
|
|
} else {
|
|
$username = $_SESSION['reset_username'];
|
|
$password_hash = password_hash($password, PASSWORD_DEFAULT);
|
|
$password_enc = $password;
|
|
|
|
try {
|
|
$stmt = $pdo->prepare("UPDATE account SET password = ?, password_enc = ? WHERE username = ?");
|
|
if ($stmt->execute([$password_hash, $password_enc, $username])) {
|
|
unset($_SESSION['reset_username']);
|
|
$success = "Password changed successfully. Redirecting to login page...";
|
|
} else {
|
|
$error = "Failed to change password.";
|
|
$step = 2;
|
|
}
|
|
} catch (PDOException $e) {
|
|
$error = "Database Error.";
|
|
$step = 2;
|
|
}
|
|
}
|
|
} elseif (isset($_POST['action']) && $_POST['action'] === 'verify_pin') {
|
|
$username = $_POST['username'] ?? '';
|
|
$pin = $_POST['pin'] ?? '';
|
|
|
|
if (empty($username) || empty($pin) || strlen($pin) < 6) {
|
|
$error = "Username and 6-digit PIN are required.";
|
|
} else {
|
|
try {
|
|
$stmt = $pdo->prepare("SELECT * FROM account WHERE username = ? AND pin = ?");
|
|
$stmt->execute([$username, $pin]);
|
|
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if ($user) {
|
|
$_SESSION['reset_username'] = $username;
|
|
$_SESSION['reset_old_password'] = isset($user['password_enc']) ? $user['password_enc'] : '(not available)';
|
|
$step = 2;
|
|
} else {
|
|
$error = "Incorrect username or PIN.";
|
|
}
|
|
} catch (PDOException $e) {
|
|
$error = "Database Error.";
|
|
}
|
|
}
|
|
}
|
|
} elseif (isset($_SESSION['reset_username'])) {
|
|
// If they refresh on step 2
|
|
$step = 2;
|
|
}
|
|
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
|
<title>Forgot Password - Identia Admin</title>
|
|
<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="shortcut icon" href="images/myn.png" />
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
|
|
<link rel="stylesheet"
|
|
href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css">
|
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet">
|
|
|
|
<style>
|
|
.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;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
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; }
|
|
|
|
/* Style for the custom tooltip to look native */
|
|
.custom-tooltip-wrapper {
|
|
position: relative;
|
|
display: block;
|
|
}
|
|
.custom-tooltip {
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
background-color: white;
|
|
color: #333;
|
|
padding: 8px 15px;
|
|
border-radius: 4px;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
font-size: 14px;
|
|
white-space: nowrap;
|
|
z-index: 1000;
|
|
margin-top: 8px;
|
|
border: 1px solid #ddd;
|
|
display: none; /* hidden by default */
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.custom-tooltip::before {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: 100%;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
border-width: 8px;
|
|
border-style: solid;
|
|
border-color: transparent transparent #ddd transparent;
|
|
}
|
|
.custom-tooltip::after {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: 100%;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
border-width: 7px;
|
|
border-style: solid;
|
|
border-color: transparent transparent white transparent;
|
|
}
|
|
.custom-tooltip .error-icon {
|
|
background-color: #ff9800;
|
|
color: white;
|
|
width: 20px;
|
|
height: 20px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-radius: 3px;
|
|
font-weight: bold;
|
|
font-family: serif;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
/* Camera UI styles */
|
|
#rightSideBg { padding: 0; min-height: 100vh; }
|
|
#rightNormalUi { padding: 6rem 5rem; }
|
|
|
|
@media (max-width: 991px) {
|
|
#rightSideBg { display: none !important; }
|
|
#rightSideBg.cam-active { display: flex !important; position: fixed !important; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 9998; }
|
|
|
|
#camArea {
|
|
position: fixed !important;
|
|
top: 0 !important;
|
|
left: 0 !important;
|
|
width: 100vw !important;
|
|
height: 100vh !important;
|
|
z-index: 9999 !important;
|
|
}
|
|
}
|
|
#camArea { position:absolute; top:0; left:0; width:100%; height:100%; background:#000; z-index:20; display:none; }
|
|
#vid { width: 100%; height: 100%; object-fit: cover; }
|
|
.overlay-cv { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; z-index: 2; }
|
|
|
|
.face-oval {
|
|
position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 3; pointer-events: none;
|
|
display: none; background: rgba(0,0,0,0.1);
|
|
}
|
|
.scanner-container {
|
|
position: absolute;
|
|
transition: left 0.15s ease-out, top 0.15s ease-out, width 0.15s ease-out, height 0.15s ease-out;
|
|
overflow: hidden;
|
|
border-radius: 40px;
|
|
}
|
|
.scanner-corners {
|
|
width: 100%; height: 100%; position: absolute; top:0; left:0;
|
|
border: 3px solid rgba(75, 73, 172, 0.8);
|
|
border-radius: 40px;
|
|
box-shadow: 0 0 30px rgba(75, 73, 172, 0.3) inset, 0 0 20px rgba(75, 73, 172, 0.3);
|
|
transition: all 0.3s ease;
|
|
}
|
|
.face-oval.ok .scanner-corners {
|
|
border-color: rgba(16, 185, 129, 0.9);
|
|
box-shadow: 0 0 40px rgba(16, 185, 129, 0.4) inset, 0 0 20px rgba(16, 185, 129, 0.4);
|
|
transform: scale(1.02);
|
|
}
|
|
.scan-laser {
|
|
position: absolute; top: 0; left: 0; width: 100%; height: 3px;
|
|
background: #3b82f6; box-shadow: 0 0 15px #3b82f6, 0 0 30px #3b82f6;
|
|
animation: laserScan 2s ease-in-out infinite alternate;
|
|
border-radius: 50%;
|
|
}
|
|
.face-oval.ok .scan-laser {
|
|
background: #10b981; box-shadow: 0 0 15px #10b981, 0 0 30px #10b981;
|
|
}
|
|
@keyframes laserScan {
|
|
0% { top: 0%; opacity: 0; }
|
|
10% { opacity: 1; }
|
|
90% { opacity: 1; }
|
|
100% { top: 100%; opacity: 0; }
|
|
}
|
|
|
|
/* Pills & Loading */
|
|
.cam-loading { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(30,34,35,0.9); z-index: 10; display: flex; flex-direction: column; align-items: center; justify-content: center; color: white; text-align: center; }
|
|
.ldring { width: 60px; height: 60px; border: 4px solid rgba(255,255,255,0.1); border-top: 4px solid #3b82f6; border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 20px; }
|
|
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
|
|
|
.load-progress { width: 250px; height: 6px; background: rgba(255,255,255,0.1); border-radius: 3px; overflow: hidden; margin-top: 15px; }
|
|
.load-progress-fill { width: 0%; height: 100%; background: #3b82f6; transition: width 0.3s ease; }
|
|
|
|
.st-pill { position: absolute; top: 40px; left: 50%; transform: translateX(-50%); z-index: 5; background: rgba(0,0,0,0.6); backdrop-filter: blur(8px); padding: 10px 25px; border-radius: 30px; display: flex; align-items: center; gap: 10px; color: white; font-size: 14px; font-weight: 600; box-shadow: 0 4px 15px rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.1); transition: all 0.3s; }
|
|
.st-pill i { font-size: 18px; }
|
|
.st-pill.searching i { color: #3b82f6; animation: pulse 1.5s infinite; }
|
|
.st-pill.found i { color: #10b981; }
|
|
.st-pill.warn i { color: #f59e0b; }
|
|
@keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.5; } 100% { opacity: 1; } }
|
|
|
|
.hud-dashboard { position: absolute; bottom: 40px; left: 50%; transform: translateX(-50%); z-index: 5; width: 90%; max-width: 400px; display: flex; flex-direction: column; gap: 15px; }
|
|
.match-card { background: rgba(0,0,0,0.7); backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,0.1); border-radius: 15px; padding: 15px; display: flex; align-items: center; gap: 15px; color: white; position: relative; overflow: hidden; }
|
|
.match-card img { width: 50px; height: 50px; border-radius: 50%; object-fit: cover; border: 2px solid #3b82f6; }
|
|
.match-card h4 { margin: 0 0 5px 0; font-size: 16px; font-weight: 600; }
|
|
.match-card p { margin: 0; font-size: 12px; color: #aaa; }
|
|
.distance-bar { position: absolute; bottom: 0; left: 0; width: 100%; height: 4px; background: rgba(255,255,255,0.1); }
|
|
.distance-fill { height: 100%; background: #10b981; width: 0%; transition: width 0.3s, background 0.3s; }
|
|
|
|
/* Modals & Checkbox */
|
|
.mdl-card { border: none; border-radius: 6px; box-shadow: 0 8px 30px rgba(0,0,0,0.1); }
|
|
.register-confirm-cb:checked ~ .custom-control-label::before {
|
|
background-color: #4b49ac !important;
|
|
border-color: #4b49ac !important;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container-fluid p-0">
|
|
<div class="row m-0 w-100" style="min-height: 100vh;">
|
|
|
|
<!-- Left Side: Form -->
|
|
<div class="col-12 col-lg-6 d-flex flex-column justify-content-center bg-white" style="z-index: 10; box-shadow: 2px 0 15px rgba(0,0,0,0.1); padding: 4rem 10%; overflow-y: auto; max-height: 100vh;">
|
|
<div class="auth-form-light text-left py-0 px-0 rounded">
|
|
<?php if($step === 1): ?>
|
|
<div class="d-flex align-items-center mb-4">
|
|
<img src="images/myn.png" alt="logo" style="height: 48px; filter: brightness(0); margin-right: 15px;">
|
|
<div>
|
|
<h2 class="font-weight-bold text-dark mb-1" style="font-size: 2rem; line-height: 1.2;">Forgot Password</h2>
|
|
<p class="font-weight-light text-muted mb-0" style="font-size: 1rem;">Verify your identity to reset password.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<form class="pt-3" method="POST" id="verifyForm">
|
|
<input type="hidden" name="action" value="verify_pin">
|
|
<input type="hidden" name="pin" id="realPin">
|
|
|
|
<div class="form-group custom-tooltip-wrapper mb-3">
|
|
<input type="text" class="form-control form-control-lg border" name="username" id="usernameInput" style="border-radius: 8px; border-color: #e2e8f0; font-size: 0.95rem; padding: 1rem;" placeholder="Username" value="<?= isset($_POST['username']) ? htmlspecialchars($_POST['username']) : '' ?>" required>
|
|
<div class="custom-tooltip" id="usernameTooltip">
|
|
<span class="error-icon">!</span>
|
|
<span>Username Unavailable</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group text-center mt-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
|
<label class="font-weight-bold mb-0" style="font-size: 0.875rem;">Enter 6-Digit PIN</label>
|
|
<a href="#" id="forgotPinBtn" style="color: #4B49AC; font-weight: 500; font-size: 0.875rem; text-decoration: none;">Forgot ?</a>
|
|
</div>
|
|
<div class="pin-input-group" id="pinGroup">
|
|
<input type="password" class="pin-box" maxlength="1" pattern="\d" disabled>
|
|
<input type="password" class="pin-box" maxlength="1" pattern="\d" disabled>
|
|
<input type="password" class="pin-box" maxlength="1" pattern="\d" disabled>
|
|
<input type="password" class="pin-box" maxlength="1" pattern="\d" disabled>
|
|
<input type="password" class="pin-box" maxlength="1" pattern="\d" disabled>
|
|
<input type="password" class="pin-box" maxlength="1" pattern="\d" disabled>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4">
|
|
<button type="submit" class="btn btn-block btn-lg text-white font-weight-medium mb-3" style="background-color: #4B49AC; border-radius: 8px; padding: 0.8rem;">Verify</button>
|
|
</div>
|
|
<div class="text-center mt-4" style="font-size: 0.9rem;">
|
|
Remember your password? <a href="login.php" style="color: #4B49AC;">Sign in</a>
|
|
</div>
|
|
</form>
|
|
|
|
<?php elseif($step === 2): ?>
|
|
<div class="d-flex align-items-center mb-4">
|
|
<img src="images/myn.png" alt="logo" style="height: 48px; filter: brightness(0); margin-right: 15px;">
|
|
<h2 class="font-weight-bold text-dark mb-0" style="font-size: 2rem; line-height: 1.2;">Change Password</h2>
|
|
</div>
|
|
|
|
<div class="info-alert mb-3" style="background: #f0fdf4; border: 1px solid #bbf7d0; color: #1e2223;">
|
|
<div class="info-alert-icon" style="width:20px; height:20px; background: #16a34a;"><i class="mdi mdi-check-circle-outline" style="font-size:12px; color: #fff;"></i></div>
|
|
<div class="info-alert-text" style="flex:1;display:flex;align-items:center;justify-content:space-between; font-size:11px; color: #1e2223;">
|
|
<span><strong>Verification Success!</strong> Please create a new password for this account.</span>
|
|
</div>
|
|
</div>
|
|
|
|
<p class="font-weight-bold mb-3">Account: <?= htmlspecialchars($_SESSION['reset_username']) ?></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;">
|
|
<?= htmlspecialchars($_SESSION['reset_old_password'] ?? '(not available)') ?>
|
|
</div>
|
|
</div>
|
|
|
|
<form class="pt-3" method="POST" id="passwordForm">
|
|
<input type="hidden" name="action" value="reset_password">
|
|
|
|
<div class="form-group mb-3">
|
|
<div class="input-group">
|
|
<input type="password" class="form-control form-control-lg border" name="password" id="newPassword" style="border-radius: 8px 0 0 8px; border-color: #e2e8f0; border-right: none; font-size: 0.95rem; padding: 1rem;" placeholder="New Password" required>
|
|
<div class="input-group-append">
|
|
<span class="input-group-text eye-toggle bg-white" data-target="newPassword" style="cursor: pointer; border-radius: 0 8px 8px 0; border: 1px solid #e2e8f0; border-left: none;">
|
|
<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-3">
|
|
<div class="input-group">
|
|
<input type="password" class="form-control form-control-lg border" name="confirm_password" id="confirmPassword" style="border-radius: 8px 0 0 8px; border-color: #e2e8f0; border-right: none; font-size: 0.95rem; padding: 1rem;" placeholder="Confirm New Password" required>
|
|
<div class="input-group-append">
|
|
<span class="input-group-text eye-toggle bg-white" data-target="confirmPassword" style="cursor: pointer; border-radius: 0 8px 8px 0; border: 1px solid #e2e8f0; border-left: none;">
|
|
<i class="mdi mdi-eye-off text-muted"></i>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4">
|
|
<button type="submit" class="btn btn-block btn-lg text-white font-weight-medium mb-3" style="background-color: #4B49AC; border-radius: 8px; padding: 0.8rem;">Change Password</button>
|
|
</div>
|
|
<div class="text-center mt-4" style="font-size: 0.9rem;">
|
|
Cancelled? <a href="login.php" style="color: #4B49AC;">Back to sign in</a>
|
|
</div>
|
|
</form>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
</div>
|
|
<!-- Right Side: Highly Innovative Tech Background -->
|
|
<div id="rightSideBg" class="col-12 col-lg-6 d-flex flex-column align-items-center justify-content-center position-relative" style="background-color: #4B49AC; overflow: hidden; padding: 0;">
|
|
|
|
<div id="rightNormalUi" style="width:100%; height:100%; position:absolute; top:0; left:0; z-index: 1; display:flex; flex-direction:column; align-items:center; justify-content:center;">
|
|
<!-- Animated Tech Rings -->
|
|
<div style="position: absolute; top: -20%; right: -20%; width: 800px; height: 800px; border: 1px dashed rgba(255,255,255,0.15); border-radius: 50%; animation: spinSlow 60s linear infinite; z-index: 0; pointer-events: none;">
|
|
<div style="position: absolute; top: 10%; left: 10%; width: 80%; height: 80%; border: 1px solid rgba(255,255,255,0.05); border-radius: 50%; animation: spinReverse 40s linear infinite;">
|
|
<div style="position: absolute; top: -5px; left: 50%; width: 10px; height: 10px; background: rgba(255,255,255,0.8); border-radius: 50%; box-shadow: 0 0 15px rgba(255,255,255,1);"></div>
|
|
</div>
|
|
<div style="position: absolute; top: 25%; left: 25%; width: 50%; height: 50%; border: 2px solid rgba(255,255,255,0.03); border-radius: 50%;"></div>
|
|
</div>
|
|
|
|
<style>
|
|
@keyframes spinSlow { 100% { transform: rotate(360deg); } }
|
|
@keyframes spinReverse { 100% { transform: rotate(-360deg); } }
|
|
</style>
|
|
|
|
<!-- Interactive Particle Canvas -->
|
|
<canvas id="particleCanvas" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; pointer-events: auto;"></canvas>
|
|
|
|
<!-- Script for Interactive Particles -->
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", function() {
|
|
const canvas = document.getElementById("particleCanvas");
|
|
if (!canvas) return;
|
|
const ctx = canvas.getContext("2d");
|
|
let particlesArray;
|
|
|
|
function resize() {
|
|
canvas.width = canvas.parentElement.clientWidth;
|
|
canvas.height = canvas.parentElement.clientHeight;
|
|
}
|
|
window.addEventListener('resize', resize);
|
|
resize();
|
|
|
|
let mouse = { x: null, y: null, radius: 120 };
|
|
canvas.addEventListener('mousemove', function(event) {
|
|
const rect = canvas.getBoundingClientRect();
|
|
mouse.x = event.clientX - rect.left;
|
|
mouse.y = event.clientY - rect.top;
|
|
});
|
|
|
|
class Particle {
|
|
constructor(x, y, directionX, directionY, size, color) {
|
|
this.x = x;
|
|
this.y = y;
|
|
this.directionX = directionX;
|
|
this.directionY = directionY;
|
|
this.size = size;
|
|
this.color = color;
|
|
}
|
|
draw() {
|
|
ctx.beginPath();
|
|
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2, false);
|
|
ctx.fillStyle = this.color;
|
|
ctx.fill();
|
|
}
|
|
update() {
|
|
if (this.x > canvas.width || this.x < 0) this.directionX = -this.directionX;
|
|
if (this.y > canvas.height || this.y < 0) this.directionY = -this.directionY;
|
|
let dx = mouse.x - this.x;
|
|
let dy = mouse.y - this.y;
|
|
let distance = Math.sqrt(dx * dx + dy * dy);
|
|
if (distance < mouse.radius) {
|
|
if (mouse.x !== null && mouse.y !== null) {
|
|
if (mouse.x < this.x && this.x < canvas.width - this.size * 10) this.x += 1;
|
|
if (mouse.x > this.x && this.x > this.size * 10) this.x -= 1;
|
|
if (mouse.y < this.y && this.y < canvas.height - this.size * 10) this.y += 1;
|
|
if (mouse.y > this.y && this.y > this.size * 10) this.y -= 1;
|
|
}
|
|
}
|
|
this.x += this.directionX;
|
|
this.y += this.directionY;
|
|
this.draw();
|
|
}
|
|
}
|
|
|
|
function init() {
|
|
particlesArray = [];
|
|
let numberOfParticles = (canvas.height * canvas.width) / 10000;
|
|
for (let i = 0; i < numberOfParticles; i++) {
|
|
let size = (Math.random() * 2) + 0.5;
|
|
let x = (Math.random() * ((canvas.width - size * 2) - (size * 2)) + size * 2);
|
|
let y = (Math.random() * ((canvas.height - size * 2) - (size * 2)) + size * 2);
|
|
let directionX = (Math.random() * 1) - 0.5;
|
|
let directionY = (Math.random() * 1) - 0.5;
|
|
let color = 'rgba(255,255,255,0.4)';
|
|
particlesArray.push(new Particle(x, y, directionX, directionY, size, color));
|
|
}
|
|
}
|
|
|
|
function connect() {
|
|
for (let a = 0; a < particlesArray.length; a++) {
|
|
for (let b = a; b < particlesArray.length; b++) {
|
|
let distance = ((particlesArray[a].x - particlesArray[b].x) * (particlesArray[a].x - particlesArray[b].x)) +
|
|
((particlesArray[a].y - particlesArray[b].y) * (particlesArray[a].y - particlesArray[b].y));
|
|
if (distance < (canvas.width / 7) * (canvas.height / 7)) {
|
|
let opacityValue = 1 - (distance / 20000);
|
|
ctx.strokeStyle = 'rgba(255,255,255,' + opacityValue * 0.4 + ')';
|
|
ctx.lineWidth = 1;
|
|
ctx.beginPath();
|
|
ctx.moveTo(particlesArray[a].x, particlesArray[a].y);
|
|
ctx.lineTo(particlesArray[b].x, particlesArray[b].y);
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function animate() {
|
|
requestAnimationFrame(animate);
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
for (let i = 0; i < particlesArray.length; i++) {
|
|
particlesArray[i].update();
|
|
}
|
|
connect();
|
|
}
|
|
|
|
init();
|
|
animate();
|
|
});
|
|
</script>
|
|
|
|
<!-- Sliding Text Container -->
|
|
<div style="z-index: 2; max-width: 600px; color: #fff; pointer-events: none;">
|
|
<div id="loginTextSlider" class="carousel slide" data-ride="carousel" data-interval="4000" style="pointer-events: auto;">
|
|
<div class="carousel-inner">
|
|
<div class="carousel-item active">
|
|
<h1 class="font-weight-bold mb-4" style="font-size: 4rem; letter-spacing: 0px; text-shadow: 0 4px 10px rgba(0,0,0,0.1);">Welcome to...</h1>
|
|
<p style="font-size: 1.15rem; opacity: 0.85; font-weight: 300; line-height: 1.6;">Experience the best way to manage your work and track user activities efficiently with our state-of-the-art platform.</p>
|
|
</div>
|
|
<div class="carousel-item">
|
|
<h1 class="font-weight-bold mb-4" style="font-size: 4rem; letter-spacing: 0px; text-shadow: 0 4px 10px rgba(0,0,0,0.1);">Secure Access</h1>
|
|
<p style="font-size: 1.15rem; opacity: 0.85; font-weight: 300; line-height: 1.6;">Your data is protected with military-grade security encryption and seamless advanced biometric login features.</p>
|
|
</div>
|
|
<div class="carousel-item">
|
|
<h1 class="font-weight-bold mb-4" style="font-size: 4rem; letter-spacing: 0px; text-shadow: 0 4px 10px rgba(0,0,0,0.1);">Deep Analytics</h1>
|
|
<p style="font-size: 1.15rem; opacity: 0.85; font-weight: 300; line-height: 1.6;">Get detailed insights, real-time history logs, and actionable metrics to make informed administrative decisions instantly.</p>
|
|
</div>
|
|
</div>
|
|
<!-- Indicators aligned to the left -->
|
|
<ol class="carousel-indicators" style="position: relative; margin-top: 3rem; margin-bottom: 0; justify-content: flex-start; margin-left: 0;">
|
|
<li data-target="#loginTextSlider" data-slide-to="0" class="active" style="width: 10px; height: 10px; border-radius: 50%; margin: 0 8px 0 0; background-color: #fff; opacity: 0.5; border: none;"></li>
|
|
<li data-target="#loginTextSlider" data-slide-to="1" style="width: 10px; height: 10px; border-radius: 50%; margin: 0 8px 0 0; background-color: #fff; opacity: 0.5; border: none;"></li>
|
|
<li data-target="#loginTextSlider" data-slide-to="2" style="width: 10px; height: 10px; border-radius: 50%; margin: 0 8px 0 0; background-color: #fff; opacity: 0.5; border: none;"></li>
|
|
</ol>
|
|
<style>
|
|
.carousel-indicators .active { opacity: 1 !important; transform: scale(1.2); }
|
|
</style>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Camera UI -->
|
|
<div id="camArea">
|
|
<video id="vid" autoplay playsinline muted></video>
|
|
<canvas class="overlay-cv" id="ovCanvas"></canvas>
|
|
|
|
<div class="face-oval" id="oval">
|
|
<div class="scanner-container">
|
|
<div class="scanner-corners"></div>
|
|
<div class="scan-laser"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="cam-loading" id="camLoading" style="display:none;">
|
|
<div class="ldring"></div>
|
|
<p id="loadText" style="text-shadow: none;">Loading AI Model & Camera...</p>
|
|
<div class="load-progress"><div class="load-progress-fill" id="loadProgress"></div></div>
|
|
</div>
|
|
|
|
<div class="st-pill searching" id="stPill" style="display:none;">
|
|
<i class="mdi mdi-face-recognition" id="stIcon"></i>
|
|
<span id="stText">Searching for face...</span>
|
|
</div>
|
|
|
|
<div class="hud-dashboard" id="hudDashboard" style="display:none;">
|
|
<div class="match-card">
|
|
<img src="" id="refImgPreview" alt="Reference">
|
|
<div>
|
|
<h4 id="hudName">Loading...</h4>
|
|
<p>Matching face identity...</p>
|
|
</div>
|
|
<div class="distance-bar">
|
|
<div class="distance-fill" id="distFill" style="background:#f59e0b;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="camCancelBtn" style="position:absolute; top:20px; right:20px; z-index:30; cursor:pointer; color:white; background:rgba(0,0,0,0.5); padding:8px 16px; border-radius:20px; font-weight:600; display:none;" onclick="cancelCamera()">Cancel</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- ==================== MODAL: FACE VERIFICATION CONFIRM ==================== -->
|
|
<div class="modal fade" id="modalFaceVerificationConfirm" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content mdl-card">
|
|
<div class="modal-body p-4" style="text-align: left;">
|
|
<h5 class="font-weight-bold mb-3">Facial Verification</h5>
|
|
<p style="font-size:14px;color:#666;margin-bottom:20px;">Please prepare your face for facial verification.</p>
|
|
|
|
<div class="custom-control custom-checkbox mb-4" style="display:flex; align-items:center;">
|
|
<input type="checkbox" class="custom-control-input register-confirm-cb" id="faceConfirmCheck" onchange="document.getElementById('btnNowFaceVerification').disabled = !this.checked" required>
|
|
<label class="custom-control-label" for="faceConfirmCheck" style="font-size:14px; color:#1e1f23; padding-top:2px;">I am ready</label>
|
|
</div>
|
|
|
|
<div class="d-flex justify-content-end" style="gap:8px;">
|
|
<button type="button" class="btn btn-light font-weight-bold" style="padding:8px 18px;font-size:13px; border-radius:6px;" data-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-primary font-weight-bold" style="padding:8px 18px;font-size:13px; border-radius:6px;" id="btnNowFaceVerification" onclick="startFaceVerification()" disabled>Now</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Custom Alert Container -->
|
|
<div id="customAlert" class="custom-alert warning">
|
|
<i class="mdi mdi-alert-circle custom-alert-icon" id="customAlertIcon"></i>
|
|
<div class="custom-alert-content">
|
|
<div class="custom-alert-title" id="customAlertTitle">Warning</div>
|
|
<div class="custom-alert-msg" id="customAlertMsg">Message goes here</div>
|
|
</div>
|
|
<i class="mdi mdi-close custom-alert-close" onclick="closeCustomAlert()"></i>
|
|
</div>
|
|
|
|
<script src="vendors/js/vendor.bundle.base.js"></script>
|
|
<script>
|
|
function showCustomAlert(title, message, type='warning') {
|
|
const alertBox = document.getElementById('customAlert');
|
|
const icon = document.getElementById('customAlertIcon');
|
|
|
|
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';
|
|
|
|
setTimeout(() => closeCustomAlert(), 5000); // auto close after 5s
|
|
}
|
|
|
|
function closeCustomAlert() {
|
|
const alertBox = document.getElementById('customAlert');
|
|
alertBox.style.animation = 'fadeOut 0.3s ease-out forwards';
|
|
setTimeout(() => {
|
|
alertBox.style.display = 'none';
|
|
}, 300);
|
|
}
|
|
|
|
<?php if ($step === 1): ?>
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const group = document.getElementById('pinGroup');
|
|
const inputs = group.querySelectorAll('.pin-box');
|
|
const hiddenInput = document.getElementById('realPin');
|
|
|
|
inputs.forEach((input, index) => {
|
|
input.addEventListener('input', (e) => {
|
|
input.value = input.value.replace(/\D/g, '');
|
|
if (input.value !== '' && index < inputs.length - 1) {
|
|
inputs[index + 1].focus();
|
|
}
|
|
updateHidden();
|
|
});
|
|
|
|
input.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Backspace' && input.value === '' && index > 0) {
|
|
inputs[index - 1].focus();
|
|
}
|
|
});
|
|
});
|
|
|
|
function updateHidden() {
|
|
let pin = '';
|
|
inputs.forEach(inp => pin += inp.value);
|
|
hiddenInput.value = pin;
|
|
|
|
const btn = document.getElementById('forgotPinBtn');
|
|
if(btn) {
|
|
if(pin.length > 0) {
|
|
btn.style.pointerEvents = 'none';
|
|
btn.style.opacity = '0.4';
|
|
} else {
|
|
btn.style.pointerEvents = 'auto';
|
|
btn.style.opacity = '1';
|
|
}
|
|
}
|
|
}
|
|
|
|
document.getElementById('verifyForm').addEventListener('submit', (e) => {
|
|
if (hiddenInput.value.length < 6) {
|
|
e.preventDefault();
|
|
showCustomAlert("Incomplete Form", "Please enter the complete 6-digit PIN.", "warning");
|
|
}
|
|
});
|
|
|
|
// Username Verification Tooltip Logic
|
|
const usernameInput = document.getElementById('usernameInput');
|
|
const usernameTooltip = document.getElementById('usernameTooltip');
|
|
let checkTimeout;
|
|
|
|
function checkUsername() {
|
|
const username = usernameInput.value.trim();
|
|
if (username === '') {
|
|
usernameTooltip.style.display = 'none';
|
|
usernameInput.setCustomValidity('');
|
|
inputs.forEach(inp => {
|
|
inp.disabled = true;
|
|
inp.value = '';
|
|
});
|
|
updateHidden();
|
|
return;
|
|
}
|
|
|
|
const formData = new FormData();
|
|
formData.append('username', username);
|
|
|
|
fetch('check_username.php', {
|
|
method: 'POST',
|
|
body: formData
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.status === 'available') {
|
|
// Available means not in database, which is an error for forgot password
|
|
usernameTooltip.style.display = 'flex';
|
|
usernameInput.setCustomValidity('USERNAME UNAVAILABLE');
|
|
inputs.forEach(inp => {
|
|
inp.disabled = true;
|
|
inp.value = '';
|
|
});
|
|
updateHidden();
|
|
} else if (data.status === 'taken') {
|
|
// Username is in database, meaning valid for reset
|
|
usernameTooltip.style.display = 'none';
|
|
usernameInput.setCustomValidity('');
|
|
inputs.forEach(inp => {
|
|
inp.disabled = false;
|
|
});
|
|
}
|
|
})
|
|
.catch(err => {
|
|
console.error(err);
|
|
});
|
|
}
|
|
|
|
usernameInput.addEventListener('input', function() {
|
|
usernameTooltip.style.display = 'none'; // hide while typing
|
|
clearTimeout(checkTimeout);
|
|
checkTimeout = setTimeout(checkUsername, 500); // debounce check
|
|
});
|
|
|
|
usernameInput.addEventListener('blur', checkUsername);
|
|
|
|
usernameInput.addEventListener('focus', function() {
|
|
usernameTooltip.style.display = 'none';
|
|
});
|
|
});
|
|
<?php endif; ?>
|
|
|
|
<?php if($error): ?>
|
|
showCustomAlert("Failed", "<?= addslashes(htmlspecialchars($error)) ?>", "error");
|
|
<?php endif; ?>
|
|
|
|
<?php if($success): ?>
|
|
showCustomAlert("Success", "<?= addslashes(htmlspecialchars($success)) ?>", "success");
|
|
setTimeout(() => { window.location.href = 'login.php'; }, 2500);
|
|
<?php endif; ?>
|
|
|
|
const pwForm = document.getElementById('passwordForm');
|
|
if(pwForm) {
|
|
pwForm.addEventListener('submit', function(e) {
|
|
var pass = document.getElementById("newPassword").value;
|
|
const pwValid = (pass.length >= 8 && /[a-z]/.test(pass) && /[A-Z]/.test(pass) && /\d/.test(pass) && /[^A-Za-z0-9]/.test(pass));
|
|
if (!pwValid) {
|
|
e.preventDefault();
|
|
showCustomAlert("Warning", "Password does not meet criteria (min. 8 chars, uppercase, lowercase, numbers, and symbols)!", "warning");
|
|
document.getElementById("newPassword").focus();
|
|
}
|
|
});
|
|
|
|
// 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 = '';
|
|
}
|
|
});
|
|
}
|
|
</script>
|
|
<script src="js/face-api/face-api.min.js"></script>
|
|
<script>
|
|
const forgotPinBtn = document.getElementById('forgotPinBtn');
|
|
const rightNormalUi = document.getElementById('rightNormalUi');
|
|
const camArea = document.getElementById('camArea');
|
|
const camLoading = document.getElementById('camLoading');
|
|
const loadProgress = document.getElementById('loadProgress');
|
|
const loadText = document.getElementById('loadText');
|
|
const stPill = document.getElementById('stPill');
|
|
const stIcon = document.getElementById('stIcon');
|
|
const stText = document.getElementById('stText');
|
|
const vid = document.getElementById('vid');
|
|
const ovCanvas = document.getElementById('ovCanvas');
|
|
const oval = document.getElementById('oval');
|
|
const hudDashboard = document.getElementById('hudDashboard');
|
|
const hudName = document.getElementById('hudName');
|
|
const distFill = document.getElementById('distFill');
|
|
const refImgPreview = document.getElementById('refImgPreview');
|
|
const camCancelBtn = document.getElementById('camCancelBtn');
|
|
|
|
let stream = null;
|
|
let detectionLoopInterval = null;
|
|
let refDescriptor = null;
|
|
let userFoundPin = '';
|
|
|
|
function setProgress(pct) { loadProgress.style.width = pct + '%'; }
|
|
function setStatus(type, txt) {
|
|
stPill.className = 'st-pill';
|
|
if (type === 'search') { stPill.classList.add('searching'); stIcon.className = 'mdi mdi-face-recognition'; }
|
|
if (type === 'ok') { stPill.classList.add('found'); stIcon.className = 'mdi mdi-check-circle'; }
|
|
if (type === 'err') { stPill.classList.add('warn'); stIcon.className = 'mdi mdi-alert-circle'; }
|
|
stText.textContent = txt;
|
|
}
|
|
|
|
function cancelCamera() {
|
|
if(stream) { stream.getTracks().forEach(t => t.stop()); stream = null; }
|
|
clearInterval(detectionLoopInterval);
|
|
|
|
const rightSide = document.getElementById('rightSideBg');
|
|
rightSide.classList.remove('d-none');
|
|
rightSide.classList.remove('cam-active');
|
|
rightNormalUi.style.display = 'flex';
|
|
|
|
camArea.style.display = 'none';
|
|
vid.srcObject = null;
|
|
}
|
|
|
|
if (forgotPinBtn) {
|
|
forgotPinBtn.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
const usernameInput = document.getElementById('usernameInput');
|
|
const username = usernameInput.value.trim();
|
|
|
|
if(!username) {
|
|
showCustomAlert("Warning", "Please enter your Username first.", "warning");
|
|
usernameInput.focus();
|
|
return;
|
|
}
|
|
|
|
document.getElementById('faceConfirmCheck').checked = false;
|
|
document.getElementById('btnNowFaceVerification').disabled = true;
|
|
$('#modalFaceVerificationConfirm').modal('show');
|
|
});
|
|
}
|
|
|
|
async function startFaceVerification() {
|
|
$('#modalFaceVerificationConfirm').modal('hide');
|
|
const usernameInput = document.getElementById('usernameInput');
|
|
const username = usernameInput.value.trim();
|
|
|
|
const rightSide = document.getElementById('rightSideBg');
|
|
rightSide.classList.remove('d-none');
|
|
rightSide.classList.add('cam-active');
|
|
|
|
camArea.style.display = 'block';
|
|
rightNormalUi.style.display = 'none';
|
|
camLoading.style.display = 'flex';
|
|
stPill.style.display = 'none';
|
|
hudDashboard.style.display = 'none';
|
|
camCancelBtn.style.display = 'block';
|
|
|
|
try {
|
|
setProgress(10);
|
|
const formData = new FormData();
|
|
formData.append('username', username);
|
|
const res = await fetch('api_get_account_foto.php', { method: 'POST', body: formData });
|
|
const data = await res.json();
|
|
|
|
if (data.status !== 'success') {
|
|
throw new Error(data.message || "Failed to get account photo.");
|
|
}
|
|
|
|
userFoundPin = data.pin;
|
|
const photoUrl = data.foto;
|
|
refImgPreview.src = photoUrl;
|
|
hudName.innerText = username;
|
|
|
|
setProgress(20);
|
|
loadText.textContent = 'Loading AI models...';
|
|
await Promise.all([
|
|
faceapi.nets.tinyFaceDetector.loadFromUri('js/face-api/models'),
|
|
faceapi.nets.faceLandmark68Net.loadFromUri('js/face-api/models'),
|
|
faceapi.nets.faceRecognitionNet.loadFromUri('js/face-api/models')
|
|
]);
|
|
|
|
setProgress(50);
|
|
loadText.textContent = 'Computing reference face...';
|
|
|
|
const refImg = new Image();
|
|
refImg.crossOrigin = 'anonymous';
|
|
refImg.src = photoUrl;
|
|
await new Promise((resolve, reject) => {
|
|
refImg.onload = resolve;
|
|
refImg.onerror = () => reject(new Error("Failed to load reference photo from server."));
|
|
});
|
|
|
|
const refDetection = await faceapi.detectSingleFace(refImg, new faceapi.TinyFaceDetectorOptions({ inputSize: 416, scoreThreshold: 0.1 })).withFaceLandmarks().withFaceDescriptor();
|
|
|
|
if (!refDetection) {
|
|
throw new Error('Unable to detect a face in the reference photo.');
|
|
}
|
|
refDescriptor = refDetection.descriptor;
|
|
|
|
setProgress(70);
|
|
loadText.textContent = 'Starting camera...';
|
|
|
|
stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user', width: { ideal: 480 } }, audio: false });
|
|
vid.srcObject = stream;
|
|
|
|
vid.onloadedmetadata = () => {
|
|
vid.play();
|
|
setProgress(100);
|
|
setTimeout(() => {
|
|
camLoading.style.display = 'none';
|
|
stPill.style.display = 'flex';
|
|
hudDashboard.style.display = 'flex';
|
|
setStatus('search', 'Position your face...');
|
|
startMatching();
|
|
}, 500);
|
|
};
|
|
|
|
} catch (err) {
|
|
cancelCamera();
|
|
showCustomAlert("Error", err.message, "error");
|
|
}
|
|
}
|
|
|
|
function startMatching() {
|
|
let consecutiveMatches = 0;
|
|
let runningCount = 0;
|
|
let isDetecting = false;
|
|
|
|
detectionLoopInterval = setInterval(async () => {
|
|
if (vid.paused || vid.ended || !vid.videoWidth || isDetecting) return;
|
|
isDetecting = true;
|
|
|
|
if (runningCount === 0) {
|
|
ovCanvas.width = vid.videoWidth;
|
|
ovCanvas.height = vid.videoHeight;
|
|
runningCount++;
|
|
}
|
|
|
|
try {
|
|
const results = await faceapi.detectSingleFace(vid, new faceapi.TinyFaceDetectorOptions({ inputSize: 160 })).withFaceLandmarks().withFaceDescriptor();
|
|
|
|
if (results) {
|
|
oval.style.display = 'block';
|
|
const res = results;
|
|
|
|
// Track Face Dynamically
|
|
const eW = vid.clientWidth;
|
|
const eH = vid.clientHeight;
|
|
const vW = vid.videoWidth;
|
|
const vH = vid.videoHeight;
|
|
const scale = Math.max(eW / vW, eH / vH);
|
|
const offX = (vW * scale - eW) / 2;
|
|
const offY = (vH * scale - eH) / 2;
|
|
|
|
const box = res.detection.box;
|
|
const screenX = box.x * scale - offX;
|
|
const screenY = box.y * scale - offY;
|
|
const screenW = box.width * scale;
|
|
const screenH = box.height * scale;
|
|
|
|
const scanner = document.querySelector('.scanner-container');
|
|
if(scanner) {
|
|
const padX = screenW * 0.4;
|
|
const padY = screenH * 0.4;
|
|
scanner.style.left = (screenX - padX/2) + 'px';
|
|
scanner.style.top = (screenY - padY/2) + 'px';
|
|
scanner.style.width = (screenW + padX) + 'px';
|
|
scanner.style.height = (screenH + padY) + 'px';
|
|
}
|
|
|
|
const distance = faceapi.euclideanDistance(refDescriptor, res.descriptor);
|
|
|
|
let mappedDistance = Math.max(0, Math.min(1, (0.6 - distance) / 0.6));
|
|
distFill.style.width = (mappedDistance * 100) + '%';
|
|
|
|
if (distance < 0.55) {
|
|
distFill.style.background = '#10b981';
|
|
oval.classList.add('ok');
|
|
setStatus('ok', 'Face Matched!');
|
|
consecutiveMatches++;
|
|
|
|
if (consecutiveMatches >= 3) {
|
|
cancelCamera();
|
|
|
|
const group = document.getElementById('pinGroup');
|
|
if (group && userFoundPin.length === 6) {
|
|
const inputs = group.querySelectorAll('.pin-box');
|
|
for (let i = 0; i < 6; i++) {
|
|
inputs[i].value = userFoundPin[i];
|
|
inputs[i].disabled = false;
|
|
}
|
|
document.getElementById('realPin').value = userFoundPin;
|
|
}
|
|
}
|
|
} else {
|
|
distFill.style.background = '#f59e0b';
|
|
oval.classList.remove('ok');
|
|
setStatus('err', 'Different face');
|
|
consecutiveMatches = 0;
|
|
}
|
|
} else {
|
|
oval.style.display = 'none';
|
|
distFill.style.width = '0%';
|
|
oval.classList.remove('ok');
|
|
setStatus('search', 'Position your face...');
|
|
consecutiveMatches = 0;
|
|
}
|
|
} catch (e) {
|
|
console.error("Detection error:", e);
|
|
} finally {
|
|
isDetecting = false;
|
|
}
|
|
}, 400);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|