feat(apk): afficher/masquer les secrets, scanner le QR 2FA, et toast() reparé
- Mot de passe et secret 2FA naissent masques dans la creation et l'edition, avec un oeil pour les reveler. - Un bouton QR dans le champ 2FA ouvre la camera du scan des reglages ; le lien otpauth:// lu part a la page, qui n'en garde que le secret. Le journal ne note que le fait. - toast() designait l'element <div id="toast"> (chaque id est une variable globale) : sept appels de la page levaient "toast is not a function" et leur message ne s'affichait jamais. Une vraie fonction globale les repare tous. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1043,6 +1043,8 @@ var ICONS = {
|
||||
+ '<circle cx="9" cy="9" r="1.2"/><circle cx="15" cy="15" r="1.2"/>'
|
||||
+ '<circle cx="15" cy="9" r="1.2"/><circle cx="9" cy="15" r="1.2"/>',
|
||||
close: '<path d="M6 6l12 12M18 6L6 18"/>',
|
||||
qr: '<rect x="4" y="4" width="6" height="6" rx="1"/><rect x="14" y="4" width="6" height="6" rx="1"/>'
|
||||
+ '<rect x="4" y="14" width="6" height="6" rx="1"/><path d="M14 14h2v2h-2zM20 14v.01M14 20h.01M17 20h3v-3"/>',
|
||||
// Un arc, pas un cercle : c'est l'ouverture qui rend la rotation visible.
|
||||
loader: '<path d="M12 3a9 9 0 019 9"/>',
|
||||
eyeOff: '<path d="M4 4l16 16"/>'
|
||||
@@ -1141,6 +1143,18 @@ document.getElementById('fields').addEventListener('click', function (ev) {
|
||||
send('touch');
|
||||
return;
|
||||
}
|
||||
var eye = t.getAttribute('data-eye');
|
||||
if (eye) {
|
||||
var ef = document.getElementById(eye);
|
||||
if (!ef) return;
|
||||
var shown = ef.type === 'text';
|
||||
ef.type = shown ? 'password' : 'text';
|
||||
t.innerHTML = ico(shown ? 'eye' : 'eyeOff', shown ? 'Show' : 'Hide');
|
||||
send('touch');
|
||||
return;
|
||||
}
|
||||
var sc = t.getAttribute('data-scan');
|
||||
if (sc) { totpTarget = sc; send('scanqr?for=totp'); return; }
|
||||
var ia = t.getAttribute('data-iconact');
|
||||
if (ia !== null) {
|
||||
// L'ICONE SE COMMET COMME LE RESTE, donc Delphi rouvre l'entree et la vue
|
||||
@@ -1379,6 +1393,29 @@ function genPassword(n) {
|
||||
}
|
||||
return out;
|
||||
}
|
||||
/// AFFICHER / MASQUER. Le champ nait masque : on ouvre un formulaire dans un
|
||||
/// lieu public autant qu'on le remplit, et le mot de passe n'a pas a
|
||||
/// s'afficher tant qu'on ne le demande pas. Un appui le revele pour relire ce
|
||||
/// qu'on tape.
|
||||
function eyeButton(target) {
|
||||
return '<button class="btn ico-btn" data-eye="' + target + '">'
|
||||
+ ico('eye', 'Show') + '</button>';
|
||||
}
|
||||
|
||||
/// SCANNER LE QR QU'AFFICHE LE SITE a l'activation de la 2FA. La camera est
|
||||
/// celle du scan des reglages ; seul ce qu'elle cherche change.
|
||||
function scanButton(target) {
|
||||
return '<button class="btn ico-btn" data-scan="' + target + '">'
|
||||
+ ico('qr', 'Scan the 2FA QR code') + '</button>';
|
||||
}
|
||||
|
||||
/// UN VRAI toast() GLOBAL. Sans lui, le nom designait l'element <div
|
||||
/// id="toast"> - le navigateur expose chaque id comme variable globale - et
|
||||
/// chaque appel toast('...') de cette page levait "toast is not a function" :
|
||||
/// le message ne s'affichait pas, et le reste du gestionnaire ne s'executait
|
||||
/// pas davantage. Une declaration de fonction l'emporte sur l'id.
|
||||
function toast(text) { PM.toast(text); }
|
||||
|
||||
function genButton(target) {
|
||||
return '<button class="btn ico-btn" data-gen="' + target + '">'
|
||||
+ ico('dice', 'Generate') + '</button>';
|
||||
@@ -1389,9 +1426,9 @@ function newBodyControl(kind) {
|
||||
return '<textarea class="field-in" id="nPwd" rows="6" enterkeyhint="enter"'
|
||||
+ ' autocorrect="off" spellcheck="false" style="' + FIELD_STYLE
|
||||
+ ';height:auto;padding:10px 14px;resize:vertical"></textarea>';
|
||||
return '<input class="field-in" id="nPwd" type="text" autocomplete="off"'
|
||||
return '<input class="field-in" id="nPwd" type="password" autocomplete="off"'
|
||||
+ ' autocorrect="off" autocapitalize="none" spellcheck="false"'
|
||||
+ ' style="' + FIELD_STYLE + '">' + genButton('nPwd');
|
||||
+ ' style="' + FIELD_STYLE + '">' + eyeButton('nPwd') + genButton('nPwd');
|
||||
}
|
||||
|
||||
function openNewEntry() {
|
||||
@@ -1423,7 +1460,11 @@ function openNewEntry() {
|
||||
// deja : on pouvait poser un second facteur sur une entree existante, pas
|
||||
// sur une entree qu'on venait de creer - il fallait enregistrer puis
|
||||
// rouvrir. Meme intitule que l'editeur pour qu'on reconnaisse le champ.
|
||||
+ newField('nTotp', '2FA secret', 'nTotpRow')
|
||||
+ '<div class="field" id="nTotpRow"><div class="cap">2FA secret</div>'
|
||||
+ '<div class="line"><input class="field-in" id="nTotp" type="password"'
|
||||
+ ' autocomplete="off" autocorrect="off" autocapitalize="characters"'
|
||||
+ ' spellcheck="false" style="' + FIELD_STYLE + '">'
|
||||
+ eyeButton('nTotp') + scanButton('nTotp') + '</div></div>'
|
||||
+ '</div>'
|
||||
+ '<div class="line" style="display:flex;margin-top:12px">'
|
||||
// flex:1 et non .wide : .wide ne regle que le rembourrage, et la largeur
|
||||
@@ -1530,6 +1571,8 @@ function openMenu() {
|
||||
/// Ce que le formulaire porte a cet instant, par nom de champ. Sert a le
|
||||
/// rouvrir tel quel apres une action d'icone, qui passe par un enregistrement.
|
||||
var editAgain = null;
|
||||
/// Le champ 2FA qui attend le resultat du scan - creation ou edition.
|
||||
var totpTarget = '';
|
||||
/// Le formulaire occupe l'ecran du detail : 'view' reste donc 'detail', et
|
||||
/// c'est ce drapeau qui dit qu'on y modifie - la fleche et la touche retour
|
||||
/// s'en servent pour refermer le formulaire avant de quitter l'entree.
|
||||
@@ -1564,11 +1607,15 @@ function openEditEntry() {
|
||||
? '<textarea class="field-in" data-ef="' + esc(r.e) + '" rows="5"'
|
||||
+ ' enterkeyhint="enter" autocorrect="off" spellcheck="false"'
|
||||
+ ' style="' + FIELD_STYLE + ';height:auto;resize:vertical"></textarea>'
|
||||
: '<input class="field-in"' + (r.e === 'password' ? ' id="editpwd"' : '')
|
||||
+ ' data-ef="' + esc(r.e) + '" type="text"'
|
||||
: '<input class="field-in"'
|
||||
+ (r.e === 'password' ? ' id="editpwd"'
|
||||
: r.e === 'totp_secret' ? ' id="edittotp"' : '')
|
||||
+ ' data-ef="' + esc(r.e) + '" type="'
|
||||
+ (r.e === 'password' || r.e === 'totp_secret' ? 'password' : 'text') + '"'
|
||||
+ ' autocomplete="off" autocorrect="off" autocapitalize="none"'
|
||||
+ ' spellcheck="false" style="' + FIELD_STYLE + '">')
|
||||
+ (r.e === 'password' ? genButton('editpwd') : '')
|
||||
+ (r.e === 'password' ? eyeButton('editpwd') + genButton('editpwd') : '')
|
||||
+ (r.e === 'totp_secret' ? eyeButton('edittotp') + scanButton('edittotp') : '')
|
||||
+ '</div></div>';
|
||||
}
|
||||
// L'ICONE SE CHANGE OU SE CHANGENT LES CHAMPS. Elle vivait dans le menu de
|
||||
@@ -2250,6 +2297,17 @@ var PM = {
|
||||
if (view === 'detail') goList();
|
||||
},
|
||||
cancelEdit: function () { renderDetail(lastDetail); },
|
||||
|
||||
/// Le QR d'un site porte otpauth://totp/Label?secret=XXXX&issuer=... - on
|
||||
/// n'en garde que le secret, comme le champ l'attend. Un QR sans secret
|
||||
/// (un autre lien) ne touche a rien.
|
||||
totpFromQr: function (uri) {
|
||||
var m = /[?&]secret=([^&#]+)/i.exec(uri || '');
|
||||
var el = document.getElementById(totpTarget);
|
||||
if (!m || !el) { PM.toast('Not a 2FA QR code'); return; }
|
||||
el.value = decodeURIComponent(m[1]).replace(/\s+/g, '').toUpperCase();
|
||||
PM.toast('2FA secret read from the QR code');
|
||||
},
|
||||
/// La touche retour annule la selection avant de verrouiller le coffre.
|
||||
/// Sans cela, un geste d'annulation fermait le coffre - le contraire de ce
|
||||
/// qu'il veut dire.
|
||||
|
||||
+21
-3
@@ -386,6 +386,9 @@ type
|
||||
// Scanner du QR affiche par le poste (ZXing.Delphi, Apache 2.0).
|
||||
var
|
||||
FScanning, FScanBusy: Boolean;
|
||||
/// Le scan en cours cherche un QR de 2FA (otpauth://) et non le lien des
|
||||
/// reglages. Pose par 'scanqr?for=totp', lu au decodage.
|
||||
FScanTotp: Boolean;
|
||||
/// L'action de la galerie survit a l'appel : le selecteur d'Android rend
|
||||
/// la main tout de suite et rappelle plus tard.
|
||||
FPickIcon: TTakePhotoFromLibraryAction;
|
||||
@@ -2062,7 +2065,10 @@ begin
|
||||
// SCANNER LE QR DU POSTE depuis l'app : le scanner du systeme envoyait
|
||||
// le lien a une recherche web ou demandait avec quoi l'ouvrir.
|
||||
else if LAction = 'scanqr' then
|
||||
StartScan
|
||||
begin
|
||||
FScanTotp := Param('for') = 'totp';
|
||||
StartScan;
|
||||
end
|
||||
|
||||
else if LAction = 'pinclear' then
|
||||
ClearUnlockCode(TNetEncoding.URL.Decode(Param('c')))
|
||||
@@ -3788,7 +3794,10 @@ begin
|
||||
except
|
||||
// Pas d'autofocus continu : l'image restera nette a bonne distance.
|
||||
end;
|
||||
FScanLbl.Text := 'Point the camera at the QR shown on the PC';
|
||||
if FScanTotp then
|
||||
FScanLbl.Text := 'Point the camera at the 2FA QR code'
|
||||
else
|
||||
FScanLbl.Text := 'Point the camera at the QR shown on the PC';
|
||||
FCam.Active := True;
|
||||
FScanWatch.Enabled := True;
|
||||
Log('QR scan started');
|
||||
@@ -3875,7 +3884,16 @@ begin
|
||||
// CHERCHE, NE COMPARE PAS LE DEBUT : le texte decode est arrive
|
||||
// avec un caractere invisible en tete (104 lus pour 103 emis),
|
||||
// et le lien etait ignore alors qu'il etait lu.
|
||||
if Pos('pmserver://sync?', LText) > 0 then
|
||||
// 2FA : le lien otpauth:// porte le SECRET. Il part a la page,
|
||||
// qui le range dans le champ ; le journal n'en dit que le fait.
|
||||
if FScanTotp and (Pos('otpauth://', LowerCase(LText)) > 0) then
|
||||
begin
|
||||
StopScan;
|
||||
Log('QR scan: 2FA code read');
|
||||
WebCall('PM.totpFromQr(' + JsArg(Trim(Copy(LText,
|
||||
Pos('otpauth://', LowerCase(LText)), MaxInt))) + ')');
|
||||
end
|
||||
else if (not FScanTotp) and (Pos('pmserver://sync?', LText) > 0) then
|
||||
begin
|
||||
StopScan;
|
||||
ApplySyncLink(Trim(Copy(LText, Pos('pmserver://sync?', LText), MaxInt)));
|
||||
|
||||
Reference in New Issue
Block a user