feat(android): change a password from the phone, section 4 complete
Pull, patch, re-encrypt, publish - in that order, and against the SERVER's snapshot rather than the local cache. That is what replaces the merge the plan asked for: anything untouched is already everyone's newest version, so there is nothing to reconcile. The only conflict left is another device publishing between our read and our write. Gitea refuses it on the sha with a 409, and the whole cycle repeats, decryption included, since the content changed. Three attempts: past that the other device is publishing in a loop and insisting would not help. The editable field is marked in Delphi rather than recognised by its caption in the page. A caption is prose; the day it changes, matching on it would silently stop offering the edit. The editor is inline rather than window.prompt: a JavaScript prompt needs a WebChromeClient that FMX does not provide, and prompt() then returns null without a word - the button would appear to do nothing. The value shows in clear while typing, deliberately: you are entering a password you cannot read back anywhere else, and masking it would mean confirming a keystroke blind. This is the one command that carries a secret over the bridge, agreed beforehand. It stays in the process, the iframe carrying it is destroyed, and the handler logs the action only - never the query. Only the password is editable. Creating or deleting an entry would need tombstones and uuid generation, and changing a password covers the need without either. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -149,6 +149,14 @@ type
|
||||
TDetailRow = record
|
||||
Caption, Value: string;
|
||||
Secret: Boolean;
|
||||
/// Modifiable depuis le telephone. UN SEUL champ l'est pour l'instant - le
|
||||
/// mot de passe - et c'est deliberé : creer ou supprimer une entree
|
||||
/// demanderait des tombstones et la generation d'uuid, alors que changer un
|
||||
/// mot de passe couvre l'essentiel du besoin sans rien de tout cela.
|
||||
/// Marque ICI plutot que reconnu au libelle cote page : un intitule est de
|
||||
/// la prose, et le jour ou il change la page cesserait silencieusement de
|
||||
/// proposer la modification.
|
||||
Editable: Boolean;
|
||||
end;
|
||||
|
||||
function BuildRows(const E: TVaultEntry): TArray<TDetailRow>;
|
||||
@@ -157,13 +165,15 @@ var
|
||||
I: Integer;
|
||||
LMeta: string;
|
||||
|
||||
procedure Add(const ACaption, AValue: string; ASecret: Boolean);
|
||||
procedure Add(const ACaption, AValue: string; ASecret: Boolean;
|
||||
AEditable: Boolean = False);
|
||||
begin
|
||||
if AValue = '' then Exit; // une ligne vide n'apprend rien
|
||||
SetLength(LRows, Length(LRows) + 1);
|
||||
LRows[High(LRows)].Caption := ACaption;
|
||||
LRows[High(LRows)].Value := AValue;
|
||||
LRows[High(LRows)].Secret := ASecret;
|
||||
LRows[High(LRows)].Caption := ACaption;
|
||||
LRows[High(LRows)].Value := AValue;
|
||||
LRows[High(LRows)].Secret := ASecret;
|
||||
LRows[High(LRows)].Editable := AEditable;
|
||||
end;
|
||||
|
||||
begin
|
||||
@@ -171,8 +181,11 @@ begin
|
||||
Add('Username', E.Username, False);
|
||||
// Le corps d'une note est range dans le champ mot de passe : meme donnee,
|
||||
// autre intitule, et pas de raison de la masquer comme un secret.
|
||||
// Seule la ligne du mot de passe est modifiable. Le corps d'une note le
|
||||
// serait en principe, mais il se saisit sur plusieurs lignes - un autre
|
||||
// controle, une autre fois.
|
||||
if E.IsNote then Add('Content', E.Password, False)
|
||||
else Add('Password', E.Password, True);
|
||||
else Add('Password', E.Password, True, True);
|
||||
|
||||
for I := 0 to High(E.Fields) do
|
||||
Add(E.Fields[I].Caption, E.Fields[I].Value, E.Fields[I].Secret);
|
||||
@@ -220,6 +233,7 @@ begin
|
||||
LObj.AddPair('c', LRows[I].Caption);
|
||||
LObj.AddPair('v', LRows[I].Value);
|
||||
LObj.AddPair('s', TJSONBool.Create(LRows[I].Secret));
|
||||
if LRows[I].Editable then LObj.AddPair('e', TJSONBool.Create(True));
|
||||
LArr.AddElement(LObj);
|
||||
end;
|
||||
Result := LRoot.ToJSON;
|
||||
|
||||
@@ -250,7 +250,7 @@
|
||||
<div class="in-wrap">
|
||||
<!-- NEUTRE : c'est l'URL qui decide de la nature du jeton, pas ce
|
||||
champ. Une adresse d'API Gitea en fait un jeton d'acces personnel
|
||||
envoye en « token » ; l'endpoint PHP en fait un « Bearer ». Le
|
||||
envoye en "token" ; l'endpoint PHP en fait un "Bearer". Le
|
||||
nommer d'apres un seul des deux le rend faux pour l'autre. -->
|
||||
<input class="field-in" id="tok" placeholder="access token"
|
||||
type="password" autocomplete="off">
|
||||
@@ -484,6 +484,9 @@ document.getElementById('qx').addEventListener('click', function () {
|
||||
|
||||
// --- detail ----------------------------------------------------------------
|
||||
var detailRows = [];
|
||||
// Le dernier detail recu, pour redessiner la ligne apres une annulation sans
|
||||
// avoir a le redemander a Delphi.
|
||||
var lastDetail = null;
|
||||
|
||||
function renderDetail(d) {
|
||||
detailRows = d.rows || [];
|
||||
@@ -503,6 +506,7 @@ function renderDetail(d) {
|
||||
+ '</div>'
|
||||
+ (r.s ? '<button class="btn" data-show="' + i + '">Show</button>' : '')
|
||||
+ '<button class="btn" data-copy="row" data-k="' + i + '">Copy</button>'
|
||||
+ (r.e ? '<button class="btn" data-edit="' + i + '">Edit</button>' : '')
|
||||
+ '</div></div>';
|
||||
}
|
||||
document.getElementById('fields').innerHTML = html;
|
||||
@@ -524,10 +528,43 @@ document.getElementById('fields').addEventListener('click', function (ev) {
|
||||
return;
|
||||
}
|
||||
var copy = t.getAttribute('data-copy');
|
||||
if (copy === 'totp') send('copy?f=totp');
|
||||
else if (copy === 'row') send('copy?f=row&k=' + t.getAttribute('data-k'));
|
||||
if (copy === 'totp') { send('copy?f=totp'); return; }
|
||||
if (copy === 'row') { send('copy?f=row&k=' + t.getAttribute('data-k')); return; }
|
||||
|
||||
var edit = t.getAttribute('data-edit');
|
||||
if (edit !== null) { openEditor(edit); return; }
|
||||
if (t.getAttribute('data-editcancel') !== null) { renderDetail(lastDetail); return; }
|
||||
if (t.getAttribute('data-editsave') !== null) {
|
||||
var input = document.getElementById('editval');
|
||||
if (!input || !input.value) return; // vide : on n'ecrase pas par megarde
|
||||
send('setpwd?v=' + encodeURIComponent(input.value));
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// EDITEUR EN PLACE, pas window.prompt : une invite JavaScript demande au
|
||||
// moteur un WebChromeClient que FMX ne fournit pas, et prompt() rend alors
|
||||
// null sans un mot - le bouton semblerait ne rien faire.
|
||||
//
|
||||
// La valeur s'affiche EN CLAIR pendant la saisie. On tape un mot de passe
|
||||
// qu'on ne peut pas relire ailleurs ; le masquer ferait valider a l'aveugle
|
||||
// une frappe qu'aucune confirmation ne rattrape.
|
||||
function openEditor(k) {
|
||||
var row = document.querySelector('[data-v="' + k + '"]');
|
||||
if (!row) return;
|
||||
var line = row.parentNode;
|
||||
line.innerHTML =
|
||||
'<input class="field-in" id="editval" type="text" autocomplete="off"'
|
||||
+ ' autocorrect="off" autocapitalize="none" spellcheck="false"'
|
||||
+ ' style="margin:0">'
|
||||
+ '<button class="btn" data-editcancel>Cancel</button>'
|
||||
+ '<button class="btn main" data-editsave>Save</button>';
|
||||
var input = document.getElementById('editval');
|
||||
input.value = detailRows[k].v;
|
||||
input.focus();
|
||||
send('touch');
|
||||
}
|
||||
|
||||
function goList() {
|
||||
document.body.classList.remove('on-detail');
|
||||
document.body.classList.add('on-list');
|
||||
|
||||
Reference in New Issue
Block a user