Restrict staff attendance punching to mobile devices

This commit is contained in:
A R R R Associates
2026-07-07 17:26:27 +05:30
parent 3ed669563a
commit ae06956b3b
3 changed files with 250 additions and 168 deletions
+81 -15
View File
@@ -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,