Block login access for relieved employees
This commit is contained in:
@@ -6,6 +6,7 @@ from sqlalchemy import select
|
|||||||
|
|
||||||
from app.core.db.deps import get_common_db
|
from app.core.db.deps import get_common_db
|
||||||
from app.modules.core.iam.models import User
|
from app.modules.core.iam.models import User
|
||||||
|
from app.modules.employees.models import Employee
|
||||||
from app.modules.core.tenancy.models import Branch
|
from app.modules.core.tenancy.models import Branch
|
||||||
from app.modules.core.tenancy.settings_models import BranchSettings
|
from app.modules.core.tenancy.settings_models import BranchSettings
|
||||||
|
|
||||||
@@ -22,6 +23,20 @@ def get_current_user(request: Request, db: Session = Depends(get_common_db)) ->
|
|||||||
|
|
||||||
user = db.execute(select(User).where(User.id == int(user_id))).scalar_one_or_none()
|
user = db.execute(select(User).where(User.id == int(user_id))).scalar_one_or_none()
|
||||||
if not user or not user.is_active or not getattr(user, "allow_login", True) or getattr(user, "is_locked", False) or getattr(user, "deleted_at", None) is not None:
|
if not user or not user.is_active or not getattr(user, "allow_login", True) or getattr(user, "is_locked", False) or getattr(user, "deleted_at", None) is not None:
|
||||||
|
request.session.pop(SESSION_USER_ID_KEY, None)
|
||||||
|
request.session.pop(SESSION_LOGIN_AT_KEY, None)
|
||||||
|
return None
|
||||||
|
|
||||||
|
relieved_employee = db.execute(
|
||||||
|
select(Employee.id).where(
|
||||||
|
Employee.user_id == int(user.id),
|
||||||
|
Employee.tenant_id == int(user.tenant_id),
|
||||||
|
Employee.status == "relieved",
|
||||||
|
).limit(1)
|
||||||
|
).scalar_one_or_none()
|
||||||
|
if relieved_employee is not None:
|
||||||
|
request.session.pop(SESSION_USER_ID_KEY, None)
|
||||||
|
request.session.pop(SESSION_LOGIN_AT_KEY, None)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Enforce session duration from BranchSettings
|
# Enforce session duration from BranchSettings
|
||||||
|
|||||||
@@ -298,6 +298,24 @@ def create_login_user_for_employee(
|
|||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def _disable_relieved_employee_login(db: Session, emp: Employee) -> None:
|
||||||
|
"""Revoke login for a relieved employee without deleting the user identity.
|
||||||
|
|
||||||
|
Historical engagements, attendance, payroll, documents and audit references
|
||||||
|
continue to point to the same User row. Authentication is disabled through
|
||||||
|
allow_login, while the session/auth layer independently blocks any employee
|
||||||
|
whose HR status is relieved.
|
||||||
|
"""
|
||||||
|
if (getattr(emp, "status", "") or "").strip().lower() != "relieved":
|
||||||
|
return
|
||||||
|
user_id = getattr(emp, "user_id", None)
|
||||||
|
if not user_id:
|
||||||
|
return
|
||||||
|
linked_user = db.get(User, int(user_id))
|
||||||
|
if linked_user is not None:
|
||||||
|
linked_user.allow_login = False
|
||||||
|
|
||||||
|
|
||||||
def create_employee(db: Session, actor: User, scope: EmployeeScope, data: dict[str, Any]) -> Employee:
|
def create_employee(db: Session, actor: User, scope: EmployeeScope, data: dict[str, Any]) -> Employee:
|
||||||
cleaned = _clean_payload(data)
|
cleaned = _clean_payload(data)
|
||||||
partner_staff_mode = bool(scope.is_partner and not scope.is_system_admin and not scope.is_firm_admin)
|
partner_staff_mode = bool(scope.is_partner and not scope.is_system_admin and not scope.is_firm_admin)
|
||||||
@@ -388,6 +406,8 @@ def create_employee(db: Session, actor: User, scope: EmployeeScope, data: dict[s
|
|||||||
updated_by_user_id=actor.id,
|
updated_by_user_id=actor.id,
|
||||||
)
|
)
|
||||||
db.add(emp)
|
db.add(emp)
|
||||||
|
db.flush()
|
||||||
|
_disable_relieved_employee_login(db, emp)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(emp)
|
db.refresh(emp)
|
||||||
return emp
|
return emp
|
||||||
@@ -426,6 +446,7 @@ def update_employee(db: Session, actor: User, emp: Employee, data: dict[str, Any
|
|||||||
setattr(emp, field, cleaned.get(field))
|
setattr(emp, field, cleaned.get(field))
|
||||||
emp.updated_by_user_id = actor.id
|
emp.updated_by_user_id = actor.id
|
||||||
emp.updated_at_utc = datetime.now(timezone.utc)
|
emp.updated_at_utc = datetime.now(timezone.utc)
|
||||||
|
_disable_relieved_employee_login(db, emp)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(emp)
|
db.refresh(emp)
|
||||||
return emp
|
return emp
|
||||||
@@ -441,6 +462,7 @@ def change_employee_status(db: Session, actor: User, emp: Employee, status: str,
|
|||||||
emp.date_of_leaving = date_of_leaving
|
emp.date_of_leaving = date_of_leaving
|
||||||
emp.updated_by_user_id = actor.id
|
emp.updated_by_user_id = actor.id
|
||||||
emp.updated_at_utc = datetime.now(timezone.utc)
|
emp.updated_at_utc = datetime.now(timezone.utc)
|
||||||
|
_disable_relieved_employee_login(db, emp)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(emp)
|
db.refresh(emp)
|
||||||
return emp
|
return emp
|
||||||
@@ -2269,6 +2291,7 @@ def complete_offboarding_request(db: Session, user: User, req: EmployeeOffboardi
|
|||||||
employee.is_active = False
|
employee.is_active = False
|
||||||
employee.date_of_leaving = req.approved_relieving_date or req.requested_relieving_date or date.today()
|
employee.date_of_leaving = req.approved_relieving_date or req.requested_relieving_date or date.today()
|
||||||
employee.updated_by_user_id = user.id
|
employee.updated_by_user_id = user.id
|
||||||
|
_disable_relieved_employee_login(db, employee)
|
||||||
req.status = "completed"
|
req.status = "completed"
|
||||||
req.completed_by_user_id = user.id
|
req.completed_by_user_id = user.id
|
||||||
req.completed_at_utc = datetime.now(timezone.utc)
|
req.completed_at_utc = datetime.now(timezone.utc)
|
||||||
|
|||||||
+15
-3
@@ -25,6 +25,7 @@ from app.modules.core.rbac.models import Permission, Role, RolePermission, UserR
|
|||||||
from app.modules.core.tenancy.models import Branch, FinancialYear, Tenant
|
from app.modules.core.tenancy.models import Branch, FinancialYear, Tenant
|
||||||
from app.modules.core.tenancy.settings_models import BranchSettings
|
from app.modules.core.tenancy.settings_models import BranchSettings
|
||||||
from app.modules.email_integration.services import send_auth_otp_email, send_password_changed_email
|
from app.modules.email_integration.services import send_auth_otp_email, send_password_changed_email
|
||||||
|
from app.modules.employees.models import Employee
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -237,10 +238,21 @@ def _post_login_redirect(must_change_password: bool, permissions: set[str], role
|
|||||||
|
|
||||||
return "/employee/dashboard"
|
return "/employee/dashboard"
|
||||||
|
|
||||||
def _is_user_login_allowed(user: User) -> tuple[bool, str | None]:
|
def _is_user_login_allowed(user: User, db=None) -> tuple[bool, str | None]:
|
||||||
if not user:
|
if not user:
|
||||||
return False, "Invalid credentials"
|
return False, "Invalid credentials"
|
||||||
|
|
||||||
|
if db is not None:
|
||||||
|
relieved_employee = db.execute(
|
||||||
|
select(Employee.id).where(
|
||||||
|
Employee.user_id == int(user.id),
|
||||||
|
Employee.tenant_id == int(user.tenant_id),
|
||||||
|
Employee.status == "relieved",
|
||||||
|
).limit(1)
|
||||||
|
).scalar_one_or_none()
|
||||||
|
if relieved_employee is not None:
|
||||||
|
return False, "Login is disabled because this employee has been relieved."
|
||||||
|
|
||||||
if not getattr(user, "is_active", True):
|
if not getattr(user, "is_active", True):
|
||||||
return False, "User account is inactive."
|
return False, "User account is inactive."
|
||||||
|
|
||||||
@@ -517,7 +529,7 @@ def _submit_login(
|
|||||||
else bool(user and not is_client_only)
|
else bool(user and not is_client_only)
|
||||||
)
|
)
|
||||||
|
|
||||||
can_login, blocked_reason = _is_user_login_allowed(user)
|
can_login, blocked_reason = _is_user_login_allowed(user, db)
|
||||||
password_ok = bool(user and verify_password(password, user.password_hash))
|
password_ok = bool(user and verify_password(password, user.password_hash))
|
||||||
|
|
||||||
if not user or not portal_allowed or not can_login or not password_ok:
|
if not user or not portal_allowed or not can_login or not password_ok:
|
||||||
@@ -861,7 +873,7 @@ def forgot_password_submit(
|
|||||||
status_code=200,
|
status_code=200,
|
||||||
)
|
)
|
||||||
|
|
||||||
can_login, _ = _is_user_login_allowed(user)
|
can_login, _ = _is_user_login_allowed(user, db)
|
||||||
if not can_login:
|
if not can_login:
|
||||||
return _render_forgot_password(
|
return _render_forgot_password(
|
||||||
request,
|
request,
|
||||||
|
|||||||
Reference in New Issue
Block a user