TKK_E32230273/finger.php

532 lines
24 KiB
PHP

<?php
session_start();
include 'koneksi.php';
$id_account = $_GET['id_account'] ?? null;
if (!$id_account) {
header("Location: register.php");
exit;
}
$error = '';
$differentDeviceUrl = '';
try {
$stmt = $pdo->prepare("SELECT finger_id, code_register FROM account WHERE id_account = ?");
$stmt->execute([$id_account]);
$acc = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$acc) {
header("Location: register.php");
exit;
}
if (!empty($acc['finger_id'])) {
$error = "Fingerprint for this account has already been registered!";
} else {
// Generate/retrieve code_register and build URL for scanning on different device
$code = $acc['code_register'] ?? null;
if (!$code) {
$code = sprintf("%06d", mt_rand(1, 999999));
$update = $pdo->prepare("UPDATE account SET code_register = ? WHERE id_account = ?");
$update->execute([$code, $id_account]);
}
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http";
$host = $_SERVER['HTTP_HOST'];
$uriPath = dirname($_SERVER['REQUEST_URI']);
if ($uriPath == '/' || $uriPath == '\\') $uriPath = '';
$differentDeviceUrl = $protocol . "://" . $host . $uriPath . "/scan_here.php?token=" . $code;
}
} catch (PDOException $e) {
$error = "A database error occurred.";
}
?>
<!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>Register Passkey - Identia</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" />
<style>
/* Custom styles for centered layout */
.auth-form-light {
text-align: center;
}
.brand-logo {
text-align: left;
margin-bottom: 2rem;
}
.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;
<?php
session_start();
include 'koneksi.php';
$id_account = $_GET['id_account'] ?? null;
if (!$id_account) {
header("Location: register.php");
exit;
}
$error = '';
$differentDeviceUrl = '';
try {
$stmt = $pdo->prepare("SELECT finger_id, code_register FROM account WHERE id_account = ?");
$stmt->execute([$id_account]);
$acc = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$acc) {
header("Location: register.php");
exit;
}
if (!empty($acc['finger_id'])) {
$error = "Fingerprint for this account has already been registered!";
} else {
// Generate/retrieve code_register and build URL for scanning on different device
$code = $acc['code_register'] ?? null;
if (!$code) {
$code = sprintf("%06d", mt_rand(1, 999999));
$update = $pdo->prepare("UPDATE account SET code_register = ? WHERE id_account = ?");
$update->execute([$code, $id_account]);
}
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http";
$host = $_SERVER['HTTP_HOST'];
$uriPath = dirname($_SERVER['REQUEST_URI']);
if ($uriPath == '/' || $uriPath == '\\') $uriPath = '';
$differentDeviceUrl = $protocol . "://" . $host . $uriPath . "/scan_here.php?token=" . $code;
}
} catch (PDOException $e) {
$error = "A database error occurred.";
}
?>
<!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>Register Passkey - 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" />
<style>
/* Custom styles for centered layout */
.auth-form-light {
text-align: center;
}
.brand-logo {
text-align: left;
margin-bottom: 2rem;
}
.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; }
body, .container-scroller, .page-body-wrapper, .content-wrapper {
background-color: #f8f9fa !important;
}
#rightSideBg { padding: 6rem 5rem; min-height: 100vh; }
@media (max-width: 991px) {
#rightSideBg { display: none !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">
<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;">Enable Touch ID</h2>
<p class="font-weight-light text-muted mb-0" style="font-size: 1rem;">Register your fingerprint for faster login.</p>
</div>
</div>
<?php if($error): ?>
<!-- Error will be shown via custom alert in script -->
<div class="mt-3">
<a href="login.php" class="btn btn-outline-primary btn-block">Cancel / Go to Login</a>
</div>
<?php else: ?>
<button type="button" id="registerFinger" class="btn btn-block btn-lg text-white font-weight-medium mb-3" style="background-color: #4B49AC; border-radius: 8px; padding: 0.8rem;">
Register fingerprint
</button>
<div class="mt-4 pt-4 border-top text-left">
<label class="font-weight-medium text-muted mb-2" style="font-size: 12px; display: block;">
Or scan fingerprint using a different device:
</label>
<div class="input-group mb-2">
<input type="text" class="form-control form-control-sm" id="differentDeviceLink" readonly value="<?= htmlspecialchars($differentDeviceUrl) ?>" style="font-size:11px; padding:4px; border-top-right-radius:0 !important; border-bottom-right-radius:0 !important;">
<div class="input-group-append">
<button class="btn btn-outline-light text-dark btn-sm" type="button" onclick="copyDifferentDeviceLink()" style="padding:4px 10px; border-top-left-radius:0 !important; border-bottom-left-radius:0 !important;"><i class="mdi mdi-content-copy mr-1"></i>Copy</button>
</div>
</div>
</div>
<div class="mt-4 pt-3 border-top">
<a href="login.php" style="text-decoration: underline; font-weight: 600; color: #101010ff; font-size: 14px; display: inline-block; transition: color 0.2s;" onmouseover="this.style.color='#4B49AC'" onmouseout="this.style.color='#6c757d'">Skip</a>
</div>
<?php endif; ?>
</div>
</div>
<!-- Right Side: Highly Innovative Tech Background -->
<div id="rightSideBg" class="col-lg-6 d-none d-lg-flex flex-column align-items-center justify-content-center position-relative" style="background-color: #4B49AC; overflow: hidden; padding: 6rem 5rem;">
<!-- 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;
});
canvas.addEventListener('mouseleave', function() {
mouse.x = null;
mouse.y = null;
});
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;
if (mouse.x != null && mouse.y != null) {
let dx = mouse.x - this.x;
let dy = mouse.y - this.y;
let distance = Math.sqrt(dx * dx + dy * dy);
if (distance < mouse.radius + this.size) {
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>
</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 src="js/off-canvas.js"></script>
<script src="js/hoverable-collapse.js"></script>
<script src="js/template.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);
}
function copyDifferentDeviceLink() {
const linkInput = document.getElementById('differentDeviceLink');
if (linkInput && linkInput.value) {
navigator.clipboard.writeText(linkInput.value).then(() => {
showCustomAlert("Success", "Link copied to clipboard successfully!", "success");
}).catch(err => {
console.error("Failed to copy link: ", err);
showCustomAlert("Error", "Failed to copy link.", "error");
});
}
}
<?php if(!$error): ?>
document.getElementById('registerFinger').addEventListener('click', async () => {
try {
if (!window.PublicKeyCredential) {
showCustomAlert("Not Supported", "This browser does not support WebAuthn (Passkey / Fingerprint).", "error");
return;
}
<?php
$stmtEx = $pdo->query("SELECT finger_id FROM account WHERE finger_id IS NOT NULL AND finger_id != ''");
$allCredsArray = $stmtEx->fetchAll(PDO::FETCH_COLUMN);
?>
const allExistingCreds = <?= json_encode($allCredsArray) ?>;
const excludeList = allExistingCreds.map(idBase64 => ({
type: "public-key",
id: Uint8Array.from(atob(idBase64), c => c.charCodeAt(0))
}));
const publicKey = {
challenge: new Uint8Array(32),
excludeCredentials: excludeList,
rp: {
name: "Identia App",
id: window.location.hostname
},
user: {
id: Uint8Array.from("uid_<?= $id_account ?>", c => c.charCodeAt(0)),
name: "Identia_user",
displayName: "Identia_user"
},
pubKeyCredParams: [{type: "public-key", alg: -7}, {type: "public-key", alg: -257}],
authenticatorSelection: {
authenticatorAttachment: "platform",
residentKey: "required",
userVerification: "required"
},
timeout: 60000,
attestation: "none"
};
const credential = await navigator.credentials.create({ publicKey });
// Cek ketersediaan credential
if(credential) {
const credId = btoa(String.fromCharCode.apply(null, new Uint8Array(credential.rawId)));
const formData = new FormData();
formData.append('id_account', '<?= $id_account ?>');
formData.append('credential_id', credId);
const res = await fetch('save_fingerprint_account.php', {
method: 'POST',
body: formData
});
const result = await res.json();
if (result.status === 'success') {
showCustomAlert("Success", "Fingerprint registered successfully. You can use it to login.", "success");
setTimeout(() => { window.location.href = 'login.php'; }, 2000);
} else {
showCustomAlert("Failed", "Server error: " + result.message, "error");
}
}
} catch(err) {
console.error(err);
showCustomAlert("Cancelled", "The process was cancelled or the fingerprint could not be read.", "warning");
}
});
<?php endif; ?>
// Tampilkan error PHP jika ada menggunakan custom alert
<?php if($error): ?>
showCustomAlert("Attention", "<?= addslashes(htmlspecialchars($error)) ?>", "warning");
<?php endif; ?>
// Enforce Notification Permission
document.addEventListener('DOMContentLoaded', () => {
if ('Notification' in window && Notification.permission !== 'granted') {
Notification.requestPermission();
}
});
</script>
</body>
</html>