fix remove code check fake location
All checks were successful
Build & Deploy on Dev / build (push) Successful in 2m12s

This commit is contained in:
Warunee Tamkoo 2026-03-30 10:47:36 +07:00
parent 02f1fd417d
commit 7edfaa4e4f
4 changed files with 157 additions and 579 deletions

View file

@ -5,51 +5,20 @@ import axios from 'axios'
import { useCounterMixin } from '@/stores/mixin' import { useCounterMixin } from '@/stores/mixin'
import { useQuasar } from 'quasar' import { useQuasar } from 'quasar'
import { usePrivacyStore } from '@/stores/privacy' import { usePrivacyStore } from '@/stores/privacy'
import { useLocationValidation } from '@/composables/useLocationValidation'
const mixin = useCounterMixin() const mixin = useCounterMixin()
const { messageError } = mixin const { messageError } = mixin
const privacyStore = usePrivacyStore() const privacyStore = usePrivacyStore()
// import type { LocationObject } from '@/interface/index/Main' const emit = defineEmits(['update:location'])
const mapElement = ref<HTMLElement | null>(null)
const emit = defineEmits(['update:location', 'locationStatus', 'mockDetected'])
const $q = useQuasar() const $q = useQuasar()
const { validateLocation } = useLocationValidation()
const MOCK_CHECK_DELAY_MS = 800
function wait(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
function getCurrentPositionAsync(options?: PositionOptions) {
return new Promise<GeolocationPosition>((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, options)
})
}
async function getPositionForValidation(fallback: GeolocationPosition) {
await wait(MOCK_CHECK_DELAY_MS)
try {
return await getCurrentPositionAsync({
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 0,
})
} catch (error) {
// Keep the original reading if the second sampling fails.
return fallback
}
}
function updateLocation(latitude: number, longitude: number, namePOI: string) { function updateLocation(latitude: number, longitude: number, namePOI: string) {
// event parent component props // event parent component props
emit('update:location', latitude, longitude, namePOI) emit('update:location', latitude, longitude, namePOI)
} }
const poiPlaceName = ref<string>('') // const poiPlaceName = ref<string>('') //
const isMapReady = ref<boolean>(false)
// Replace ArcGIS api key // Replace ArcGIS api key
const apiKey = ref<string>( const apiKey = ref<string>(
@ -58,7 +27,7 @@ const apiKey = ref<string>(
) )
const zoomMap = ref<number>(18) const zoomMap = ref<number>(18)
async function initializeMap(position: GeolocationPosition) { async function initializeMap() {
try { try {
// Load modules of ArcGIS // Load modules of ArcGIS
loadModules([ loadModules([
@ -69,149 +38,159 @@ async function initializeMap(position: GeolocationPosition) {
'esri/Graphic', 'esri/Graphic',
'esri/layers/TileLayer', 'esri/layers/TileLayer',
]).then(async ([esriConfig, Map, MapView, Point, Graphic, TileLayer]) => { ]).then(async ([esriConfig, Map, MapView, Point, Graphic, TileLayer]) => {
// Set apiKey
// esriConfig.apiKey =
// 'AAPK4f700a4324d04e9f8a1a134e0771ac45FXWawdCl-OotFfr52gz9XKxTDJTpDzw_YYcwbmKDDyAJswf14FoPyw0qBkN64DvP'
// Create a FeatureLayer using a custom server URL
// const hillshadeLayer = new TileLayer({
// url: `https://bmagis.bangkok.go.th/arcgis/rest/services/cache/BMA_3D_2D_Cache/MapServer`,
// })
const map = new Map({ const map = new Map({
basemap: 'streets', basemap: 'streets',
// basemap: 'arcgis-topographic',
// layers: [hillshadeLayer],
}) })
const { latitude, longitude } = position.coords navigator.geolocation.getCurrentPosition(async (position) => {
const { latitude, longitude } = position.coords
const mapView = new MapView({ const mapView = new MapView({
container: 'mapViewDisplay', container: 'mapViewDisplay',
map: map, map: map,
center: { center: {
latitude: latitude, latitude: latitude,
longitude: longitude, longitude: longitude,
}, // Set the initial map center current position }, // Set the initial map center current position
zoom: zoomMap.value, zoom: zoomMap.value,
constraints: { constraints: {
snapToZoom: false, // Disables snapping to the zoom level snapToZoom: false, // Disables snapping to the zoom level
minZoom: zoomMap.value, // Set minimum zoom level minZoom: zoomMap.value, // Set minimum zoom level
maxZoom: zoomMap.value, // Set maximum zoom level (same as minZoom for fixed zoom) maxZoom: zoomMap.value, // Set maximum zoom level (same as minZoom for fixed zoom)
}, },
ui: { ui: {
components: [], // Empty array to remove all default UI components components: [], // Empty array to remove all default UI components
}, },
})
isMapReady.value = true
//
const userPoint = new Point({ longitude, latitude })
const userSymbol = {
type: 'picture-marker',
url: 'http://maps.google.com/mapfiles/ms/icons/red.png',
width: '32px',
height: '32px',
}
const userGraphic = new Graphic({
geometry: userPoint,
symbol: userSymbol,
})
mapView.graphics.add(userGraphic)
// Get POI place server .
await axios
.get(
'https://bmagis.bangkok.go.th/portal/sharing/servers/e4732c3a9fe549ab8bc697573b468f68/rest/services/World/GeocodeServer/reverseGeocode/',
{
params: {
f: 'json', // Format JSON response
distance: 2000,
category: 'POI',
location: {
spatialReference: { wkid: 4326 },
x: longitude,
y: latitude,
},
token: apiKey.value,
},
}
)
.then((response) => {
// console.log('poi', response.data.location)
poiPlaceName.value = response.data.address
? response.data.address.PlaceName === ''
? response.data.address.ShortLabel
: response.data.address.PlaceName
: 'ไม่พบข้อมูล'
const poiPoint = new Point({
longitude: response.data.location.x,
latitude: response.data.location.y,
})
const poiSymbol = {
type: 'picture-marker',
url: 'http://maps.google.com/mapfiles/ms/icons/blue.png',
width: '32px',
height: '32px',
}
const poiGraphic = new Graphic({
geometry: poiPoint,
symbol: poiSymbol,
})
mapView.graphics.add(poiGraphic)
// POI
mapView.goTo({
target: [userPoint, poiPoint],
zoom: zoomMap.value,
})
updateLocation(latitude, longitude, poiPlaceName.value)
}) })
.catch(async (error) => {
// console.error('Error fetching points of interest:', error) //
// Get POI place server arcgis token const userPoint = new Point({ longitude, latitude })
await axios const userSymbol = {
.get( type: 'picture-marker',
'https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/reverseGeocode/', url: 'http://maps.google.com/mapfiles/ms/icons/red.png',
{ width: '32px',
params: { height: '32px',
f: 'json', // Format JSON response }
distance: 2000, const userGraphic = new Graphic({
category: 'POI', geometry: userPoint,
location: { symbol: userSymbol,
spatialReference: { wkid: 4326 }, })
x: longitude, mapView.graphics.add(userGraphic)
y: latitude, // Get POI place server .
}, await axios
.get(
'https://bmagis.bangkok.go.th/portal/sharing/servers/e4732c3a9fe549ab8bc697573b468f68/rest/services/World/GeocodeServer/reverseGeocode/',
{
params: {
f: 'json', // Format JSON response
distance: 2000,
category: 'POI',
location: {
spatialReference: { wkid: 4326 },
x: longitude,
y: latitude,
}, },
} token: apiKey.value,
) },
.then((response) => { }
// console.log('poi', response.data.location) )
poiPlaceName.value = response.data.address .then((response) => {
? response.data.address.PlaceName === '' // console.log('poi', response.data.location)
? response.data.address.ShortLabel poiPlaceName.value = response.data.address
: response.data.address.PlaceName ? response.data.address.PlaceName === ''
: 'ไม่พบข้อมูล' ? response.data.address.ShortLabel
const poiPoint = new Point({ : response.data.address.PlaceName
longitude: response.data.location.x, : 'ไม่พบข้อมูล'
latitude: response.data.location.y, const poiPoint = new Point({
}) longitude: response.data.location.x,
const poiSymbol = { latitude: response.data.location.y,
type: 'picture-marker', })
url: 'http://maps.google.com/mapfiles/ms/icons/blue.png', const poiSymbol = {
width: '32px', type: 'picture-marker',
height: '32px', url: 'http://maps.google.com/mapfiles/ms/icons/blue.png',
} width: '32px',
const poiGraphic = new Graphic({ height: '32px',
geometry: poiPoint, }
symbol: poiSymbol, const poiGraphic = new Graphic({
}) geometry: poiPoint,
mapView.graphics.add(poiGraphic) symbol: poiSymbol,
// POI })
mapView.goTo({ mapView.graphics.add(poiGraphic)
target: [userPoint, poiPoint], // POI
zoom: zoomMap.value, mapView.goTo({
}) target: [userPoint, poiPoint],
zoom: zoomMap.value,
})
updateLocation(latitude, longitude, poiPlaceName.value) updateLocation(latitude, longitude, poiPlaceName.value)
}) })
.catch((error) => { .catch(async (error) => {
// Keep map visible even when POI lookup fails. // console.error('Error fetching points of interest:', error)
poiPlaceName.value = 'ไม่พบข้อมูล' // Get POI place server arcgis token
updateLocation(latitude, longitude, poiPlaceName.value) await axios
}) .get(
}) 'https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/reverseGeocode/',
{
params: {
f: 'json', // Format JSON response
distance: 2000,
category: 'POI',
location: {
spatialReference: { wkid: 4326 },
x: longitude,
y: latitude,
},
},
}
)
.then((response) => {
// console.log('poi', response.data.location)
poiPlaceName.value = response.data.address
? response.data.address.PlaceName === ''
? response.data.address.ShortLabel
: response.data.address.PlaceName
: 'ไม่พบข้อมูล'
const poiPoint = new Point({
longitude: response.data.location.x,
latitude: response.data.location.y,
})
const poiSymbol = {
type: 'picture-marker',
url: 'http://maps.google.com/mapfiles/ms/icons/blue.png',
width: '32px',
height: '32px',
}
const poiGraphic = new Graphic({
geometry: poiPoint,
symbol: poiSymbol,
})
mapView.graphics.add(poiGraphic)
// POI
mapView.goTo({
target: [userPoint, poiPoint],
zoom: zoomMap.value,
})
updateLocation(latitude, longitude, poiPlaceName.value)
})
.catch((error) => {
// console.error('Error fetching points of interest:', error)
})
})
})
}) })
} catch (error) { } catch (error) {
console.error('Error loading the map', error) console.error('Error loading the map', error)
@ -242,35 +221,10 @@ const requestLocationPermission = () => {
navigator.geolocation.getCurrentPosition( navigator.geolocation.getCurrentPosition(
async (position) => { async (position) => {
const sampledPosition = await getPositionForValidation(position) // Permission granted
// Validate location first
const validationResult = validateLocation(sampledPosition)
// Do not block map preview on initial mock detection.
// The hard-stop warning is enforced at submit time in HomeView.
if (validationResult.isMockDetected) {
locationGranted.value = true
emit('locationStatus', true)
}
// Check for critical errors (invalid coordinates) that prevent showing location
const hasCriticalErrors = validationResult.errors.some((error) =>
error.includes('พิกัดตำแหน่งไม่ถูกต้อง')
)
if (hasCriticalErrors) {
locationGranted.value = false
emit('locationStatus', false)
messageError($q, '', validationResult.errors[0])
return
}
// Permission granted for map preview state.
locationGranted.value = true locationGranted.value = true
emit('locationStatus', true)
const { latitude, longitude } = sampledPosition.coords const { latitude, longitude } = position.coords
// console.log('Current position:', latitude, longitude) // console.log('Current position:', latitude, longitude)
if (!latitude || !longitude) { if (!latitude || !longitude) {
@ -280,13 +234,12 @@ const requestLocationPermission = () => {
// Center map on user's location if map is initialized // Center map on user's location if map is initialized
if (privacyStore.isAccepted) { if (privacyStore.isAccepted) {
await initializeMap(sampledPosition) await initializeMap()
} }
}, },
(error) => { (error) => {
// Permission denied // Permission denied
locationGranted.value = false locationGranted.value = false
emit('locationStatus', false)
switch (error.code) { switch (error.code) {
case error.PERMISSION_DENIED: case error.PERMISSION_DENIED:
@ -311,19 +264,18 @@ const requestLocationPermission = () => {
break break
} }
}, },
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 0 } { enableHighAccuracy: true }
) )
} }
defineExpose({ defineExpose({
requestLocationPermission, requestLocationPermission,
locationGranted,
}) })
</script> </script>
<template> <template>
<!-- Loading skeleton --> <!-- Loading skeleton -->
<div v-if="!isMapReady" class="col-12"> <div v-if="!poiPlaceName" class="col-12">
<q-skeleton <q-skeleton
:height="$q.screen.gt.xs ? '35vh' : '45px'" :height="$q.screen.gt.xs ? '35vh' : '45px'"
width="100%" width="100%"
@ -333,7 +285,7 @@ defineExpose({
</div> </div>
<q-card <q-card
v-show="isMapReady" v-show="poiPlaceName"
bordered bordered
flat flat
class="col-12 bg-grey-2 shadow-0" class="col-12 bg-grey-2 shadow-0"
@ -355,7 +307,7 @@ defineExpose({
> >
นทใกลเคยง นทใกลเคยง
<span class="q-px-sm">:</span> <span class="q-px-sm">:</span>
{{ poiPlaceName || 'ไม่พบข้อมูล' }} {{ poiPlaceName }}
</div> </div>
</div> </div>
@ -372,7 +324,7 @@ defineExpose({
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
{{ poiPlaceName || 'ไม่พบข้อมูล' }} {{ poiPlaceName }}
</q-item-section> </q-item-section>
</template> </template>

View file

@ -176,7 +176,7 @@ function onClose() {
<template> <template>
<q-dialog v-model="modal" persistent> <q-dialog v-model="modal" persistent>
<q-card style="width: 700px; max-width: 80vw"> <q-card style="width: 700px; max-width: 90vw">
<q-form greedy @submit.prevent @validation-success="onSubmit"> <q-form greedy @submit.prevent @validation-success="onSubmit">
<DialogHeader :tittle="title" :close="onClose" /> <DialogHeader :tittle="title" :close="onClose" />
<q-separator /> <q-separator />

View file

@ -1,255 +0,0 @@
import { ref } from 'vue'
import { useQuasar } from 'quasar'
import { useCounterMixin } from '@/stores/mixin'
export interface LocationValidationResult {
isValid: boolean
isMockDetected: boolean
confidence: 'low' | 'medium' | 'high'
warnings: string[]
errors: string[]
}
export interface PositionSnapshot {
latitude: number
longitude: number
timestamp: number
}
// Configuration constants - exported for documentation and testing purposes
export const VALIDATION_CONFIG = {
MAX_TIMESTAMP_AGE_MS: 60_000, // 60 seconds - maximum acceptable age of location data
MAX_ACCURACY_METERS: 100, // 100 meters - maximum acceptable GPS accuracy
MAX_SPEED_MS: 100, // 100 m/s (~360 km/h) - maximum plausible movement speed
POSITION_HISTORY_SIZE: 5, // number of positions to keep for pattern detection
MOCK_INDICATOR_THRESHOLD: 3, // threshold for mock detection (indicators >= 3 = mock)
SUSPICIOUS_ACCURACY_MAX: 5, // accuracy ≤ 5m AND integer = suspicious (real GPS never rounds to whole number)
// Geographic service area (Thailand) to reduce spoofing risk from remote fake coordinates.
SERVICE_AREA: {
MIN_LAT: 5.0,
MAX_LAT: 21.0,
MIN_LON: 97.0,
MAX_LON: 106.0,
},
} as const
export function useLocationValidation() {
const $q = useQuasar()
const { messageError } = useCounterMixin()
// Thai error messages - exported for i18n consistency
const errorMessages = {
MOCK_DETECTED:
'ตรวจพบว่าตำแหน่ง GPS อาจไม่ถูกต้อง กรุณาปิดแอปจำลองตำแหน่งและลองใหม่',
INVALID_COORDINATES: 'พิกัดตำแหน่งไม่ถูกต้อง กรุณาลองใหม่',
STALE_TIMESTAMP: 'ข้อมูลตำแหน่งเก่าเกินไป กรุณารับสัญญาณ GPS ใหม่',
POOR_ACCURACY: 'ความแม่นยำตำแหน่งต่ำเกินไป กรุณาตรวจสอบการรับสัญญาณ GPS',
IMPOSSIBLE_SPEED:
'ตรวจพบการเคลื่อนที่ด้วยความเร็วผิดปกติ อาจเป็นการจำลองตำแหน่ง',
SUSPICIOUS_ACCURACY: 'ตรวจพบค่าความแม่นยำที่ผิดปกติ อาจเป็นการจำลองตำแหน่ง',
DUPLICATE_POSITION: 'ตรวจพบพิกัดตำแหน่งซ้ำกัน อาจเป็นการจำลองตำแหน่ง',
OUT_OF_SERVICE_AREA:
'ตรวจพบตำแหน่งนอกพื้นที่ใช้งานของระบบ กรุณาปิดแอปจำลองตำแหน่งและลองใหม่',
}
const previousPositions = ref<PositionSnapshot[]>([])
// คำนวณระยะห่างระหว่าง 2 จุด (Haversine formula)
const haversineDistance = (
lat1: number,
lon1: number,
lat2: number,
lon2: number
): number => {
const R = 6371e3 // Earth's radius in meters
const φ1 = (lat1 * Math.PI) / 180
const φ2 = (lat2 * Math.PI) / 180
const Δφ = ((lat2 - lat1) * Math.PI) / 180
const Δλ = ((lon2 - lon1) * Math.PI) / 180
const a =
Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2)
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
return R * c
}
// ตรวจสอบพิกัดถูกต้อง
const validateCoordinates = (lat: number, lon: number): boolean => {
return (
lat >= -90 &&
lat <= 90 &&
lon >= -180 &&
lon <= 180 &&
!isNaN(lat) &&
!isNaN(lon) &&
!(lat === 0 && lon === 0) // Mock มักใช้ 0,0
)
}
// ตรวจสอบความแม่นยำ
const validateAccuracy = (accuracy: number | null): boolean => {
if (accuracy === null) return true
return accuracy <= VALIDATION_CONFIG.MAX_ACCURACY_METERS
}
// ตรวจสอบ Timestamp
const validateTimestamp = (timestamp: number): boolean => {
const now = Date.now()
const age = Math.abs(now - timestamp)
return age <= VALIDATION_CONFIG.MAX_TIMESTAMP_AGE_MS
}
// คำนวณความเร็ว
const calculateSpeed = (
pos1: PositionSnapshot,
pos2: PositionSnapshot
): number => {
const distance = haversineDistance(
pos1.latitude,
pos1.longitude,
pos2.latitude,
pos2.longitude
)
const timeDiff = Math.abs(pos2.timestamp - pos1.timestamp) / 1000 // seconds
return timeDiff > 0 ? distance / timeDiff : 0
}
// ตรวจสอบความเร็วปกติ
const validateSpeed = (
current: PositionSnapshot,
previous: PositionSnapshot
): boolean => {
const speed = calculateSpeed(previous, current)
return speed <= VALIDATION_CONFIG.MAX_SPEED_MS
}
// ตรวจสอบค่าความแม่นยำที่ผิดปกติ (mock apps มักรายงานค่าที่เป็นเลขจำนวนเต็มสวยงาม เช่น 5.0, 3.0)
const hasSuspiciousPerfectAccuracy = (accuracy: number | null): boolean => {
if (accuracy === null) return false
return (
Number.isInteger(accuracy) &&
accuracy <= VALIDATION_CONFIG.SUSPICIOUS_ACCURACY_MAX
)
}
// ตรวจสอบพิกัดซ้ำกันทุกค่า (real GPS มีการสั่นเล็กน้อย mock app มักคืนพิกัดเดิมซ้ำ)
const hasDuplicateCoordinates = (lat: number, lon: number): boolean => {
return previousPositions.value.some(
(p) => p.latitude === lat && p.longitude === lon
)
}
const isWithinServiceArea = (lat: number, lon: number): boolean => {
const area = VALIDATION_CONFIG.SERVICE_AREA
return (
lat >= area.MIN_LAT &&
lat <= area.MAX_LAT &&
lon >= area.MIN_LON &&
lon <= area.MAX_LON
)
}
// Main validation function
const validateLocation = (
position: GeolocationPosition
): LocationValidationResult => {
const warnings: string[] = []
const errors: string[] = []
let mockIndicators = 0
const { latitude, longitude, accuracy } = position.coords
const { timestamp } = position
// 1. Coordinate validation
if (!validateCoordinates(latitude, longitude)) {
errors.push(errorMessages.INVALID_COORDINATES)
mockIndicators += 3
}
// 1.1 Service-area validation (critical for remote spoofed coordinates)
if (!isWithinServiceArea(latitude, longitude)) {
errors.push(errorMessages.OUT_OF_SERVICE_AREA)
mockIndicators += 3
}
// 2. Timestamp validation
if (!validateTimestamp(timestamp)) {
errors.push(errorMessages.STALE_TIMESTAMP)
mockIndicators += 2
}
// 3. Accuracy validation
if (!validateAccuracy(accuracy)) {
warnings.push(errorMessages.POOR_ACCURACY)
mockIndicators += 1
}
// 4. Compare with previous positions
if (previousPositions.value.length > 0) {
const previous =
previousPositions.value[previousPositions.value.length - 1]
if (!validateSpeed({ latitude, longitude, timestamp }, previous)) {
errors.push(errorMessages.IMPOSSIBLE_SPEED)
mockIndicators += 3
}
}
// 5. Suspiciously perfect accuracy — real GPS never rounds to a whole-number ≤ 5m
if (hasSuspiciousPerfectAccuracy(accuracy)) {
warnings.push(errorMessages.SUSPICIOUS_ACCURACY)
mockIndicators += 2
}
// 6. Exact duplicate coordinates — real GPS always drifts slightly between readings
if (
previousPositions.value.length > 0 &&
hasDuplicateCoordinates(latitude, longitude)
) {
warnings.push(errorMessages.DUPLICATE_POSITION)
mockIndicators += 2
}
// Store current position
previousPositions.value.push({ latitude, longitude, timestamp })
if (
previousPositions.value.length > VALIDATION_CONFIG.POSITION_HISTORY_SIZE
) {
previousPositions.value.shift()
}
// Determine result
const isMockDetected =
mockIndicators >= VALIDATION_CONFIG.MOCK_INDICATOR_THRESHOLD
const isValid = errors.length === 0
let confidence: 'low' | 'medium' | 'high' = 'low'
if (mockIndicators >= 5) confidence = 'high'
else if (mockIndicators >= 3) confidence = 'medium'
return {
isValid,
isMockDetected,
confidence,
warnings,
errors,
}
}
const showMockWarning = (result: LocationValidationResult) => {
if (!result.isMockDetected) return
messageError($q, '', errorMessages.MOCK_DETECTED)
}
const resetValidation = () => {
previousPositions.value = []
}
return {
validateLocation,
showMockWarning,
resetValidation,
}
}

View file

@ -8,7 +8,6 @@ import config from '@/app.config'
import http from '@/plugins/http' import http from '@/plugins/http'
import { useCounterMixin } from '@/stores/mixin' import { useCounterMixin } from '@/stores/mixin'
import { usePermissions } from '@/composables/usePermissions' import { usePermissions } from '@/composables/usePermissions'
import { useLocationValidation } from '@/composables/useLocationValidation'
import { usePrivacyStore } from '@/stores/privacy' import { usePrivacyStore } from '@/stores/privacy'
import type { FormRef, OptionReason } from '@/interface/response/checkin' import type { FormRef, OptionReason } from '@/interface/response/checkin'
@ -19,8 +18,6 @@ const mixin = useCounterMixin()
const { date2Thai, showLoader, hideLoader, messageError, dialogConfirm } = mixin const { date2Thai, showLoader, hideLoader, messageError, dialogConfirm } = mixin
const $q = useQuasar() const $q = useQuasar()
const { checkPrivacyAccepted } = usePermissions() const { checkPrivacyAccepted } = usePermissions()
const { validateLocation, showMockWarning, resetValidation } =
useLocationValidation()
const privacyStore = usePrivacyStore() const privacyStore = usePrivacyStore()
const MOCK_CHECK_DELAY_MS = 800 const MOCK_CHECK_DELAY_MS = 800
@ -36,8 +33,6 @@ const endTimeAfternoon = ref<string>('12:00:00') //เวลาเช็คเ
const isLoadingCheckTime = ref<boolean>(false) // const isLoadingCheckTime = ref<boolean>(false) //
const disabledBtn = ref<boolean>(false) const disabledBtn = ref<boolean>(false)
const locationGranted = ref<boolean>(false)
const isMockLocationDetected = ref<boolean>(false)
/** /**
* fetch เชคเวลาตองลงเวลาเขาหรอออกงาน * fetch เชคเวลาตองลงเวลาเขาหรอออกงาน
@ -119,26 +114,7 @@ async function updateLocation(
formLocation.POI = namePOI formLocation.POI = namePOI
} }
/**
* บคาสถานะ location จาก AscGISMap
*/
function onLocationStatus(status: boolean) {
locationGranted.value = status
if (status) {
isMockLocationDetected.value = false
}
}
/**
* บค mock location detection จาก AscGISMap
*/
function onMockDetected(result: any) {
isMockLocationDetected.value = !!result?.isMockDetected
disabledBtn.value = false
}
function resetLocationForRetry() { function resetLocationForRetry() {
locationGranted.value = false
formLocation.lat = 0 formLocation.lat = 0
formLocation.lng = 0 formLocation.lng = 0
formLocation.POI = '' formLocation.POI = ''
@ -174,62 +150,6 @@ async function getDelayedFreshPosition() {
} }
} }
async function revalidateLocationBeforeSubmit() {
if (!navigator.geolocation) {
messageError(
$q,
'',
'ไม่สามารถระบุตำแหน่งปัจจุบันได้ เบราว์เซอร์ของคุณไม่รองรับ Geolocation'
)
return false
}
try {
// If previous attempt was mock, clear history so fresh GPS is not compared
// against spoofed coordinates and incorrectly flagged as impossible speed.
if (isMockLocationDetected.value) {
resetValidation()
}
const position = await getDelayedFreshPosition()
const validationResult = validateLocation(position)
if (validationResult.isMockDetected) {
isMockLocationDetected.value = true
disabledBtn.value = false
resetValidation()
resetLocationForRetry()
showMockWarning(validationResult)
mapRef.value?.requestLocationPermission()
return false
}
if (validationResult.errors.length > 0) {
disabledBtn.value = false
resetValidation()
resetLocationForRetry()
messageError($q, '', validationResult.errors[0])
mapRef.value?.requestLocationPermission()
return false
}
locationGranted.value = true
isMockLocationDetected.value = false
return true
} catch (error) {
disabledBtn.value = false
resetLocationForRetry()
messageError(
$q,
'',
'ไม่สามารถตรวจสอบตำแหน่งล่าสุดก่อนลงเวลาได้ กรุณาลองใหม่อีกครั้ง'
)
mapRef.value?.requestLocationPermission()
return false
}
}
const location = ref<string>('') // const location = ref<string>('') //
const model = ref<string>('') // const model = ref<string>('') //
// //
@ -462,11 +382,6 @@ async function validateForm() {
} }
} }
if (hasError.every((result) => result === true)) { if (hasError.every((result) => result === true)) {
const isLocationValid = await revalidateLocationBeforeSubmit()
if (!isLocationValid) {
return
}
if (statusCheckin.value == false) { if (statusCheckin.value == false) {
getCheck() getCheck()
} else if (statusCheckin.value) { } else if (statusCheckin.value) {
@ -511,11 +426,6 @@ async function confirm() {
return return
} }
const isLocationValid = await revalidateLocationBeforeSubmit()
if (!isLocationValid) {
return
}
showLoader() showLoader()
const isLocation = workplace.value === 'in-place' //*true , false const isLocation = workplace.value === 'in-place' //*true , false
const locationName = workplace.value === 'in-place' ? '' : useLocation.value const locationName = workplace.value === 'in-place' ? '' : useLocation.value
@ -734,16 +644,6 @@ watch(
} }
} }
) )
watch(
() => locationGranted.value,
(newVal) => {
// Removed auto-reset of isMockLocationDetected to prevent
// clearing mock detection state when permission is granted.
// Mock detection state should only be reset after explicit user action
// or after a successful validation without mock indicators.
}
)
</script> </script>
<template> <template>
@ -816,8 +716,6 @@ watch(
v-if="$q.screen.gt.xs" v-if="$q.screen.gt.xs"
ref="mapRef" ref="mapRef"
@update:location="updateLocation" @update:location="updateLocation"
@location-status="onLocationStatus"
@mock-detected="onMockDetected"
/> />
</div> </div>
</div> </div>
@ -984,12 +882,7 @@ watch(
</div> </div>
<div class="col-12" v-if="$q.screen.xs"> <div class="col-12" v-if="$q.screen.xs">
<MapCheck <MapCheck ref="mapRef" @update:location="updateLocation" />
ref="mapRef"
@update:location="updateLocation"
@location-status="onLocationStatus"
@mock-detected="onMockDetected"
/>
</div> </div>
</div> </div>
<!-- กรอกขอม หนามอถ --> <!-- กรอกขอม หนามอถ -->
@ -1138,13 +1031,7 @@ watch(
push push
size="18px" size="18px"
:class="$q.screen.gt.xs ? 'q-px-md' : 'full-width q-pa-sm'" :class="$q.screen.gt.xs ? 'q-px-md' : 'full-width q-pa-sm'"
:disable=" :disable="disabledBtn ? true : camera && img ? false : true"
disabledBtn || !locationGranted || isMockLocationDetected
? true
: camera && img
? false
: true
"
@click="validateForm" @click="validateForm"
:loading="inQueue" :loading="inQueue"
/> />
@ -1259,13 +1146,7 @@ watch(
push push
size="18px" size="18px"
:class="$q.screen.gt.xs ? 'q-px-md' : 'full-width q-pa-sm'" :class="$q.screen.gt.xs ? 'q-px-md' : 'full-width q-pa-sm'"
:disable=" :disable="disabledBtn ? true : camera && img ? false : true"
disabledBtn || !locationGranted || isMockLocationDetected
? true
: camera && img
? false
: true
"
@click="validateForm" @click="validateForm"
:loading="inQueue" :loading="inQueue"
/> />