Fix admin PIN after profile switches

Reset profile PIN dialog controls each time it opens so stale profile-specific event listeners cannot submit an admin PIN against a previously selected profile.

Keep failed PIN attempts retryable and restrict launch-lock verification to the admin profile PIN only, so non-admin profile PINs cannot mark the admin lock as verified.
This commit is contained in:
Broque Thomas 2026-05-22 17:18:06 -07:00
parent 5d1f3c1b48
commit 4ebbffb898
2 changed files with 26 additions and 10 deletions

View file

@ -24315,9 +24315,10 @@ def select_profile():
return jsonify({'success': False, 'error': 'Invalid PIN'}), 401
session['profile_id'] = profile_id
# If PIN was just validated, also mark launch PIN as verified
# so the subsequent page reload doesn't ask again
if pin:
# If the admin PIN was just validated, also mark launch PIN as
# verified so the subsequent page reload doesn't ask again. A
# non-admin profile PIN must not unlock the admin launch lock.
if pin and profile_id == 1:
session['launch_pin_verified'] = True
return jsonify({'success': True, 'profile': profile})
except Exception as e:
@ -24402,12 +24403,16 @@ def reset_pin_via_credential():
# Credential verified — clear PIN for the requested profile (default: admin)
database = get_database()
target_profile = data.get('profile_id', 1)
try:
target_profile = int(data.get('profile_id', 1))
except (TypeError, ValueError):
target_profile = 1
database.update_profile(target_profile, pin_hash=None)
# If clearing admin PIN, also disable launch lock
if target_profile == 1:
config_manager.set('security.require_pin_on_launch', False)
session['launch_pin_verified'] = True
if target_profile == 1:
session['launch_pin_verified'] = True
return jsonify({'success': True, 'message': 'PIN cleared. You can set a new PIN in Settings.'})
except Exception as e:

View file

@ -707,8 +707,19 @@ function showPinDialog(profile) {
const dialog = document.getElementById('profile-pin-dialog');
const avatar = document.getElementById('profile-pin-avatar');
const nameEl = document.getElementById('profile-pin-name');
const input = document.getElementById('profile-pin-input');
const errorEl = document.getElementById('profile-pin-error');
const oldInput = document.getElementById('profile-pin-input');
const oldSubmit = document.getElementById('profile-pin-submit');
const oldCancel = document.getElementById('profile-pin-cancel');
// Replace controls on every open so stale listeners from a previous
// profile cannot submit the new PIN against the old profile id.
const input = oldInput.cloneNode(true);
const submit = oldSubmit.cloneNode(true);
const cancel = oldCancel.cloneNode(true);
oldInput.parentNode.replaceChild(input, oldInput);
oldSubmit.parentNode.replaceChild(submit, oldSubmit);
oldCancel.parentNode.replaceChild(cancel, oldCancel);
renderProfileAvatar(avatar, profile);
nameEl.textContent = profile.name;
@ -718,13 +729,12 @@ function showPinDialog(profile) {
dialog.style.display = 'flex';
setTimeout(() => input.focus(), 100);
const submit = document.getElementById('profile-pin-submit');
const cancel = document.getElementById('profile-pin-cancel');
const wasSwitching = !!currentProfile;
const handleSubmit = async () => {
const pin = input.value;
if (!pin) return;
submit.disabled = true;
submit.textContent = 'Verifying...';
try {
const res = await fetch('/api/profiles/select', {
method: 'POST',
@ -753,7 +763,8 @@ function showPinDialog(profile) {
errorEl.textContent = 'Connection error';
errorEl.style.display = '';
}
cleanup();
submit.disabled = false;
submit.textContent = 'Submit';
};
const handleCancel = () => {