Files
gravity/hosxp-webservice/views/login.php
T
2026-09-16 23:20:08 +07:00

444 lines
24 KiB
PHP

<?php
ini_set("display_errors", 1);
error_reporting(E_ALL);
// views/login.php
require_once("../core/security.php");
require_once("../core/settings.php");
require_once("../config/db.php");
configure_secure_session();
session_start();
send_security_headers();
// Handle Logout
if (isset($_GET['action']) && $_GET['action'] == 'logout') {
if (isset($_SESSION['account'])) {
require_once("../core/utils.php");
system_log($conn2, $_SESSION['account'], 'LOGOUT', ['reason' => 'User initiated']);
}
session_destroy();
header("Location: login.php");
exit();
}
// Handle Auto Logout (Timeout)
if (isset($_GET['action']) && $_GET['action'] == 'timeout') {
if (isset($_SESSION['account'])) {
require_once("../core/utils.php");
system_log($conn2, $_SESSION['account'], 'AUTO_LOGOUT', ['reason' => 'Inactivity timeout']);
}
session_destroy();
header("Location: login.php?error=timeout");
exit();
}
// Redirect if already logged in
if (!empty($_SESSION['sess_userid']) && $_SESSION['sess_userid'] === session_id()) {
header("Location: dashboard.php");
exit();
}
$error_msg = "";
if (isset($_GET['error']) && $_GET['error'] === 'timeout') {
$error_msg = "เซสชั่นหมดอายุ<br><span class='text-[11px] font-normal text-rose-400 mt-1 block'>เนื่องจากไม่มีการใช้งานระบบเป็นเวลานาน กรุณาล็อกอินใหม่อีกครั้ง</span>";
}
if ($_SERVER["REQUEST_METHOD"] == "POST") {
require_once("../core/utils.php");
require_once("../inc/rfc6238.php");
// CSRF Check
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
die("CSRF Token Validation Failed");
}
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
$ip_address = $_SERVER['REMOTE_ADDR'];
$otp_arr = $_POST['otp'] ?? [];
$otp_val = implode('', $otp_arr);
if (empty($username) || empty($password)) {
$error_msg = "ข้อมูลไม่ครบถ้วน<br><span class='text-[11px] font-normal text-rose-400 mt-1 block'>กรุณากรอก Username และ Password ให้ครบถ้วน</span>";
} else if (!check_rate_limit($conn2, $ip_address)) {
$error_msg = "บัญชีถูกระงับชั่วคราว<br><span class='text-[11px] font-normal text-rose-400 mt-1 block'>ท่านพยายามเข้าสู่ระบบผิดพลาดหลายครั้ง กรุณารอ 15 นาที</span>";
system_log($conn2, $username, 'LOGIN_BLOCKED', ['reason' => 'Rate limit exceeded']);
} else {
$stmt = $conn1->prepare("SELECT * FROM opduser WHERE loginname = ?");
$stmt->bind_param("s", $username);
$stmt->execute();
$result = $stmt->get_result();
$stmt->close();
if ($result->num_rows == 1) {
$row = $result->fetch_assoc();
if (isset($row['account_disable']) && $row['account_disable'] === 'Y') {
$error_msg = "บัญชีนี้ถูกยกเลิกการใช้งาน<br><span class='text-[11px] font-normal text-rose-400 mt-1 block'>กรุณาติดต่อผู้ดูแลระบบ (Admin) เพื่อขอเปิดสิทธิ์</span>";
system_log($conn2, $username, 'LOGIN_FAILED', ['reason' => 'Account disabled']);
} else if (md5($password) === $row['passweb']) {
// Check 2FA if user has secret key
$secretkey = '';
$stmt2 = $conn2->prepare("SELECT secret_key FROM sys_user_2fa WHERE loginname = ?");
if ($stmt2) {
$stmt2->bind_param("s", $username);
$stmt2->execute();
$res2 = $stmt2->get_result();
if ($res2->num_rows > 0) {
$secretkey = $res2->fetch_assoc()['secret_key'];
}
$stmt2->close();
}
$pass_2fa = true;
if (!empty($secretkey)) {
if (empty($otp_val)) {
$pass_2fa = false;
$error_msg = "ตรวจสอบสิทธิ์ล้มเหลว (2FA)<br><span class='text-[11px] font-normal text-rose-400 mt-1 block'>กรุณากรอกรหัสยืนยันตัวตน 6 หลัก จากแอปพลิเคชัน</span>";
system_log($conn2, $username, 'LOGIN_FAILED', ['reason' => 'Missing 2FA code']);
record_failed_login($conn2, $ip_address, $username);
} else {
$pass_2fa = TokenAuth6238::verify($secretkey, $otp_val);
if (!$pass_2fa) {
$error_msg = "รหัส 2FA ไม่ถูกต้อง<br><span class='text-[11px] font-normal text-rose-400 mt-1 block'>กรุณาตรวจสอบรหัสจาก Google Authenticator และลองอีกครั้ง</span>";
system_log($conn2, $username, 'LOGIN_FAILED', ['reason' => 'Invalid 2FA code']);
record_failed_login($conn2, $ip_address, $username);
}
}
}
if ($pass_2fa) {
session_regenerate_id();
$_SESSION['sess_userid'] = session_id();
$_SESSION['account'] = $row['loginname'];
$_SESSION['name'] = $row['name'];
$_SESSION['position'] = 'เจ้าหน้าที่'; // simplified
$_SESSION['ip'] = $_SERVER['REMOTE_ADDR'];
$client_os = getOS();
$client_browser = getBrowser();
system_log($conn2, $username, 'LOGIN_SUCCESS', [
'message' => 'Logged in successfully',
'os' => $client_os,
'browser' => $client_browser
]);
clear_failed_logins($conn2, $ip_address);
header("Location: dashboard.php");
exit();
}
} else {
record_failed_login($conn2, $ip_address, $username);
$error_msg = "รหัสผ่านไม่ถูกต้อง<br><span class='text-[11px] font-normal text-rose-400 mt-1 block'>กรุณาตรวจสอบตัวสะกด พิมพ์เล็ก/ใหญ่ แล้วลองอีกครั้ง</span>";
system_log($conn2, $username, 'LOGIN_FAILED', ['reason' => 'Invalid password']);
}
} else {
record_failed_login($conn2, $ip_address, $username);
$error_msg = "ไม่พบชื่อผู้ใช้งานนี้<br><span class='text-[11px] font-normal text-rose-400 mt-1 block'>ไม่มีชื่อผู้ใช้งานนี้ในระบบ HOSxP กรุณาตรวจสอบ Username อีกครั้ง</span>";
system_log($conn2, $username, 'LOGIN_FAILED', ['reason' => 'Username not found']);
}
}
}
?>
<!DOCTYPE html>
<html lang="th">
<head>
<?php require_once("../components/head.php"); ?>
<style>
/* Hide number arrows */
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type=number] {
-moz-appearance: textfield;
}
/* Custom animations */
@keyframes blob {
0% {
transform: translate(0px, 0px) scale(1);
}
33% {
transform: translate(30px, -50px) scale(1.1);
}
66% {
transform: translate(-20px, 20px) scale(0.9);
}
100% {
transform: translate(0px, 0px) scale(1);
}
}
.animate-blob {
animation: blob 7s infinite;
}
.animation-delay-2000 {
animation-delay: 2s;
}
.animation-delay-4000 {
animation-delay: 4s;
}
@keyframes shine {
0% {
left: -100%;
opacity: 0;
}
20% {
opacity: 1;
}
50% {
left: 100%;
opacity: 0;
}
100% {
left: 100%;
opacity: 0;
}
}
.animate-shine {
position: absolute;
top: 0;
width: 50%;
height: 100%;
background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.8) 50%, rgba(255, 255, 255, 0) 100%);
animation: shine 4s infinite;
transform: skewX(-20deg);
z-index: 20;
pointer-events: none;
}
</style>
</head>
<body class="flex items-center justify-center min-h-screen bg-[#cfe6d8] text-slate-800 font-sans antialiased relative overflow-hidden">
<!-- Background Blobs -->
<div class="absolute inset-0 overflow-hidden pointer-events-none z-0">
<div class="absolute top-[-10%] left-[-10%] w-[50vw] h-[50vw] rounded-full bg-emerald-200/60 mix-blend-multiply filter blur-[100px] opacity-70 animate-blob"></div>
<div class="absolute top-[20%] right-[-10%] w-[40vw] h-[40vw] rounded-full bg-teal-200/60 mix-blend-multiply filter blur-[100px] opacity-70 animate-blob animation-delay-2000"></div>
<div class="absolute bottom-[-20%] left-[20%] w-[60vw] h-[60vw] rounded-full bg-green-200/60 mix-blend-multiply filter blur-[100px] opacity-70 animate-blob animation-delay-4000"></div>
</div>
<!-- Main Card -->
<div class="relative w-full max-w-[460px] bg-white/40 backdrop-blur-2xl border-2 border-white/70 shadow-[0_8px_32px_rgba(16,185,129,0.15)] rounded-[2.5rem] p-8 sm:p-12 text-center m-4 z-10 before:absolute before:inset-0 before:bg-gradient-to-b before:from-white/50 before:to-transparent before:rounded-[2.5rem] before:pointer-events-none">
<!-- Logo -->
<div class="relative w-28 h-28 mx-auto mb-6 group">
<div class="absolute inset-0 bg-emerald-400/30 rounded-full filter blur-xl group-hover:bg-emerald-400/40 transition-colors duration-500"></div>
<div class="relative w-full h-full bg-white/80 backdrop-blur-md rounded-full shadow-[0_4px_20px_rgba(16,185,129,0.2)] border-[3px] border-white flex items-center justify-center p-0.5 overflow-hidden">
<img src="../img/logo.png" alt="Logo" class="w-full h-full object-contain drop-shadow-sm rounded-full relative z-10">
<div class="animate-shine"></div>
</div>
</div>
<!-- Titles -->
<h2 class="text-3xl font-extrabold text-slate-800 mb-1 tracking-tight">โรงพยาบาลเกาะสมุย</h2>
<p class="text-slate-500 font-medium tracking-wide mb-8">ระบบฐานข้อมูล HOSxP Data</p>
<div class="mb-8" style="display: none;">
<p class="text-emerald-600 font-bold text-[15px] tracking-wide mb-0.5">ยินดีต้อนรับเข้าสู่ระบบ</p>
<p class="text-slate-400 text-sm">กรุณาเข้าสู่ระบบเพื่อใช้งาน</p>
</div>
<?php if (!empty($error_msg)): ?>
<div class="bg-rose-50 text-rose-600 px-4 py-3 rounded-2xl mb-6 text-sm text-left font-medium flex gap-3 items-start border border-rose-100 shadow-sm animate-enter">
<svg class="w-5 h-5 shrink-0 mt-0.5 text-rose-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
</svg>
<div>
<?php echo $error_msg; ?>
</div>
</div>
<?php endif; ?>
<form id="loginForm" method="post" action="login.php" class="text-left space-y-5" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?php echo generate_csrf_token(); ?>">
<!-- Honeypot / Fake inputs to defeat browser autofill -->
<input type="text" style="display:none" name="fake_username" autocomplete="username">
<input type="password" style="display:none" name="fake_password" autocomplete="current-password">
<!-- Username Input -->
<div>
<label class="block text-[13px] font-bold text-slate-600 mb-2">ชื่อผู้ใช้งาน (Username)</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-emerald-600">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd"></path>
</svg>
</div>
<input type="text" name="username" class="w-full pl-11 pr-4 py-3.5 bg-white/80 border border-white focus:border-emerald-400 focus:ring-4 focus:ring-emerald-500/20 rounded-2xl outline-none transition-all font-medium text-slate-700 placeholder-slate-400 shadow-[inset_0_3px_8px_rgba(0,0,0,0.06),0_1px_2px_rgba(255,255,255,0.9)]" placeholder="บัญชีผู้ใช้" required autocomplete="new-password">
</div>
<p id="usernameWarning" class="text-rose-500 text-[11px] mt-1.5 ml-1 hidden font-medium animate-pulse">⚠️ ตรวจพบภาษาไทย กรุณาเปลี่ยนแป้นพิมพ์เป็นภาษาอังกฤษ</p>
</div>
<!-- Password Input -->
<div>
<label class="block text-[13px] font-bold text-slate-600 mb-2">รหัสผ่าน (Password)</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-emerald-600">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"></path>
</svg>
</div>
<input type="password" id="pwdInput" name="password" class="w-full pl-11 pr-12 py-3.5 bg-white/80 border border-white focus:border-emerald-400 focus:ring-4 focus:ring-emerald-500/20 rounded-2xl outline-none transition-all font-medium text-slate-700 placeholder-slate-400 shadow-[inset_0_3px_8px_rgba(0,0,0,0.06),0_1px_2px_rgba(255,255,255,0.9)]" placeholder="รหัสผ่าน" required autocomplete="new-password">
<button type="button" tabindex="-1" onclick="const p = document.getElementById('pwdInput'); p.type = p.type === 'password' ? 'text' : 'password';" class="absolute inset-y-0 right-0 pr-4 flex items-center text-emerald-600 hover:text-emerald-700 transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
</svg>
</button>
</div>
<p id="pwdWarning" class="text-rose-500 text-[11px] mt-1.5 ml-1 hidden font-medium animate-pulse">⚠️ ตรวจพบภาษาไทย กรุณาเปลี่ยนแป้นพิมพ์เป็นภาษาอังกฤษ</p>
</div>
<!-- 2FA Input -->
<div>
<label class="flex items-center text-[13px] font-bold text-slate-600 mb-2">
รหัส 2FA
<span class="ml-1.5 bg-white rounded-full shadow-[0_1px_3px_rgba(0,0,0,0.1)] p-0.5 inline-flex" title="Google Authenticator">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" class="w-[18px] h-[18px] animate-spin" style="animation-duration: 6s; animation-timing-function: linear;">
<line x1="50" y1="12" x2="50" y2="88" stroke="#34A853" stroke-width="16" stroke-linecap="round" />
<line x1="23" y1="23" x2="77" y2="77" stroke="#FBBC05" stroke-width="16" stroke-linecap="round" />
<line x1="12" y1="50" x2="88" y2="50" stroke="#4285F4" stroke-width="16" stroke-linecap="round" />
<line x1="77" y1="23" x2="23" y2="77" stroke="#EA4335" stroke-width="16" stroke-linecap="round" />
<circle cx="50" cy="50" r="14" fill="#ffffff" />
</svg>
</span>
</label>
<div class="flex gap-2 justify-between" id="otp-container">
<?php for ($i = 0; $i < 6; $i++): ?>
<input type="number" name="otp[]" maxlength="1" class="w-[50px] h-[56px] text-center text-2xl font-extrabold text-emerald-700 rounded-2xl border border-white bg-white/80 focus:outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-500/20 transition-all shadow-[inset_0_3px_8px_rgba(0,0,0,0.06),0_1px_2px_rgba(255,255,255,0.9)] otp-input" autocomplete="off" oninput="if(this.value.length > 1) this.value = this.value.slice(0,1);">
<?php endfor; ?>
</div>
</div>
<!-- Submit Button -->
<button type="submit" id="submitBtn" class="w-full bg-gradient-to-r from-emerald-500 to-green-600 hover:from-emerald-600 hover:to-green-700 text-white font-bold py-4 px-6 rounded-full shadow-[0_8px_20px_rgba(16,185,129,0.4),inset_0_2px_0_rgba(255,255,255,0.3)] hover:shadow-[0_10px_25px_rgba(16,185,129,0.5),inset_0_2px_0_rgba(255,255,255,0.4)] transition-all flex items-center justify-center gap-2 mt-8 transform hover:-translate-y-0.5 border border-emerald-400/50">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"></path>
</svg>
เข้าสู่ระบบ
</button>
</form>
<!-- Footer in Card -->
<div class="mt-8 pt-6 border-t border-emerald-500/10 flex items-center justify-center gap-2 text-[11px] font-semibold text-emerald-600/80">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M2.166 4.999A11.954 11.954 0 0010 1.944 11.954 11.954 0 0017.834 5c.11.65.166 1.32.166 2.001 0 5.225-3.34 9.67-8 11.317C5.34 16.67 2 12.225 2 7c0-.682.057-1.35.166-2.001zm11.541 3.708a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
</svg>
ระบบมีความปลอดภัยสูง ข้อมูลของท่านได้รับการปกป้อง
</div>
</div>
<!-- Absolute Footer -->
<?php
$app_settings = get_app_settings();
$changelogs = get_changelogs();
$latest_version = !empty($changelogs) ? $changelogs[0]['version'] : '';
$footer_html = str_replace('{YEAR}', date('Y'), $app_settings['footer_text'] ?? '');
?>
<div class="absolute bottom-6 left-0 w-full text-center text-xs text-emerald-800/50 font-medium z-10 flex flex-col sm:flex-row items-center justify-center gap-2 sm:gap-3">
<?php if (!empty($latest_version)): ?>
<span><?php echo htmlspecialchars($latest_version); ?></span>
<span class="text-emerald-800/30 hidden sm:inline">|</span>
<?php endif; ?>
<span><?php echo $footer_html; ?></span>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const otpInputs = document.querySelectorAll('.otp-input');
const loginForm = document.getElementById('loginForm');
// Language check for Username and Password
const checkLang = (inputEl, warningEl) => {
inputEl.addEventListener('input', (e) => {
// Check if value contains non-ASCII characters (e.g. Thai)
if (/[^\x00-\x7F]/.test(e.target.value)) {
warningEl.classList.remove('hidden');
} else {
warningEl.classList.add('hidden');
}
});
};
const usernameInput = document.querySelector('input[name="username"]');
const usernameWarning = document.getElementById('usernameWarning');
if (usernameInput && usernameWarning) checkLang(usernameInput, usernameWarning);
const pwdInput = document.getElementById('pwdInput');
const pwdWarning = document.getElementById('pwdWarning');
if (pwdInput && pwdWarning) checkLang(pwdInput, pwdWarning);
function checkAndSubmit() {
let allFilled = true;
otpInputs.forEach(inp => {
if (!inp.value) allFilled = false;
});
const uname = document.querySelector('input[name="username"]').value;
const pwd = document.querySelector('input[name="password"]').value;
if (allFilled && uname && pwd) {
// Optional: show a loading state on the button
const btn = document.getElementById('submitBtn');
if (btn) {
btn.innerHTML = '<svg class="animate-spin -ml-1 mr-2 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg> กำลังเข้าสู่ระบบ...';
btn.disabled = true;
}
loginForm.submit();
}
}
otpInputs.forEach((input, index) => {
input.addEventListener('input', (e) => {
if (e.target.value.length === 1) {
if (index < otpInputs.length - 1) {
otpInputs[index + 1].focus();
} else {
checkAndSubmit();
}
}
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Backspace' && !e.target.value && index > 0) {
otpInputs[index - 1].focus();
}
});
// Allow pasting 6 digits
input.addEventListener('paste', (e) => {
e.preventDefault();
const pastedData = e.clipboardData.getData('text').slice(0, 6);
if (/^\d+$/.test(pastedData)) {
pastedData.split('').forEach((char, i) => {
if (i < otpInputs.length) {
otpInputs[i].value = char;
}
});
const focusIndex = Math.min(pastedData.length, 5);
otpInputs[focusIndex].focus();
if (pastedData.length === 6) {
checkAndSubmit();
}
}
});
});
});
</script>
</body>
</html>