Restrict staff attendance punching to mobile devices
This commit is contained in:
+81
-15
@@ -1,4 +1,4 @@
|
||||
from __future__ import annotations
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from io import BytesIO
|
||||
@@ -195,6 +195,80 @@ def _client_ip(request: Request) -> str | None:
|
||||
return request.client.host if request.client else None
|
||||
|
||||
|
||||
_ATTENDANCE_MOBILE_ONLY_MESSAGE = (
|
||||
"Attendance punch-in and punch-out are allowed only from a mobile browser with location/GPS enabled. "
|
||||
"Please open the ERP on your mobile phone to mark attendance. Desktop access is view-only for attendance history."
|
||||
)
|
||||
|
||||
|
||||
def _is_mobile_attendance_device(request: Request) -> bool:
|
||||
"""Server-side guard for staff self-attendance punch actions.
|
||||
|
||||
This intentionally does not replace GPS/geofence validation. It only blocks
|
||||
desktop/laptop browsers from submitting punch-in or punch-out requests.
|
||||
The existing attendance service still performs employee status, geo/IP,
|
||||
timing, duplicate punch and approval checks.
|
||||
"""
|
||||
user_agent = (request.headers.get("user-agent") or "").strip().lower()
|
||||
if not user_agent:
|
||||
return False
|
||||
|
||||
mobile_indicators = (
|
||||
"mobi",
|
||||
"android",
|
||||
"iphone",
|
||||
"ipod",
|
||||
"ipad",
|
||||
"windows phone",
|
||||
"blackberry",
|
||||
"bb10",
|
||||
"opera mini",
|
||||
"opera mobi",
|
||||
"mobile safari",
|
||||
)
|
||||
desktop_indicators = (
|
||||
"windows nt",
|
||||
"macintosh",
|
||||
"x11",
|
||||
"cros",
|
||||
"linux x86_64",
|
||||
)
|
||||
|
||||
has_mobile_signal = any(token in user_agent for token in mobile_indicators)
|
||||
has_desktop_signal = any(token in user_agent for token in desktop_indicators)
|
||||
|
||||
if has_mobile_signal:
|
||||
return True
|
||||
if has_desktop_signal:
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
def _attendance_mobile_context(request: Request) -> dict:
|
||||
is_mobile = _is_mobile_attendance_device(request)
|
||||
return {
|
||||
"is_mobile_attendance_device": is_mobile,
|
||||
"attendance_punch_blocked_reason": None if is_mobile else _ATTENDANCE_MOBILE_ONLY_MESSAGE,
|
||||
}
|
||||
|
||||
|
||||
def _render_self_attendance_page(request: Request, db, current_user, *, errors: list[str] | None = None):
|
||||
employee = get_employee_for_user(db, current_user)
|
||||
rows = list_own_attendance(db, current_user)
|
||||
return _render(
|
||||
request,
|
||||
"modules/employees/templates/employees/self_attendance.html",
|
||||
db,
|
||||
current_user,
|
||||
title="My Attendance",
|
||||
employee=employee,
|
||||
rows=rows,
|
||||
today_attendance=get_today_attendance_for_user(db, current_user),
|
||||
attendance_rows=rows[:7],
|
||||
errors=errors or [],
|
||||
**_attendance_mobile_context(request),
|
||||
)
|
||||
|
||||
|
||||
|
||||
def _active_financial_year(request: Request) -> str | None:
|
||||
@@ -2527,6 +2601,7 @@ def _employee_portal_context(request: Request, db, current_user, *, active_tab:
|
||||
"payslips": list_own_payslips(db, current_user)[:5],
|
||||
"own_documents": list_own_employee_documents(db, current_user)[:8],
|
||||
"work_payload": work_payload,
|
||||
**_attendance_mobile_context(request),
|
||||
}
|
||||
|
||||
|
||||
@@ -2586,20 +2661,7 @@ def employee_self_attendance(request: Request):
|
||||
require_permission(db, current_user, "employees.attendance.view_self")
|
||||
except Exception:
|
||||
return _redirect_denied()
|
||||
employee = get_employee_for_user(db, current_user)
|
||||
rows = list_own_attendance(db, current_user)
|
||||
return _render(
|
||||
request,
|
||||
"modules/employees/templates/employees/self_attendance.html",
|
||||
db,
|
||||
current_user,
|
||||
title="My Attendance",
|
||||
employee=employee,
|
||||
rows=rows,
|
||||
today_attendance=get_today_attendance_for_user(db, current_user),
|
||||
attendance_rows=list_own_attendance(db, current_user)[:7],
|
||||
errors=[],
|
||||
)
|
||||
return _render_self_attendance_page(request, db, current_user, errors=[])
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@@ -2626,6 +2688,8 @@ def employee_punch_in_submit(
|
||||
require_permission(db, current_user, "employees.attendance.punch")
|
||||
except Exception:
|
||||
return _redirect_denied()
|
||||
if not _is_mobile_attendance_device(request):
|
||||
return _render_self_attendance_page(request, db, current_user, errors=[_ATTENDANCE_MOBILE_ONLY_MESSAGE])
|
||||
punch_in_attendance(
|
||||
db,
|
||||
current_user,
|
||||
@@ -2662,6 +2726,8 @@ def employee_punch_out_submit(
|
||||
require_permission(db, current_user, "employees.attendance.punch")
|
||||
except Exception:
|
||||
return _redirect_denied()
|
||||
if not _is_mobile_attendance_device(request):
|
||||
return _render_self_attendance_page(request, db, current_user, errors=[_ATTENDANCE_MOBILE_ONLY_MESSAGE])
|
||||
punch_out_attendance(
|
||||
db,
|
||||
current_user,
|
||||
|
||||
Reference in New Issue
Block a user