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:
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user