feat(shopfloor): /fp/tablet/unlock_session mints real Odoo session
PIN verify -> request.session.authenticate(type=fp_tablet_pin) -> new session sid, cookie swap, audit event written. Failed attempts also written to audit log (failed_unlock, failure_reason=wrong_pin or locked_out or no_pin_set or user_inactive). OLD /fp/tablet/unlock stays alive during the 1-week overlap window per spec Section 5. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -211,6 +211,116 @@ class FpTabletController(http.Controller):
|
||||
'attempts_remaining': threshold - new_count,
|
||||
}
|
||||
|
||||
# ======================================================================
|
||||
# /fp/tablet/unlock_session — verify PIN + mint REAL Odoo session as tech
|
||||
# ======================================================================
|
||||
@http.route('/fp/tablet/unlock_session', type='jsonrpc', auth='user')
|
||||
def unlock_session(self, user_id, pin):
|
||||
"""Phase 1 of the tablet PIN session redesign.
|
||||
|
||||
Verifies the PIN, then mints a real Odoo session AS the tech
|
||||
via the fp_tablet_pin custom auth manager. Browser cookie
|
||||
swaps; subsequent requests carry the tech's session uid, so
|
||||
create_uid / write_uid / chatter authorship attribute correctly
|
||||
without any tablet_tech_id plumbing.
|
||||
|
||||
Spec: docs/superpowers/specs/2026-05-24-tablet-pin-session-redesign-design.md
|
||||
"""
|
||||
from odoo.exceptions import AccessDenied
|
||||
from ._tablet_session_audit import write_event, _sha256_session_sid
|
||||
env = request.env
|
||||
Users = env['res.users'].sudo()
|
||||
target = Users.browse(int(user_id))
|
||||
if not target.exists():
|
||||
return {'ok': False, 'error': _('User not found.')}
|
||||
|
||||
# No PIN set yet
|
||||
if not target.x_fc_tablet_pin_hash:
|
||||
write_event(env,
|
||||
event_type='failed_unlock',
|
||||
attempted_user_id=target.id,
|
||||
failure_reason='no_pin_set')
|
||||
return {
|
||||
'ok': False,
|
||||
'error': _('No PIN set. Set one in Preferences first.'),
|
||||
'needs_setup': True,
|
||||
}
|
||||
|
||||
# Inactive
|
||||
if not target.active:
|
||||
write_event(env,
|
||||
event_type='failed_unlock',
|
||||
attempted_user_id=target.id,
|
||||
failure_reason='user_inactive')
|
||||
return {'ok': False, 'error': _('User is inactive.')}
|
||||
|
||||
# Currently locked out?
|
||||
now = fields.Datetime.now()
|
||||
if target.x_fc_tablet_locked_until and target.x_fc_tablet_locked_until > now:
|
||||
write_event(env,
|
||||
event_type='failed_unlock',
|
||||
attempted_user_id=target.id,
|
||||
failure_reason='locked_out')
|
||||
return {
|
||||
'ok': False,
|
||||
'error': _('Account locked. Try again in a few minutes.'),
|
||||
'locked_until': target.x_fc_tablet_locked_until.isoformat(),
|
||||
}
|
||||
|
||||
# Attempt the real Odoo session swap via the custom auth manager.
|
||||
# session.authenticate validates credentials through _check_credentials,
|
||||
# issues a new sid, sets the cookie, returns the user dict.
|
||||
try:
|
||||
request.session.authenticate(
|
||||
request.db,
|
||||
{'type': 'fp_tablet_pin',
|
||||
'login': target.login,
|
||||
'pin': pin},
|
||||
)
|
||||
except AccessDenied:
|
||||
# Wrong PIN — increment failure counter
|
||||
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}
|
||||
failure_reason = 'wrong_pin'
|
||||
if new_count >= threshold:
|
||||
vals['x_fc_tablet_locked_until'] = now + timedelta(minutes=lockout_min)
|
||||
failure_reason = 'locked_out'
|
||||
target.write(vals)
|
||||
write_event(env,
|
||||
event_type='failed_unlock',
|
||||
attempted_user_id=target.id,
|
||||
failure_reason=failure_reason)
|
||||
_logger.warning(
|
||||
'Tablet PIN failure for uid %s (count=%d, locked=%s)',
|
||||
target.id, new_count, bool(vals.get('x_fc_tablet_locked_until')),
|
||||
)
|
||||
return {'ok': False, 'error': _('Incorrect PIN.')}
|
||||
|
||||
# Success path. session.authenticate already swapped the cookie.
|
||||
sid = request.session.sid
|
||||
target.write({
|
||||
'x_fc_tablet_pin_failed_count': 0,
|
||||
'x_fc_tablet_locked_until': False,
|
||||
})
|
||||
write_event(env,
|
||||
event_type='unlock',
|
||||
user_id=target.id,
|
||||
session_id_hash=_sha256_session_sid(sid),
|
||||
session_started_at=now)
|
||||
_logger.info(
|
||||
'Tablet session minted for uid %s (sid %s..)',
|
||||
target.id, sid[:8] if sid else '',
|
||||
)
|
||||
return {
|
||||
'ok': True,
|
||||
'tech_id': target.id,
|
||||
'tech_name': target.name,
|
||||
}
|
||||
|
||||
# ======================================================================
|
||||
# /fp/tablet/tiles — lock-screen tile grid
|
||||
# ======================================================================
|
||||
|
||||
Reference in New Issue
Block a user