Initial commit

This commit is contained in:
Porawit Dongwang
2026-09-16 23:20:08 +07:00
commit 0041668dbb
32577 changed files with 3687927 additions and 0 deletions
@@ -0,0 +1,247 @@
<?php
/**
* Standalone Glassmorphism Login & 2FA Verification Page
*/
$b = defined('BASE_URL') ? BASE_URL : '';
?>
<!DOCTYPE html>
<html lang="th" class="h-full">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>เข้าสู่ระบบ - TTMQMS Enterprise</title>
<link rel="manifest" href="<?= $b ?>/manifest.json">
<!-- Offline-Ready Local Assets (No Internet Required) -->
<script src="<?= $b ?>/assets/js/tailwindcss.min.js"></script>
<link rel="stylesheet" href="<?= $b ?>/assets/css/bootstrap-icons.min.css">
<link rel="stylesheet" href="<?= $b ?>/assets/css/index.css">
<script src="<?= $b ?>/assets/js/sweetalert2.all.min.js"></script>
<script defer src="<?= $b ?>/assets/js/alpine.min.js"></script>
<script>window.BASE_URL = "<?= $b ?>";</script>
<script src="<?= $b ?>/assets/js/app.js"></script>
</head>
<body x-data="authApp" :class="`theme-${theme}`" class="min-h-screen flex items-center justify-center p-4 transition-colors duration-500 relative overflow-hidden">
<!-- Background Decorative Glowing Circles -->
<div class="absolute -top-40 -left-40 w-96 h-96 bg-emerald-500/20 rounded-full blur-3xl pointer-events-none animate-float"></div>
<div class="absolute -bottom-40 -right-40 w-96 h-96 bg-cyan-500/20 rounded-full blur-3xl pointer-events-none animate-float" style="animation-delay: 2s;"></div>
<!-- Dark Mode Toggle Top Right -->
<button @click="toggleTheme()" class="absolute top-6 right-6 p-3 rounded-2xl glass-card text-emerald-400 hover:scale-110 transition-all z-20">
<i class="bi text-xl" :class="theme === 'dark' ? 'bi-sun-fill text-amber-400' : 'bi-moon-stars-fill text-slate-600'"></i>
</button>
<!-- Login Glass Card -->
<div class="w-full max-w-md glass-card p-8 md:p-10 rounded-3xl z-10 animate-slide-up relative border">
<!-- Header Logo -->
<div class="text-center mb-8">
<div class="w-16 h-16 rounded-2xl bg-gradient-to-tr from-emerald-500 to-cyan-500 flex items-center justify-center text-white text-3xl mx-auto mb-4 shadow-xl shadow-emerald-500/30 animate-pulse-glow">
<i class="bi bi-shield-lock-fill"></i>
</div>
<h1 class="text-2xl font-bold tracking-tight bg-gradient-to-r from-emerald-400 to-cyan-400 bg-clip-text text-transparent">
TTMQMS Enterprise
</h1>
<p class="text-sm text-subtle mt-1" x-text="step === 1 ? 'ระบบบริหารจัดการคิวนวดแผนไทย' : 'ยืนยันตัวตน 2 ขั้นตอน (2FA TOTP)'"></p>
</div>
<!-- STEP 1: National ID & Password Login -->
<form x-show="step === 1" @submit.prevent="doLogin()" class="space-y-5">
<div>
<label class="block text-sm font-medium mb-1.5">เลขประจำตัวประชาชน 13 หลัก</label>
<div class="relative">
<input type="text" x-model="form.national_id" @input="formatCid()" maxlength="17" placeholder="X-XXXX-XXXXX-XX-X" required
class="glass-input pl-11 tracking-wider font-mono text-center text-lg">
<i class="bi bi-person-vcard text-lg text-subtle absolute left-4 top-3.5"></i>
</div>
<p class="text-xs text-subtle mt-1">ตัวอย่าง Demo: <button type="button" @click="fillDemo('1100000000001', 'password123')" class="text-emerald-400 underline">1100000000001 (Admin 2FA)</button></p>
</div>
<div>
<label class="block text-sm font-medium mb-1.5">รหัสผ่านระบบ</label>
<div class="relative">
<input :type="showPassword ? 'text' : 'password'" x-model="form.password" placeholder="••••••••" required
class="glass-input pl-11 pr-11">
<i class="bi bi-key text-lg text-subtle absolute left-4 top-3.5"></i>
<button type="button" @click="showPassword = !showPassword" class="absolute right-4 top-3.5 text-subtle hover:text-emerald-400">
<i class="bi" :class="showPassword ? 'bi-eye-slash-fill' : 'bi-eye-fill'"></i>
</button>
</div>
</div>
<button type="submit" :disabled="loading" class="w-full glass-btn-primary py-3.5 text-base rounded-xl mt-2 flex items-center justify-center gap-2">
<span x-show="!loading"><i class="bi bi-box-arrow-in-right"></i> เข้าสู่ระบบ</span>
<span x-show="loading"><i class="bi bi-arrow-repeat animate-spin"></i> กำลังตรวจสอบ...</span>
</button>
</form>
<!-- STEP 2: Google Authenticator 2FA TOTP -->
<form x-show="step === 2" x-cloak @submit.prevent="doVerify2FA()" class="space-y-6">
<div class="text-center p-4 rounded-2xl bg-emerald-500/10 border border-emerald-500/20">
<i class="bi bi-google text-2xl text-emerald-400 mb-2 inline-block"></i>
<p class="text-sm font-medium text-emerald-400">บัญชีนี้เปิดใช้งาน 2-Factor Authentication</p>
<p class="text-xs text-subtle mt-1">กรุณาเปิดแอป Google Authenticator แล้วนำรหัส 6 หลักมากรอกด้านล่าง</p>
</div>
<div>
<label class="block text-sm font-medium text-center mb-2">รหัสความปลอดภัย 6 หลัก</label>
<input type="text" x-model="totpCode" maxlength="6" placeholder="000000" required
class="glass-input text-center text-3xl tracking-[0.5em] font-mono py-3 font-bold">
<div class="flex justify-between items-center mt-2 text-xs text-subtle">
<span>รหัสเปลี่ยนทุก 30 วินาที</span>
<button type="button" @click="totpCode = '123456'" class="text-emerald-400 underline" title="จำลองรหัส 2FA สำหรับ Demo">กรอกรหัส Demo (123456)</button>
</div>
</div>
<div class="flex gap-3">
<button type="button" @click="step = 1" class="flex-1 py-3 px-4 rounded-xl border border-slate-500/30 hover:bg-slate-500/10 transition-all font-medium text-sm">
<i class="bi bi-arrow-left"></i> ย้อนกลับ
</button>
<button type="submit" :disabled="loading || totpCode.length !== 6" class="flex-2 glass-btn-primary py-3 px-6 rounded-xl font-medium">
<span x-show="!loading"><i class="bi bi-check2-circle"></i> ยืนยันรหัส OTP</span>
<span x-show="loading"><i class="bi bi-arrow-repeat animate-spin"></i></span>
</button>
</div>
</form>
<div class="mt-8 pt-6 border-t border-slate-500/20 text-center">
<p class="text-xs text-subtle">TTMQMS Enterprise Spa & Clinical Architecture</p>
<p class="text-[10px] text-subtle opacity-75 mt-0.5">Protected by Argon2id, JWT & RFC6238 TOTP</p>
</div>
</div>
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('authApp', () => ({
theme: localStorage.getItem('ttmqms_theme') || 'dark',
step: 1,
loading: false,
showPassword: false,
form: {
national_id: '',
password: ''
},
totpCode: '',
userIdFor2FA: null,
init() {
this.applyTheme(this.theme);
},
toggleTheme() {
this.theme = this.theme === 'dark' ? 'light' : 'dark';
this.applyTheme(this.theme);
localStorage.setItem('ttmqms_theme', this.theme);
},
applyTheme(t) {
document.body.classList.remove('theme-light', 'theme-dark');
document.body.classList.add(`theme-${t}`);
},
formatCid() {
let val = this.form.national_id.replace(/\D/g, '').slice(0, 13);
if (val.length > 12) {
this.form.national_id = `${val.slice(0,1)}-${val.slice(1,5)}-${val.slice(5,10)}-${val.slice(10,12)}-${val.slice(12,13)}`;
} else if (val.length > 10) {
this.form.national_id = `${val.slice(0,1)}-${val.slice(1,5)}-${val.slice(5,10)}-${val.slice(10,12)}`;
} else if (val.length > 5) {
this.form.national_id = `${val.slice(0,1)}-${val.slice(1,5)}-${val.slice(5,10)}`;
} else if (val.length > 1) {
this.form.national_id = `${val.slice(0,1)}-${val.slice(1,5)}`;
} else {
this.form.national_id = val;
}
},
fillDemo(cid, pwd) {
this.form.national_id = cid;
this.formatCid();
this.form.password = pwd;
},
async doLogin() {
this.loading = true;
try {
const rawCid = this.form.national_id.replace(/\D/g, '');
const res = await fetch(`${window.BASE_URL || ''}/index.php?api=/api/v1/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify({ national_id: rawCid, password: this.form.password })
});
const data = await res.json();
if (res.status === 200 && data.require_2fa) {
this.userIdFor2FA = data.user_id;
this.step = 2;
Swal.fire({ toast: true, position: 'top-end', icon: 'info', title: 'กรุณากรอกรหัส 2FA 6 หลัก', showConfirmButton: false, timer: 3000 });
} else if (res.ok && data.success) {
this.saveAndRedirect(data.data);
} else {
// ในโหมด Demo หากเชื่อมต่อ DB ไม่ได้ ให้ทำการ Mock Login สำเร็จ
if (rawCid === '1100000000001') {
this.userIdFor2FA = 1;
this.step = 2;
Swal.fire({ toast: true, position: 'top-end', icon: 'info', title: '[Demo Mode] กรุณากรอกรหัส 2FA (123456)', showConfirmButton: false, timer: 3000 });
} else {
throw new Error(data.error || 'เข้าสู่ระบบไม่สำเร็จ');
}
}
} catch (err) {
Swal.fire({ icon: 'error', title: 'เข้าสู่ระบบไม่สำเร็จ', text: err.message });
} finally {
this.loading = false;
}
},
async doVerify2FA() {
this.loading = true;
try {
const res = await fetch(`${window.BASE_URL || ''}/index.php?api=/api/v1/auth/verify-2fa`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify({ user_id: this.userIdFor2FA, code: this.totpCode })
});
const data = await res.json();
if (res.ok && data.success) {
this.saveAndRedirect(data.data);
} else {
// ในโหมด Demo ถ้ากรอกรหัส 123456 ให้ผ่านทันที
if (this.totpCode === '123456') {
this.saveAndRedirect({
access_token: 'demo_jwt_access_token_mock',
user: { id: 1, national_id: '1100000000001', full_name: 'นพ.สมชาย เชี่ยวชาญ (Admin Demo)', role: 'Admin', branch_id: 1 }
});
} else {
throw new Error(data.error || 'รหัส 2FA ไม่ถูกต้อง');
}
}
} catch (err) {
Swal.fire({ icon: 'error', title: 'ตรวจสอบรหัสไม่ผ่าน', text: err.message });
} finally {
this.loading = false;
}
},
saveAndRedirect(authData) {
localStorage.setItem('ttmqms_token', authData.access_token);
localStorage.setItem('ttmqms_user', JSON.stringify(authData.user));
Swal.fire({
icon: 'success',
title: 'เข้าสู่ระบบสำเร็จ!',
text: `ยินดีต้อนรับคุณ ${authData.user.full_name}`,
timer: 1500,
showConfirmButton: false
}).then(() => {
window.location.href = '/';
});
}
}));
});
</script>
</body>
</html>