cleanup(shopfloor): remove legacy /fp/tablet/unlock + _tablet_audit helper

Session-swap is now the only flow. Legacy /fp/tablet/unlock endpoint
deleted. _tablet_audit.py (env_for_tablet_tech helper) deleted with
its last caller gone. /fp/tablet/ping no longer takes current_tech_id
(session uid IS the tech). /fp/tablet/tiles drops tablet_session_mode
return key (kiosk_uid + current_uid retained for OWL isLocked logic).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-24 14:31:21 -04:00
parent 3120612e35
commit d9f2983ea7
2 changed files with 7 additions and 130 deletions

View File

@@ -1,42 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
"""Helper for audit-credit propagation (Phase 6.3 tablet redesign).
Controllers that accept an optional `tablet_tech_id` kwarg use this
helper to switch their `env` to the tech-of-record before performing
writes. The result: chatter posts + create_uid/write_uid carry the
unlocked tech's identity, not the tablet's persistent session user.
"""
import logging
_logger = logging.getLogger(__name__)
def env_for_tablet_tech(env, tablet_tech_id):
"""Return an env scoped to `tablet_tech_id` if it's a valid user;
otherwise return the original env unchanged.
Validation: the user must exist and be active. We deliberately do
NOT cross-check that they actually unlocked recently — the OWL
component is the source of truth for "who's at the tablet right
now", and the only path that produces a tablet_tech_id is a
successful /fp/tablet/unlock followed by an active session in the
OWL tech_store.
"""
if not tablet_tech_id:
return env
try:
tech_id = int(tablet_tech_id)
except (TypeError, ValueError):
return env
User = env['res.users'].sudo()
tech = User.browse(tech_id)
if not tech.exists() or not tech.active:
_logger.warning(
"tablet_tech_id %s invalid (not found or inactive); "
"falling back to session uid %s",
tablet_tech_id, env.uid,
)
return env
return env(user=tech_id)

View File

@@ -141,78 +141,6 @@ class FpTabletController(http.Controller):
)
return {'ok': True}
# ======================================================================
# /fp/tablet/unlock — verify PIN + manage failure counter / lockout
# ======================================================================
@http.route('/fp/tablet/unlock', type='jsonrpc', auth='user')
def unlock(self, user_id, pin):
env = request.env
Users = env['res.users'].sudo() # need sudo to read hash field
target = Users.browse(int(user_id))
if not target.exists():
return {'ok': False, 'error': _('User not found.')}
# No PIN set yet — caller must set one first
if not target.x_fc_tablet_pin_hash:
return {
'ok': False,
'error': _('No PIN set. Set one in Preferences first.'),
'needs_setup': True,
}
# Currently locked out?
now = fields.Datetime.now()
if target.x_fc_tablet_locked_until and target.x_fc_tablet_locked_until > now:
return {
'ok': False,
'error': _('Account locked. Try again in a few minutes.'),
'locked_until': target.x_fc_tablet_locked_until.isoformat(),
}
if target.verify_tablet_pin(pin):
# Reset failure state on success
target.write({
'x_fc_tablet_pin_failed_count': 0,
'x_fc_tablet_locked_until': False,
})
_logger.info(
"Tablet unlocked by uid %s (session uid %s)",
target.id, env.uid,
)
return {
'ok': True,
'current_tech_id': target.id,
'current_tech_name': target.name,
}
# Wrong PIN — increment and check threshold
new_count = (target.x_fc_tablet_pin_failed_count or 0) + 1
threshold = int(env['ir.config_parameter'].sudo().get_param(
'fp.shopfloor.tablet_pin_fail_threshold', 5,
))
lockout_min = int(env['ir.config_parameter'].sudo().get_param(
'fp.shopfloor.tablet_pin_fail_lockout_minutes', 5,
))
vals = {'x_fc_tablet_pin_failed_count': new_count}
if new_count >= threshold:
vals['x_fc_tablet_locked_until'] = now + timedelta(minutes=lockout_min)
target.write(vals)
_logger.warning(
"Tablet PIN failure for uid %s (count=%d, locked=%s)",
target.id, new_count, bool(vals.get('x_fc_tablet_locked_until')),
)
if vals.get('x_fc_tablet_locked_until'):
return {
'ok': False,
'error': _('Too many failed attempts. Locked for %d minutes.') % lockout_min,
'locked_until': vals['x_fc_tablet_locked_until'].isoformat(),
}
return {
'ok': False,
'error': _('Incorrect PIN.'),
'attempts_remaining': threshold - new_count,
}
# ======================================================================
# /fp/tablet/unlock_session — verify PIN + mint REAL Odoo session as tech
# ======================================================================
@@ -498,12 +426,10 @@ class FpTabletController(http.Controller):
})
# Clocked-in first, then alphabetical within bucket
tiles.sort(key=lambda t: (not t['is_clocked_in'], t['name']))
# Phase D bootstrap: lock screen needs the feature flag + kiosk
# uid so it knows whether to call /unlock (legacy) or
# /unlock_session (new), and whether the current session is
# the kiosk's (= no tech logged in).
ICP = env['ir.config_parameter'].sudo()
session_mode = ICP.get_param('fp.shopfloor.tablet_session_mode', 'legacy')
# Lock screen needs the kiosk uid + current session uid so it can
# tell whether a tech is currently logged in (current_uid !=
# kiosk_uid) or the tablet is back on the kiosk (current_uid ==
# kiosk_uid). Post-Phase-G session_swap is the only flow.
kiosk = env.ref(
'fusion_plating_shopfloor.user_fp_tablet_kiosk',
raise_if_not_found=False,
@@ -513,7 +439,6 @@ class FpTabletController(http.Controller):
'ok': True,
'company': _lock_company_payload(request.env),
'tiles': tiles,
'tablet_session_mode': session_mode,
'kiosk_uid': kiosk_uid,
'current_uid': request.env.uid,
}
@@ -522,15 +447,9 @@ class FpTabletController(http.Controller):
# /fp/tablet/ping — heartbeat used by the OWL component on every action
# ======================================================================
@http.route('/fp/tablet/ping', type='jsonrpc', auth='user')
def ping(self, current_tech_id=None):
def ping(self):
"""Lightweight heartbeat. Used by the OWL component to confirm
the server-side session is alive AND to log the tech-of-record
every few minutes so the server has forensic visibility into
which tech was 'driving' the tablet at any moment.
the server-side session is alive. The session uid IS the tech
post-Phase-G — no extra plumbing needed.
"""
if current_tech_id:
_logger.debug(
"Tablet ping: session uid %s carrying tablet_tech_id=%s",
request.env.uid, current_tech_id,
)
return {'ok': True, 'server_time': fields.Datetime.now().isoformat()}