This commit is contained in:
Warunee Tamkoo 2024-12-16 23:13:43 +07:00
parent 50eecafad2
commit b8e1c415e1
18 changed files with 601 additions and 110 deletions

View file

@ -5,16 +5,6 @@
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>HRMS - Landing Page</title> <title>HRMS - Landing Page</title>
<style>
html,
body,
#viewDiv {
padding: 0;
margin: 0;
height: 100%;
width: 100%;
}
</style>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View file

@ -11,26 +11,31 @@
"format": "prettier ./src --write" "format": "prettier ./src --write"
}, },
"dependencies": { "dependencies": {
"@arcgis/core": "^4.28.10", "@fullcalendar/core": "^6.1.8",
"@fullcalendar/daygrid": "^6.1.8",
"@fullcalendar/interaction": "^6.1.8",
"@fullcalendar/list": "^6.1.8",
"@fullcalendar/timegrid": "^6.1.8",
"@fullcalendar/vue3": "^6.1.8",
"@googlemaps/js-api-loader": "^1.16.2",
"@quasar/extras": "^1.15.8", "@quasar/extras": "^1.15.8",
"@vuepic/vue-datepicker": "^5.2.1", "@vuepic/vue-datepicker": "^5.2.1",
"keycloak-js": "^22.0.2", "keycloak-js": "^22.0.2",
"moment": "^2.29.4", "moment": "^2.29.4",
"pinia": "^2.1.4", "pinia": "^2.1.4",
"quasar": "^2.17.5", "quasar": "^2.11.1",
"register-service-worker": "^1.7.2", "register-service-worker": "^1.7.2",
"sass": "^1.83.0",
"simple-vue-camera": "^1.1.3", "simple-vue-camera": "^1.1.3",
"vite-plugin-pwa": "^0.16.7", "vite-plugin-pwa": "^0.16.7",
"vue": "^3.4.15", "vue": "^3.4.15",
"vue-router": "^4.5.0" "vue-router": "^4.1.6",
"vue3-google-map": "^0.18.0"
}, },
"devDependencies": { "devDependencies": {
"@quasar/vite-plugin": "^1.3.0", "@quasar/vite-plugin": "^1.3.0",
"@rushstack/eslint-patch": "^1.1.4", "@rushstack/eslint-patch": "^1.1.4",
"@types/jsdom": "^20.0.1", "@types/jsdom": "^20.0.1",
"@types/node": "^18.18.10", "@types/node": "^18.18.10",
"@types/vue-router": "^2.0.0",
"@vitejs/plugin-vue": "^4.0.0", "@vitejs/plugin-vue": "^4.0.0",
"@vitejs/plugin-vue-jsx": "^3.0.0", "@vitejs/plugin-vue-jsx": "^3.0.0",
"@vue/eslint-config-prettier": "^7.0.0", "@vue/eslint-config-prettier": "^7.0.0",
@ -44,10 +49,13 @@
"jsdom": "^20.0.3", "jsdom": "^20.0.3",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"sass": "^1.83.0",
"sass-loader": "^16.0.4",
"start-server-and-test": "^1.15.2", "start-server-and-test": "^1.15.2",
"typescript": "~4.7.4", "typescript": "~4.7.4",
"vite": "^4.0.0", "vite": "^4.0.0",
"vitest": "^0.25.6", "vitest": "^0.25.6",
"vue-loader": "^17.4.2",
"vue-tsc": "^1.0.12" "vue-tsc": "^1.0.12"
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1 MiB

Before After
Before After

BIN
src/assets/3135715.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
src/assets/bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

BIN
src/assets/line2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
src/assets/screen1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

BIN
src/assets/screen2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
src/assets/screen3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

BIN
src/assets/screen4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

View file

@ -0,0 +1,90 @@
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide" persistent>
<q-card class="q-pa-sm">
<q-card-section class="row">
<div class="q-pr-md">
<q-avatar
:icon="icon"
size="lg"
font-size="25px"
color="blue-1"
:text-color="color"
/>
</div>
<div class="col text-dark">
<span class="text-bold">{{ title }}</span>
<br />
<span>{{ message }}</span>
</div>
</q-card-section>
<q-card-actions
align="right"
class="bg-white text-teal"
v-if="onlycancel"
>
<q-btn label="ตกลง" flat color="grey-8" @click="onDialogCancel" />
<!-- <q-btn :label="textOk" :color="color" @click="onOKClick" /> -->
</q-card-actions>
<q-card-actions align="right" class="bg-white text-teal" v-else>
<q-btn label="ยกเลิก" flat color="grey-8" @click="onDialogCancel" />
<q-btn :label="textOk" :color="color" @click="onOKClick" />
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
import { useDialogPluginComponent } from "quasar";
const props = defineProps({
color: {
type: String,
default: "primary",
},
textOk: {
type: String,
default: "ตกลง",
},
title: {
type: String,
default: "หัวข้อ?",
},
message: {
type: String,
default: "ข้อความ",
},
icon: {
type: String,
default: "question_mark",
},
onlycancel: {
type: Boolean,
default: false,
},
});
defineEmits([
// REQUIRED; need to specify some events that your
// component will emit through useDialogPluginComponent()
...useDialogPluginComponent.emits,
]);
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
useDialogPluginComponent();
// dialogRef - Vue ref to be applied to QDialog
// onDialogHide - Function to be used as handler for @hide on QDialog
// onDialogOK - Function to call to settle dialog with "ok" outcome
// example: onDialogOK() - no payload
// example: onDialogOK({ /*...*/ }) - with payload
// onDialogCancel - Function to call to settle dialog with "cancel" outcome
// this is part of our example (so not required)
function onOKClick() {
// on OK, it is REQUIRED to
// call onDialogOK (with optional payload)
onDialogOK();
// or with payload: onDialogOK({ ... })
// ...and it will also hide the dialog automatically
}
</script>

27
src/plugins/cookie.ts Normal file
View file

@ -0,0 +1,27 @@
// authen with keycloak client
function setCookie(name: string, value: string, days: number) {
let expires = "";
if (days) {
const date = new Date();
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
expires = "; expires=" + date.toUTCString();
}
document.cookie = name + "=" + (value || "") + expires + "; path=/";
}
function getCookie(name: string) {
const nameEQ = name + "=";
const ca = document.cookie.split(";");
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == " ") c = c.substring(1, c.length);
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
function deleteCookie(name: string) {
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
}
export { setCookie, getCookie, deleteCookie };

View file

@ -1,70 +0,0 @@
// authen with keycloak client
import Keycloak from "keycloak-js";
// const ACCESS_TOKEN = 'BMAHRIS_KEYCLOAK_IDENTITY'
// const REFRESH_TOKEN = 'BMAHRIS_KEYCLOAK_REFRESH'
// const keycloakConfig = {
// url: import.meta.env.VITE_URL_KEYCLOAK,
// realm: import.meta.env.VITE_REALM_KEYCLOAK,
// clientId: import.meta.env.VITE_CLIENTID_KEYCLOAK,
// clientSecret: import.meta.env.VITE_CLIENTSECRET_KEYCLOAK,
// }
// const keycloak = new Keycloak(keycloakConfig)
// async function kcAuthen(access_token: string, refresh_token: string) {
// await setCookie(ACCESS_TOKEN, access_token, 1)
// await setCookie(REFRESH_TOKEN, refresh_token, 1)
// window.location.href = '/'
// }
// async function kcLogout() {
// await deleteCookie(ACCESS_TOKEN)
// await deleteCookie(REFRESH_TOKEN)
// if (keycloak.authenticated !== undefined) {
// keycloak.logout()
// }
// window.location.href = '/login'
// }
// async function getToken() {
// return {
// token: getCookie(ACCESS_TOKEN),
// refresh_token: getCookie(REFRESH_TOKEN),
// }
// }
// function setCookie(name: string, value: any, days: number) {
// let expires = ''
// if (days) {
// const date = new Date()
// date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000)
// expires = '; expires=' + date.toUTCString()
// }
// document.cookie = name + '=' + (value || '') + expires + '; path=/'
// }
// function getCookie(name: string) {
// const nameEQ = name + '='
// const ca = document.cookie.split(';')
// for (let i = 0; i < ca.length; i++) {
// let c = ca[i]
// while (c.charAt(0) == ' ') c = c.substring(1, c.length)
// if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length)
// }
// return null
// }
// function deleteCookie(name: string) {
// document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`
// }
// export default keycloak
// export {
// keycloakConfig,
// getToken,
// kcAuthen,
// kcLogout,
// ACCESS_TOKEN,
// REFRESH_TOKEN,
// }

View file

@ -1,5 +1,6 @@
import { createRouter, createWebHistory } from "vue-router"; import { createRouter, createWebHistory } from "vue-router";
const homeView = () => import("@/views/home.vue"); const homeView = () => import("@/views/home.vue");
const ssoPage = () => import("@/views/sso.vue");
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
@ -23,6 +24,14 @@ const router = createRouter({
}, },
], ],
}, },
{
path: "/sso",
name: "sso-page",
component: ssoPage,
meta: {
Auth: false,
},
},
], ],
}); });

