TKK_E32230273/192.168.1.20.php

472 lines
19 KiB
PHP

<?php
// Validate token strictly before rendering anything.
$token = isset($_GET['token']) ? trim($_GET['token']) : '';
function renderError($message) {
header("Location: 404.php");
exit;
}
if (empty($token)) {
renderError("The link you are using missing an access token.");
}
$tokensFile = __DIR__ . '/door_tokens.json';
if (!file_exists($tokensFile)) {
renderError("The validation system is currently unavailable.");
}
$tokens = json_decode(file_get_contents($tokensFile), true) ?: [];
if (!isset($tokens[$token])) {
renderError("Access token not found or invalid.");
}
if ($tokens[$token]['used']) {
renderError("This link has already been used. Please request a new link from the Admin.");
}
if (time() - $tokens[$token]['created_at'] > 300) { // 5 minutes expiration
renderError("This access link has expired (more than 5 minutes).");
}
// Mark as used exclusively for this session
$tokens[$token]['used'] = true;
file_put_contents($tokensFile, json_encode($tokens, JSON_PRETTY_PRINT));
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no">
<title>Face Access — Identia</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="vendors/mdi/css/materialdesignicons.min.css">
<style>
:root {
--primary: #4747a1;
--primary-glow: rgba(71,71,161,0.3);
--success: #10b981;
--bg: #f8fafc;
--card: #ffffff;
--text: #0f172a;
--muted: #64748b;
--border: #e2e8f0;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
font-family: 'Inter', sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: flex-start;
padding: 20px 16px 40px;
}
.card {
background: var(--card);
border-radius: 20px;
width: 100%; max-width: 460px;
padding: 32px 28px 36px;
border: 1px solid var(--border);
box-shadow: 0 10px 30px rgba(0,0,0,0.05);
position: relative;
}
/* ── HEADER ── */
.card-header {
display: flex; align-items: center; gap: 16px;
margin-bottom: 24px;
}
.header-icon {
width: 52px; height: 52px; border-radius: 16px;
background: #e6e6f2; display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.header-icon i { font-size: 26px; color: var(--primary); }
.header-text h1 { font-size: 20px; font-weight: 800; color: var(--text); letter-spacing: -0.3px; }
.header-text p { font-size: 12px; color: var(--muted); margin-top: 3px; font-weight: 500; }
/* ── TIMER STATUS ── */
.badge-timer {
position: absolute; top: 20px; right: 28px;
background: #fef2f2; border: 1px solid #fecaca;
color: #ef4444; font-size: 12px; font-weight: 700;
padding: 6px 12px; border-radius: 20px;
display: flex; align-items: center; gap: 6px;
}
/* ── CAMERA WRAPPER ── */
.cam-wrapper {
width: 100%; position: relative;
border-radius: 16px; overflow: hidden;
background: #1a1e20; aspect-ratio: 3/4; cursor: default;
border: 1px solid var(--border); transition: all 0.4s ease;
}
#cam-video { width: 100%; height: 100%; object-fit: cover; transform: scaleX(-1); display: block; }
#scan-line {
position: absolute; top: 0; left: 0;
width: 100%; height: 3px; background: #ffffff;
box-shadow: 0 0 10px #ffffff, 0 0 20px rgba(255,255,255,0.5);
z-index: 4; animation: scanMove 2.8s ease-in-out infinite;
}
@keyframes scanMove { 0% { top: 0%; } 50% { top: calc(100% - 3px); } 100% { top: 0%; } }
#cam-loading {
position: absolute; inset: 0; background: #ffffff;
display: flex; flex-direction: column; align-items: center;
justify-content: center; gap: 16px; z-index: 10;
}
.ld-ring {
width: 44px; height: 44px; border-radius: 50%;
border: 3px solid #e6e6f2; border-top-color: var(--primary);
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
#cam-loading p { font-size: 13px; color: var(--muted); font-weight: 600; text-align: center; padding: 0 20px;}
/* Live status pill */
#face-pill {
position: absolute; top: 16px; left: 50%; transform: translateX(-50%);
display: none; align-items: center; gap: 8px;
padding: 8px 18px; border-radius: 24px;
font-size: 12px; font-weight: 700; white-space: nowrap; z-index: 5;
background: rgba(255,255,255,0.9); color: var(--text); backdrop-filter: blur(8px);
}
/* ── SUCCESS VIEW (Replaces Camera) ── */
.success-view {
display: none; flex-direction: column; align-items: center;
justify-content: center; text-align: center; gap: 16px;
padding: 40px 20px; background: #f0fdf4; border-radius: 16px;
border: 1px solid #bbf7d0; margin-bottom: 20px; animation: popIn 0.5s ease;
}
@keyframes popIn { 0%{opacity:0; transform:scale(0.9);} 100%{opacity:1; transform:scale(1);} }
.success-icon {
width: 80px; height: 80px; border-radius: 50%; background: #dcfce7;
display: flex; align-items: center; justify-content: center; color: #16a34a; font-size: 40px;
}
.success-view h2 { font-size: 22px; font-weight: 800; color: #166534; margin:0; }
.success-view p { font-size: 14px; color: #15803d; margin:0; font-weight: 500;}
/* ── UNLOCK BUTTON ── */
.btn-unlock {
display: none; width: 100%; padding: 18px;
background: var(--primary); color: #fff;
border: none; border-radius: 16px; font-size: 16px; font-weight: 800;
cursor: pointer; display: none; align-items: center; justify-content: center; gap: 10px;
transition: all 0.3s;
box-shadow: 0 10px 20px rgba(71, 71, 161, 0.4);
margin-top: 10px; letter-spacing: 0.5px;
}
.btn-unlock:hover { transform: translateY(-3px); box-shadow: 0 15px 30px rgba(71, 71, 161, 0.5); }
.btn-unlock:active { transform: translateY(0); }
.btn-unlock.opened { background: var(--success); box-shadow: 0 10px 20px rgba(16, 185, 129, 0.4); pointer-events: none;}
.toast {
position: fixed; top: 20px; right: 20px; background: #fff; border-left: 4px solid var(--success);
padding: 14px 20px; border-radius: 8px; box-shadow: 0 4px 15px rgba(0,0,0,0.1);
display: none; z-index: 10000; font-weight: 600; font-size: 14px;
}
.overlay-mask {
display: none; position: fixed; inset: 0; background: rgba(255,255,255,0.9);
z-index: 9999; flex-direction: column; align-items: center; justify-content: center; text-align: center;
}
.overlay-mask h1 { color: #0f172a; font-size: 24px; font-weight: 800; }
.overlay-mask p { color: #64748b; font-size: 16px; }
</style>
</head>
<body>
<div class="card">
<div class="badge-timer" id="timerBadge"><i class="mdi mdi-timer-sand"></i> <span id="timeTxt">05:00</span></div>
<div class="card-header">
<div class="header-icon"><i class="mdi mdi-face-recognition"></i></div>
<div class="header-text">
<h1>Face Verification</h1>
<p>Scan your face to unlock the door</p>
</div>
</div>
<!-- Camera View Focus -->
<div class="cam-wrapper" id="camWrapper">
<div id="cam-loading">
<div class="ld-ring"></div>
<p id="camLoadTxt">Connecting to API and loading AI face models...</p>
</div>
<div id="scan-line"></div>
<video id="cam-video" autoplay playsinline muted></video>
<div id="face-pill"><i class="mdi mdi-magnify" id="fpIcon"></i> <span id="fpTxt">Searching for Face...</span></div>
</div>
<!-- Success Output -->
<div class="success-view" id="successView">
<div class="success-icon"><i class="mdi mdi-check-decagram"></i></div>
<h2>Access Granted!</h2>
<p>Hello, <strong id="ocName">John Doe</strong>.</p>
</div>
<!-- The Door Mechanism -->
<button class="btn-unlock" id="btnUnlock">
<i class="mdi mdi-door-closed" id="doorIcon"></i> <span id="btnTxt">Open the door</span>
</button>
<button class="btn-unlock" id="btnLock" style="display:none; background:#ef4444; box-shadow:0 10px 20px rgba(239,68,68,0.3); margin-top:12px;">
<i class="mdi mdi-lock" id="lockIcon"></i> <span id="lockTxt">Lock the door</span>
</button>
</div>
<div class="toast" id="toastMsg">Status updated!</div>
<div class="overlay-mask" id="expireMask">
<i class="mdi mdi-timer-off-outline" style="font-size: 60px; color:#f59e0b; margin-bottom:10px;"></i>
<h1>Session Expired</h1>
<p>Your session has expired. This page will close automatically.</p>
</div>
<!-- Mengambil faceapi dari vendor as referenced from registrasi_foto.php -->
<script src="js/face-api/face-api.min.js"></script>
<script>
(function() {
// UI Elements
const camWrapper = document.getElementById('camWrapper');
const camLoadTxt = document.getElementById('camLoadTxt');
const camLoading = document.getElementById('cam-loading');
const video = document.getElementById('cam-video');
const facePill = document.getElementById('face-pill');
const fpIcon = document.getElementById('fpIcon');
const fpTxt = document.getElementById('fpTxt');
const btnUnlock = document.getElementById('btnUnlock');
const successView = document.getElementById('successView');
const ocName = document.getElementById('ocName');
let faceMatcher = null;
let recognitionLoop = null;
let stream = null;
// Page expires at max 5 mins tightly
let secondsRemaining = 300;
let doorTimer = null;
const TOKEN = new URLSearchParams(window.location.search).get('token');
// Display timer
const timeInterval = setInterval(() => {
secondsRemaining--;
if(secondsRemaining <= 0) {
expireSession();
return;
}
const m = Math.floor(secondsRemaining / 60);
const s = secondsRemaining % 60;
document.getElementById('timeTxt').textContent = `${m.toString().padStart(2,'0')}:${s.toString().padStart(2,'0')}`;
}, 1000);
function expireSession() {
clearInterval(timeInterval);
clearTimeout(doorTimer);
document.getElementById('expireMask').style.display = 'flex';
// Cleanup tokens automatically handled by API generating, but we enforce it here locally.
setTimeout(() => { window.location.href = '192.168.4.1.php'; }, 4000);
}
async function init() {
try {
// Load neural nets. We fallback to CDN if faceRecognitionNet isn't local.
camLoadTxt.textContent = "Loading Face APIs...";
await faceapi.nets.ssdMobilenetv1.loadFromUri('https://cdn.jsdelivr.net/npm/@vladmandic/face-api/model');
await faceapi.nets.faceLandmark68Net.loadFromUri('https://cdn.jsdelivr.net/npm/@vladmandic/face-api/model');
await faceapi.nets.faceRecognitionNet.loadFromUri('https://cdn.jsdelivr.net/npm/@vladmandic/face-api/model');
// Get reference faces
camLoadTxt.textContent = "Fetcfhing Access Data...";
const res = await fetch('api_get_reference_faces.php');
const data = await res.json();
if (!data.data || data.data.length === 0) {
camLoadTxt.textContent = "No registered users to match.";
return;
}
const labeledDescriptors = [];
let totalProcessed = 0;
for (const occ of data.data) {
camLoadTxt.textContent = `Processing reference... ${occ.name}`;
try {
const img = await faceapi.fetchImage(occ.foto + '?t=' + Date.now());
const detect = await faceapi.detectSingleFace(img).withFaceLandmarks().withFaceDescriptor();
if (detect) {
labeledDescriptors.push(new faceapi.LabeledFaceDescriptors(occ.name, [detect.descriptor]));
totalProcessed++;
}
} catch(e) { console.error("Could not process " + occ.name); }
}
if (totalProcessed === 0) {
camLoadTxt.textContent = "All photo references failed to load. Please call a technician.";
return;
}
// Create Matcher (Distance 0.55 makes sure it's accurate)
faceMatcher = new faceapi.FaceMatcher(labeledDescriptors, 0.55);
// Start Webcam
camLoadTxt.textContent = "Starting Webcam...";
stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user' } });
video.srcObject = stream;
video.onloadedmetadata = () => {
camLoading.style.display = 'none';
facePill.style.display = 'flex';
startMatching();
};
} catch (err) {
camLoadTxt.textContent = "Error: " + err.message;
console.error(err);
}
}
function startMatching() {
recognitionLoop = setInterval(async () => {
if (video.paused) return;
const detection = await faceapi.detectSingleFace(video).withFaceLandmarks().withFaceDescriptor();
if (detection) {
const bestMatch = faceMatcher.findBestMatch(detection.descriptor);
if (bestMatch.label !== 'unknown') {
// Berhasil Deteksi!
fpIcon.className = "mdi mdi-check-circle";
fpTxt.textContent = "Face Recognized!";
facePill.style.background = "#10b981";
facePill.style.color = "#fff";
clearInterval(recognitionLoop);
handleSuccess(bestMatch.label);
} else {
fpIcon.className = "mdi mdi-alert";
fpTxt.textContent = "Face Not Recognized";
facePill.style.background = "#ef4444";
facePill.style.color = "#fff";
}
} else {
fpIcon.className = "mdi mdi-magnify";
fpTxt.textContent = "Searching for Face...";
facePill.style.background = "rgba(255,255,255,0.9)";
facePill.style.color = "#0f172a";
}
}, 500);
}
function handleSuccess(name) {
// Stop Camera Streams
stream.getTracks().forEach(t => t.stop());
// Hide camera, show success logic
camWrapper.style.display = 'none';
ocName.textContent = name;
successView.style.display = 'flex';
// Show btn
btnUnlock.style.display = 'flex';
}
// Door Interactivity
btnUnlock.addEventListener('click', async () => {
// Play action unlock
btnUnlock.querySelector('#btnTxt').textContent = "Opening...";
btnUnlock.querySelector('#doorIcon').className = "mdi mdi-loading mdi-spin";
try {
// Post Unlock
let formData = new URLSearchParams();
formData.append("action", "unlock");
const res = await fetch("api_door_status.php", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: formData.toString()
});
const dat = await res.json();
if(dat.status === 'success') {
btnUnlock.classList.add('opened');
btnUnlock.querySelector('#doorIcon').className = "mdi mdi-door-open";
btnUnlock.querySelector('#btnTxt').textContent = "Door Unlocked!";
btnUnlock.style.pointerEvents = 'none';
// Show the Lock button
document.getElementById('btnLock').style.display = 'flex';
showToast("Door unlocked successfully! Access expires in 5 minutes.");
// Clear the earlier page countdown, force a new 5 minute door lock countdown
clearInterval(timeInterval);
document.getElementById('timerBadge').innerHTML = "<i class='mdi mdi-lock-clock'></i> Will automatically lock in 5 mins";
// Lock back after 5 minutes
doorTimer = setTimeout(() => {
lockTheDoor();
}, 300000); // 300 seconds
}
} catch(e) {
console.error("Failed handling door API", e);
}
});
// Lock The Door button
document.getElementById('btnLock').addEventListener('click', async () => {
clearTimeout(doorTimer); // Cancel auto-lock timer
await lockTheDoor();
});
async function lockTheDoor() {
const lockBtn = document.getElementById('btnLock');
lockBtn.querySelector('#lockTxt').textContent = 'Locking...';
lockBtn.querySelector('#lockIcon').className = 'mdi mdi-loading mdi-spin';
lockBtn.style.pointerEvents = 'none';
try {
// 1. Lock the door
let formLock = new URLSearchParams();
formLock.append("action", "lock");
await fetch("api_door_status.php", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: formLock.toString()
});
// 2. Turn off Bluetooth on ESP32
await fetch('api_wifi.php?action=trigger_bt_off');
showToast('Door locked and Bluetooth disabled.');
} catch(e) {
console.error('Lock error:', e);
}
// End session regardless
expireSession();
}
function showToast(msg) {
const t = document.getElementById('toastMsg');
t.textContent = msg;
t.style.display = 'block';
setTimeout(() => { t.style.display = 'none'; }, 5000);
}
init();
})();
</script>
</body>
</html>