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.
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
/// sert pour refuser (409) une ecriture si quelqu'un d'autre a publie
/// entre-temps. Vide = creation d'un fichier qui n'existait pas.
@@ -99,6 +105,55 @@ begin
Result := FBaseUrl.ToLower.Contains('/api/v1/repos/');
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
/// { "content": { "sha": ... }, "commit": { ... } }. Vide si la forme change -
/// 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
lisait du meme gris que "Vault on this device" - on ne voyait pas que
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; }
/* 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
@@ -568,6 +568,7 @@
</div>
<div class="scroll">
<h2>Repository</h2>
<div class="note err" id="sRepoErr" hidden></div>
<div class="in-wrap">
<!-- 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
@@ -981,6 +982,9 @@ var ICONS = {
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"/>',
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"/>',
// Un arc, pas un cercle : c'est l'ouverture qui rend la rotation visible.
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
// l'intention. Tout ce qui suit lit et ecrit sur le bouton.
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');
if (k !== null) {
var cell = document.querySelector('[data-v="' + k + '"]');
@@ -1207,6 +1220,7 @@ function openEditor(k) {
var line = row.parentNode;
line.classList.toggle('wrap', !!detailRows[k].m);
line.innerHTML = editorControl(k)
+ (editField === 'password' ? genButton('editval') : '')
// 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.
//
@@ -1355,6 +1369,25 @@ function newField(id, label, rowId) {
/// 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
/// 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) {
if (kind === 'note')
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>';
return '<input class="field-in" id="nPwd" type="text" autocomplete="off"'
+ ' autocorrect="off" autocapitalize="none" spellcheck="false"'
+ ' style="' + FIELD_STYLE + '">';
+ ' style="' + FIELD_STYLE + '">' + genButton('nPwd');
}
function openNewEntry() {
@@ -1633,6 +1666,7 @@ function openSettings() {
document.getElementById('sPwd').value = elPwd.value;
document.getElementById('sPinNew').value = '';
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
// taire quand elle est juste en dessous.
document.getElementById('sUnlockHint').textContent =
@@ -1685,6 +1719,7 @@ document.getElementById('sSave').addEventListener('click', function () {
elTok.value = document.getElementById('sTok').value.trim();
elPwd.value = document.getElementById('sPwd').value;
pushConfig();
document.getElementById('sRepoErr').hidden = true;
toast('Saved');
});
@@ -1838,6 +1873,13 @@ var PM = {
document.body.classList.remove('on-list', 'on-detail', 'searching');
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) {
var s = document.getElementById('status');
s.textContent = text;