View file

@ -1,16 +1,173 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted } from "vue"; import { deleteCookie, getCookie, setCookie } from "@/plugins/cookie";
import router from "@/router";
import { computed, onMounted, ref } from "vue";
import { useQuasar } from "quasar";
import axios from "axios";
import CustomComponent from "@/components/CustomDialog.vue";
const $q = useQuasar();
const urlAdmin = import.meta.env.VITE_URL_ADMIN ?? "";
const urlUser = import.meta.env.VITE_URL_USER ?? "";
const urlMgt = import.meta.env.VITE_URL_MGT ?? "";
const urlCheckin = import.meta.env.VITE_URL_CHECKIN ?? "";
const token = ref<any>("");
const refreshToken = ref<any>("");
const fullname = computed(() => {
if (token.value) {
const base64Url = token.value.split(".")[1];
// Base64 URL-safe Base64
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
// Base64
const decoded = atob(base64);
const decodedData = JSON.parse(decoded);
//
return decodeURIComponent(escape(decodedData.name));
} else return "";
});
async function goPage(sys: string, url: string) {
// Payload JWT ( 2)
const base64Url = token.value.split(".")[1];
// Base64 URL-safe Base64
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
// Base64
const decoded = atob(base64);
// requiredRole sys
let requiredRole: string[] = [];
if (sys === "user" || sys === "checkin") {
requiredRole = ["USER"];
} else if (sys === "mgt") {
requiredRole = ["STAFF"]; // sys "ADMIN"
} else if (sys === "admin") {
requiredRole = ["ADMIN", "SUPER_ADMIN"];
}
console.log("requiredRole===>", requiredRole);
console.log("decoded===>", JSON.parse(decoded).realm_access.roles);
// payload.role role
if (
requiredRole.some((role) =>
JSON.parse(decoded).realm_access.roles.includes(role)
)
) {
window.location.href = `${url}/auth?token=${token.value}&accessToken=${refreshToken.value}`;
} else {
// alert("");
$q.dialog({
component: CustomComponent,
componentProps: {
title: `แจ้งเตือน`,
message: "คุณไม่มีสิทธิ์เข้าใช้งานระบบนี้",
icon: "warning",
color: "red",
onlycancel: true,
},
});
}
}
async function logout() {
await deleteCookie("BMAHRIS_KEYCLOAK_IDENTITY");
await deleteCookie("BMAHRIS_KEYCLOAK_REFRESH");
router.push("/sso");
}
onMounted(async () => { onMounted(async () => {
// const checkAuthen = await authenticated(); token.value = await getCookie("BMAHRIS_KEYCLOAK_IDENTITY");
// if (checkAuthen) { refreshToken.value = await getCookie("BMAHRIS_KEYCLOAK_REFRESH");
// router.push("/");
// } deleteCookie("BMAHRISADM_KEYCLOAK_IDENTITY");
deleteCookie("BMAHRISCKI_KEYCLOAK_IDENTITY");
deleteCookie("BMAHRISUSER_KEYCLOAK_IDENTITY");
const checkToken = (await token.value) ?? null;
if (!checkToken && !token.value) {
await axios
.post(
`${import.meta.env.VITE_SSO_URL}/kcauth`,
{},
{
headers: {
"Content-Type": "application/json",
},
withCredentials: true, // Include cookies with the request
}
)
.then((res: any) => {
console.log("res===>", res);
if (res.status === 200) {
setCookie("BMAHRIS_KEYCLOAK_IDENTITY", res.data.access_token, 1);
setCookie("BMAHRIS_KEYCLOAK_REFRESH", res.data.refresh_token, 1);
}
})
.catch((err: any) => {
router.push("/sso");
});
}
}); });
</script> </script>
<template> <template>
<h1>Lanading page</h1> <div class="q-ma-md">
<div class="row">
{{ fullname }}
<q-btn @click="logout()" class="btn_logout">
ออกจากระบบ <i class="mdi mdi-logout"></i>
</q-btn>
</div>
<div class="row">
<div class="col-4">
<img src="@/assets/screen1.png" style="width: 100%" />
<div class="h-100 py-3">
<a @click="goPage('user', urlUser)" class="link">
ระบบบรการเจาของขอม
</a>
</div>
</div>
<div class="col-4">
<img src="@/assets/screen2.png" style="width: 100%" />
<div class="h-100 py-3">
<a @click="goPage('checkin', urlCheckin)" class="link">
ระบบลงเวลาปฏราชการ
</a>
</div>
</div>
<div class="col-4">
<img src="@/assets/screen3.png" style="width: 100%" />
<div class="h-100 py-3">
<a @click="goPage('mgt', urlMgt)" class="link"> ระบบบรหารจดการ </a>
</div>
</div>
<div class="col-4">
<img src="@/assets/screen4.png" style="width: 100%" />
<div class="h-100 py-3">
<a @click="goPage('admin', urlAdmin)" class="link"> ระบบแอดม </a>
</div>
</div>
</div>
</div>
</template> </template>
<style lang="scss" scoped></style> <style lang="scss" scoped>
.link {
cursor: pointer;
}
</style>

