feat(apk): generateur de mot de passe, erreurs du depot affichees dans les reglages

- de sur le champ mot de passe (nouvelle entree et modification), tirage
  crypto.getRandomValues avec rejet
- Download vault sans reglages ouvre les reglages
- 404/401 tranches (jeton refuse, utilisateur ou depot inexistant, fichier
  absent) et mauvais mot de passe de synchro : les reglages s'ouvrent avec
  le message en rouge

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-09-18 10:18:47 +01:00
parent 3eb26e23e5
commit ba8c4205f9
3 changed files with 132 additions and 7 deletions
+55
View File
@@ -73,6 +73,12 @@ type
/// n'y ressemble pas est de toute facon inutilisable. /// n'y ressemble pas est de toute facon inutilisable.
function IsGitea: Boolean; function IsGitea: Boolean;
/// POURQUOI UN 404 OU UN 401, en clair pour l'utilisateur. Gitea repond 404
/// aussi bien pour un fichier absent que pour un depot ou un proprietaire
/// inexistant : deux requetes de plus tranchent - le depot, puis
/// l'utilisateur. Vide si rien de plus precis n'a pu etre etabli.
function Diagnose(AHttpCode: Integer): string;
/// Publie un conteneur. ASha est celui du fichier qu'on a lu : Gitea s'en /// Publie un conteneur. ASha est celui du fichier qu'on a lu : Gitea s'en
/// sert pour refuser (409) une ecriture si quelqu'un d'autre a publie /// sert pour refuser (409) une ecriture si quelqu'un d'autre a publie
/// entre-temps. Vide = creation d'un fichier qui n'existait pas. /// entre-temps. Vide = creation d'un fichier qui n'existait pas.
@@ -99,6 +105,55 @@ begin
Result := FBaseUrl.ToLower.Contains('/api/v1/repos/'); Result := FBaseUrl.ToLower.Contains('/api/v1/repos/');
end; end;
function TSyncClient.Diagnose(AHttpCode: Integer): string;
var
LHttp: THTTPClient;
LApi, LOwner, LRepo: string;
LParts: TArray<string>;
LP: Integer;
function CodeOf(const AUrl: string): Integer;
begin
try
Result := LHttp.Get(AUrl).StatusCode;
except
Result := 0;
end;
end;
begin
Result := '';
if AHttpCode = 401 then
Exit('Token refused - expired, revoked or mistyped');
if (AHttpCode <> 404) or not IsGitea then Exit;
// https://hote/api/v1/repos/OWNER/REPO/contents/FICHIER
LP := Pos('/api/v1/repos/', LowerCase(FBaseUrl));
LApi := Copy(FBaseUrl, 1, LP + Length('/api/v1/') - 1);
LParts := Copy(FBaseUrl, LP + Length('/api/v1/repos/'), MaxInt).Split(['/']);
if Length(LParts) < 2 then Exit;
LOwner := LParts[0];
LRepo := LParts[1];
LHttp := THTTPClient.Create;
try
LHttp.ConnectionTimeout := FTimeoutMs;
LHttp.ResponseTimeout := FTimeoutMs;
LHttp.HandleRedirects := False;
LHttp.CustomHeaders['Authorization'] := 'token ' + FToken;
case CodeOf(LApi + 'repos/' + LOwner + '/' + LRepo) of
200: Result := 'No vault in this repository yet - sync from the PC first';
401: Result := 'Token refused - expired, revoked or mistyped';
404:
if CodeOf(LApi + 'users/' + LOwner) = 404 then
Result := 'User "' + LOwner + '" does not exist'
else
Result := 'Repository "' + LOwner + '/' + LRepo +
'" not found, or this token cannot see it';
end;
finally
LHttp.Free;
end;
end;
/// Le sha du fichier ecrit, tire de la reponse au PUT : Gitea rend /// Le sha du fichier ecrit, tire de la reponse au PUT : Gitea rend
/// { "content": { "sha": ... }, "commit": { ... } }. Vide si la forme change - /// { "content": { "sha": ... }, "commit": { ... } }. Vide si la forme change -
/// l'appelant relira, ce qui coute un appel mais ne perd rien. /// l'appelant relira, ce qui coute un appel mais ne perd rien.
+44 -2
View File
@@ -276,7 +276,7 @@
/* UN REFUS N'EST PAS UN ETAT. "Master password: 8 characters at least" se /* UN REFUS N'EST PAS UN ETAT. "Master password: 8 characters at least" se
lisait du meme gris que "Vault on this device" - on ne voyait pas que lisait du meme gris que "Vault on this device" - on ne voyait pas que
l'application venait de refuser quelque chose. */ l'application venait de refuser quelque chose. */
.status.err { color: #ff9d9d; } .status.err, .note.err { color: #ff9d9d; }
.stack { display: flex; flex-direction: column; gap: 8px; } .stack { display: flex; flex-direction: column; gap: 8px; }
/* L'espace de 8 px vit A L'INTERIEUR de chaque pile. Deux piles voisines se /* L'espace de 8 px vit A L'INTERIEUR de chaque pile. Deux piles voisines se
touchaient donc, et "Create vault" collait a "Download vault" alors que touchaient donc, et "Create vault" collait a "Download vault" alors que
@@ -568,6 +568,7 @@
</div> </div>
<div class="scroll"> <div class="scroll">
<h2>Repository</h2> <h2>Repository</h2>
<div class="note err" id="sRepoErr" hidden></div>
<div class="in-wrap"> <div class="in-wrap">
<!-- ZONE DE TEXTE ET NON input : l'URL passe a la ligne et se lit en <!-- ZONE DE TEXTE ET NON input : l'URL passe a la ligne et se lit en
entier. Un input d'une ligne ne defilait pas sous le doigt dans la entier. Un input d'une ligne ne defilait pas sous le doigt dans la
@@ -981,6 +982,9 @@ var ICONS = {
dots: '<circle cx="12" cy="5" r="1.4"/><circle cx="12" cy="12" r="1.4"/>' dots: '<circle cx="12" cy="5" r="1.4"/><circle cx="12" cy="12" r="1.4"/>'
+ '<circle cx="12" cy="19" r="1.4"/>', + '<circle cx="12" cy="19" r="1.4"/>',
check: '<path d="M4 12.5l5.5 5.5L20 7"/>', check: '<path d="M4 12.5l5.5 5.5L20 7"/>',
dice: '<rect x="4" y="4" width="16" height="16" rx="3"/>'
+ '<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"/>', close: '<path d="M6 6l12 12M18 6L6 18"/>',
// Un arc, pas un cercle : c'est l'ouverture qui rend la rotation visible. // Un arc, pas un cercle : c'est l'ouverture qui rend la rotation visible.
loader: '<path d="M12 3a9 9 0 019 9"/>', loader: '<path d="M12 3a9 9 0 019 9"/>',
@@ -1051,6 +1055,15 @@ document.getElementById('fields').addEventListener('click', function (ev) {
// Meme raison que dans la liste : le dessin recoit l'appui, le bouton porte // Meme raison que dans la liste : le dessin recoit l'appui, le bouton porte
// l'intention. Tout ce qui suit lit et ecrit sur le bouton. // l'intention. Tout ce qui suit lit et ecrit sur le bouton.
var t = ev.target.closest('button') || ev.target; var t = ev.target.closest('button') || ev.target;
var gen = t.getAttribute('data-gen');
if (gen) {
// Visible une fois tire : on garde ce qu'on voit, pas une suite de points.
var gi = document.getElementById(gen);
gi.value = genPassword(20);
gi.type = 'text';
send('touch');
return;
}
var k = t.getAttribute('data-show'); var k = t.getAttribute('data-show');
if (k !== null) { if (k !== null) {
var cell = document.querySelector('[data-v="' + k + '"]'); var cell = document.querySelector('[data-v="' + k + '"]');
@@ -1207,6 +1220,7 @@ function openEditor(k) {
var line = row.parentNode; var line = row.parentNode;
line.classList.toggle('wrap', !!detailRows[k].m); line.classList.toggle('wrap', !!detailRows[k].m);
line.innerHTML = editorControl(k) line.innerHTML = editorControl(k)
+ (editField === 'password' ? genButton('editval') : '')
// Show n'a de sens que sur un champ masque. Sur un site en clair il // Show n'a de sens que sur un champ masque. Sur un site en clair il
// n'aurait rien a reveler - et l'inverse : il le CACHERAIT. // n'aurait rien a reveler - et l'inverse : il le CACHERAIT.
// //
@@ -1355,6 +1369,25 @@ function newField(id, label, rowId) {
/// sanitisation de HTML et retire les retours a la ligne : on pouvait lire et /// sanitisation de HTML et retire les retours a la ligne : on pouvait lire et
/// modifier du multiligne, mais pas en creer - il fallait creer la note puis /// modifier du multiligne, mais pas en creer - il fallait creer la note puis
/// l'ouvrir pour la mettre en forme. /// l'ouvrir pour la mettre en forme.
/// MOT DE PASSE ALEATOIRE, tire de crypto.getRandomValues. Rejet des octets
/// au-dela du dernier multiple de la taille du jeu : un simple modulo
/// favoriserait les premiers caracteres.
var GEN_CHARS = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789'
+ '!@#$%^&*-_=+?';
function genPassword(n) {
var out = '', buf = new Uint8Array(1),
lim = 256 - (256 % GEN_CHARS.length);
while (out.length < (n || 20)) {
crypto.getRandomValues(buf);
if (buf[0] < lim) out += GEN_CHARS.charAt(buf[0] % GEN_CHARS.length);
}
return out;
}
function genButton(target) {
return '<button class="btn ico-btn" data-gen="' + target + '">'
+ ico('dice', 'Generate') + '</button>';
}
function newBodyControl(kind) { function newBodyControl(kind) {
if (kind === 'note') if (kind === 'note')
return '<textarea class="field-in" id="nPwd" rows="6" enterkeyhint="enter"' return '<textarea class="field-in" id="nPwd" rows="6" enterkeyhint="enter"'
@@ -1362,7 +1395,7 @@ function newBodyControl(kind) {
+ ';height:auto;padding:10px 14px;resize:vertical"></textarea>'; + ';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="text" autocomplete="off"'
+ ' autocorrect="off" autocapitalize="none" spellcheck="false"' + ' autocorrect="off" autocapitalize="none" spellcheck="false"'
+ ' style="' + FIELD_STYLE + '">'; + ' style="' + FIELD_STYLE + '">' + genButton('nPwd');
} }
function openNewEntry() { function openNewEntry() {
@@ -1633,6 +1666,7 @@ function openSettings() {
document.getElementById('sPwd').value = elPwd.value; document.getElementById('sPwd').value = elPwd.value;
document.getElementById('sPinNew').value = ''; document.getElementById('sPinNew').value = '';
document.getElementById('secCode').hidden = !vaultOpen; document.getElementById('secCode').hidden = !vaultOpen;
document.getElementById('sRepoErr').hidden = true;
// La section n'existe que coffre ouvert : le dire tant qu'elle manque, et se // La section n'existe que coffre ouvert : le dire tant qu'elle manque, et se
// taire quand elle est juste en dessous. // taire quand elle est juste en dessous.
document.getElementById('sUnlockHint').textContent = document.getElementById('sUnlockHint').textContent =
@@ -1685,6 +1719,7 @@ document.getElementById('sSave').addEventListener('click', function () {
elTok.value = document.getElementById('sTok').value.trim(); elTok.value = document.getElementById('sTok').value.trim();
elPwd.value = document.getElementById('sPwd').value; elPwd.value = document.getElementById('sPwd').value;
pushConfig(); pushConfig();
document.getElementById('sRepoErr').hidden = true;
toast('Saved'); toast('Saved');
}); });
@@ -1838,6 +1873,13 @@ var PM = {
document.body.classList.remove('on-list', 'on-detail', 'searching'); document.body.classList.remove('on-list', 'on-detail', 'searching');
view = 'unlock'; view = 'unlock';
}, },
/// Erreur de depot, affichee dans les reglages au-dessus des champs a corriger.
settingsError: function (msg) {
var e = document.getElementById('sRepoErr');
e.textContent = msg;
e.hidden = false;
},
setStatus: function (text, err) { setStatus: function (text, err) {
var s = document.getElementById('status'); var s = document.getElementById('status');
s.textContent = text; s.textContent = text;
+33 -5
View File
@@ -352,6 +352,7 @@ type
/// que le proprietaire ne connaitrait pas. /// que le proprietaire ne connaitrait pas.
procedure SetUnlockCode(const ANew: string); procedure SetUnlockCode(const ANew: string);
procedure PushUnlockMode; procedure PushUnlockMode;
procedure ShowSettingsError(const AMsg: string);
// Scanner du QR affiche par le poste (ZXing.Delphi, Apache 2.0). // Scanner du QR affiche par le poste (ZXing.Delphi, Apache 2.0).
var var
FScanning, FScanBusy: Boolean; FScanning, FScanBusy: Boolean;
@@ -2526,9 +2527,15 @@ begin
// range qui ne convient plus. // range qui ne convient plus.
LPwd := FPwd; LPwd := FPwd;
if LPwd = '' then LPwd := FSyncPwd; if LPwd = '' then LPwd := FSyncPwd;
if (LUrl = '') or (LTok = '') then
begin
ShowSettingsError('Set the repository first - scan the QR shown on the PC,'
+ ' or type the URL and the token');
Exit;
end;
if LPwd = '' then if LPwd = '' then
begin begin
StatusErr('Sync password required'); ShowSettingsError('Sync password required');
Exit; Exit;
end; end;
WebCall('PM.clearLog()'); WebCall('PM.clearLog()');
@@ -2745,8 +2752,15 @@ begin
// Le serveur repond pareil dans les deux cas, par conception. // Le serveur repond pareil dans les deux cas, par conception.
Log(' 404: no snapshot OR invalid token'); Log(' 404: no snapshot OR invalid token');
if not ASilent then if not ASilent then
TThread.Queue(nil, procedure begin
begin Status('Nothing to download (404)'); end); // TRANCHER L'AMBIGUITE : fichier absent, depot ou utilisateur
// inexistant - deux requetes de plus, seulement quand
// quelqu'un attend la reponse.
var LWhy := LClient.Diagnose(404);
if LWhy = '' then LWhy := 'Nothing to download (404)';
Log(' ' + LWhy);
TThread.Queue(nil, procedure begin ShowSettingsError(LWhy); end);
end;
Exit; Exit;
end; end;
ssRefused, ssHttpError, ssNetworkError: ssRefused, ssHttpError, ssNetworkError:
@@ -2755,7 +2769,12 @@ begin
// est affiche, hors ligne est un etat normal, et une alerte ici // est affiche, hors ligne est un etat normal, et une alerte ici
// apprendrait a l'utilisateur a ignorer les alertes. // apprendrait a l'utilisateur a ignorer les alertes.
Log(' failed: ' + LRes.Error); Log(' failed: ' + LRes.Error);
if not ASilent then if (not ASilent) and (LRes.HttpCode = 401) then
begin
var LWhy401 := LClient.Diagnose(401);
TThread.Queue(nil, procedure begin ShowSettingsError(LWhy401); end);
end
else if not ASilent then
TThread.Queue(nil, procedure TThread.Queue(nil, procedure
begin StatusErr('Network failure'); end); begin StatusErr('Network failure'); end);
Exit; Exit;
@@ -2881,7 +2900,7 @@ begin
// le champ prerempli, et le coffre local reste consultable. // le champ prerempli, et le coffre local reste consultable.
if not ASilent then if not ASilent then
TThread.Queue(nil, procedure TThread.Queue(nil, procedure
begin Status('Failed - wrong sync password?'); end); begin ShowSettingsError('Wrong sync password - it must be the one set on the PC'); end);
end; end;
end; end;
finally finally
@@ -3620,6 +3639,15 @@ begin
WebCall('PM.setStatus(' + JsArg(AMsg) + ', true)'); WebCall('PM.setStatus(' + JsArg(AMsg) + ', true)');
end; end;
/// UNE ERREUR DE REGLAGE S'AFFICHE LA OU ON LA CORRIGE : les reglages s'ouvrent
/// et le message y est en rouge, au-dessus des champs du depot.
procedure TForm8.ShowSettingsError(const AMsg: string);
begin
StatusErr(AMsg);
WebCall('openSettings()');
WebCall('PM.settingsError(' + JsArg(AMsg) + ')');
end;
procedure TForm8.SetBusy(ABusy: Boolean); procedure TForm8.SetBusy(ABusy: Boolean);
var var
LCanOpen: Boolean; LCanOpen: Boolean;