298
src/views/sso.vue Normal file
View file

@ -0,0 +1,298 @@
<!-- authen with keycloak client -->
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { useQuasar } from "quasar";
import axios from "axios";
import { useRouter } from "vue-router";
import { useCounterMixin } from "@/stores/mixin";
const $q = useQuasar();
const router = useRouter();
const mixin = useCounterMixin();
const { showLoader, hideLoader } = mixin;
const username = ref<string>(""); //
const password = ref<string>(""); //
const msgError = ref<string>(""); //
/**
* งกนเขาสระบบ
*/
async function onSubmit() {
showLoader();
msgError.value = "";
// const formdata = new URLSearchParams();
// formdata.append("username", username.value);
// formdata.append("password", password.value);
await axios
.post(
`${import.meta.env.VITE_SSO_URL}/signin`,
{
username: username.value,
password: password.value,
},
{
headers: {
"Content-Type": "application/json",
},
withCredentials: true, // Include cookies with the request
}
)
.then((res) => {
if (res.data === "OK") {
router.push("/");
}
console.log("res===>", res);
})
.catch((err) => {
msgError.value = "ชื่อผู้ใช้งานหรือรหัสผ่านไม่ถูกต้อง";
})
.finally(() => {
hideLoader();
});
}
/**
* ทำงานเม components กเรยกใชงาน
*/
onMounted(async () => {});
</script>
<template>
<div class="bg-image">
<div class="shadow-top"></div>
<div class="shadow-bottom"></div>
<div class="row fit items-center">
<div class="row full-width justify-evenly">
<div class="">
<div id="myimage"></div>
<div class="text-logo">
ระบบบรหารจดการการใชงาน <br />ระบบสารสนเทศสนบสนนการเชอมโยง
</div>
<div class="subtext-logo">
Bangkok Metropolitan Administration Single Sign-On
</div>
</div>
<img src="@/assets/line.png" class="img_absolute_line" />
<div class="card-pf">
<div class="row q-gutter-sm items-center q-mb-sm">
<div class="column">
<div class="login-pf-header">
<h1 id="kc-page-title">เขาใชงานระบบ</h1>
</div>
</div>
</div>
<q-form
greedy
@submit.prevent
@validation-success="onSubmit"
style="max-width: 100%; min-width: 30%"
>
<div class="row">
<div class="col-12 form-group">
<label
for="username"
class="pf-c-form__label pf-c-form__label-text"
>อผใชงาน</label
>
<q-input
v-model="username"
dense
outlined
lazy-rules
hide-bottom-space
:rules="[(val:string) => !!val || `${'กรุณากรอกชื่อผู้ใช้งาน'}`,]"
></q-input>
</div>
<div class="col-12 form-group">
<label
for="username"
class="pf-c-form__label pf-c-form__label-text"
>รหสผาน</label
>
<q-input
v-model="password"
dense
outlined
lazy-rules
hide-bottom-space
type="password"
:rules="[(val:string) => !!val || `${'กรุณากรอกรหัสผ่าน'}`,]"
></q-input>
</div>
<div class="col-12 text-red text-center">{{ msgError }}</div>
<div class="col-12">
<q-btn
unelevated
color="primary"
class="full-width"
label="เข้าระบบ"
style="font-size: 16px; border-radius: 8px"
type="submit"
>
</q-btn>
</div>
</div>
</q-form>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.shadow-top {
position: absolute;
height: 4rem;
width: 4rem;
border-radius: 50%;
background-color: $primary;
top: -4rem;
right: -4rem;
opacity: 0.5;
box-shadow: 0px 0px 300px 320px $primary;
backdrop-filter: blur(550px);
}
.shadow-bottom {
position: absolute;
height: 4rem;
width: 4rem;
border-radius: 50%;
background-color: $primary;
bottom: -4rem;
left: -4rem;
opacity: 0.5;
box-shadow: 0px 0px 300px 320px $primary;
backdrop-filter: blur(550px);
}
.color-icon {
border-radius: 8px;
}
.link {
text-decoration: none;
color: #cc0004;
}
.link:hover {
color: #ff0004;
text-decoration: underline;
}
.checkbox,
.radio {
position: relative;
display: block;
margin-top: 10px;
margin-bottom: 10px;
}
input[type="checkbox"],
input[type="radio"] {
margin: 1px 3px 0 0;
line-height: normal;
}
.checkbox label,
.radio label {
min-height: 20px;
margin-bottom: 0;
font-weight: 400;
cursor: pointer;
}
.form-group {
margin-bottom: 15px;
text-align: left;
}
#kc-content {
width: 100%;
}
#kc-content-wrapper {
margin-top: 20px;
}
#kc-form-options .checkbox {
margin-top: 0;
color: #72767b;
}
.bg-image {
font-family: "Noto Sans Thai", sans-serif;
font-family: "Noto Sans Thai", sans-serif;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-size: cover;
height: 100vh;
background: rgb(30, 50, 49);
background: linear-gradient(
0deg,
rgba(30, 50, 49, 1) 0%,
rgba(20, 120, 99, 1) 100%
);
}
#myimage {
width: 120px;
height: 120px;
object-position: center;
background-repeat: no-repeat;
background-size: 100% !important;
background: url(@/assets/logo.png) no-repeat center center;
margin-bottom: 20px;
}
.text-logo {
text-align: left;
color: white;
font-weight: 600;
font-size: 1.4rem;
}
.subtext-logo {
color: #fff;
font-weight: 200;
font-size: 1rem;
}
.card-pf {
padding: 20px;
max-width: 400px;
min-width: 30%;
position: relative;
z-index: 10 !important;
border-radius: 10px;
background: #fff;
}
.login-pf-page .login-pf-header {
display: -ms-flexbox;
display: flex;
-ms-flex-direction: column;
flex-direction: column;
}
.login-pf-page .login-pf-header h1 {
flex-wrap: wrap;
display: flex;
justify-content: start;
}
h1#kc-page-title {
font-weight: 800;
font-size: 1.3rem;
line-height: 1.1;
}
.login-pf-page .login-pf-header h1 {
font-size: 16px;
}
.img_absolute_line {
position: absolute;
height: auto;
width: 400px;
left: 0;
bottom: 0;
}
</style>

View file

@ -28,24 +28,6 @@ export default defineConfig({
name: 'BMA-Checkin', name: 'BMA-Checkin',
short_name: 'EHR Checkin', short_name: 'EHR Checkin',
theme_color: '#ffffff', theme_color: '#ffffff',
icons: [
{
src: 'icons/android-chrome-192x192.png',
sizes: '192x192',
type: 'image/png',
},
{
src: 'icons/android-chrome-512x512.png',
sizes: '512x512',
type: 'image/png',
},
{
src: 'icons/android-chrome-512x512.png',
sizes: '512x512',
type: 'image/png',
purpose: ['any', 'maskable'],
},
],
}, },
}), }),
], ],