feat(p12a): services git distants (PAT/app_password) + clone HTTPS

Modèle de données :
- migration #11 git_credentials (secrets chiffrés SecretBox : secret_encrypted ; colonnes ssh/oauth posées pour P12b/P12c) ; #12 repos ALTER remote_url/git_service/credential_id (pas de FK)
- types partagés api.ts (GitCredentialSummary sans secret + hasSecret/secretLast4, CRUD, RemoteRepoSummary, Clone*) ; protocole additif : topic 'clones' + message clone_update (CloneOperation)

Backend :
- core/git-credentials.ts (GitCredentialsManager(db, box)) : CRUD chiffré, test() (GET /user), getSecret()/authContext() internes, NULLification de repos.credential_id à la suppression
- core/git-clients/ (github/gitlab/gitea) via fetch, sans dépendance : verify()+listRepos() paginés, erreurs typées AUTH_FAILED/RATE_LIMITED/UNREACHABLE, SSRF base_url http(s)
- core/git-auth.ts : withGitAuth (GIT_ASKPASS éphémère 0o700, secret par env, GIT_TERMINAL_PROMPT=0, jamais dans l'URL/.git/config, nettoyage finally)
- core/git.ts cloneRepo (spawn git clone --progress, parse progression, timeout)
- core/clone-manager.ts (EventEmitter) : clone async, dest confiné sous scanRoots + non existant, auto-enregistrement via addRepo + métadonnées de provenance, nettoyage du clone partiel, events topic 'clones'
- routes/git-connections.ts (CRUD + /test + /:id/repos + POST /repos/clone 202 + GET /repos/clone/:id) ; app.ts câble box→GitCredentialsManager + CloneManager→gateway ; gateway relaie 'clones'

Frontend :
- ws-client subscribeClones ; stores git-connections + clone (suivi WS)
- components/settings/GitConnectionsSection (liste + formulaire pat/app_password, secret jamais ré-affiché) inséré dans SettingsView ; CloneRepoModal (connexion → repos distants paginés → dest scanRoots[0] → barre de progression WS → redirection) ; bouton « Cloner » dans DashboardView ; i18n EN+FR

Tests : git-credentials (round-trip SecretBox, résumé sans secret, NULLification) ; acceptance-p12.mjs (clone bare local file:// → clone_update done + repo enregistré + secret ABSENT de l'API, de la DB et du .git/config)

Sous-phases restantes : P12b (SSH), P12c (OAuth device flow).
This commit is contained in:
2026-06-27 14:27:18 +02:00
parent e8d10b7ec0
commit 08695a707d
22 changed files with 1583 additions and 10 deletions

View File

@@ -11,6 +11,8 @@ export default {
settings: 'Settings',
close: 'Close',
retry: 'Retry',
confirm: 'Confirm',
delete: 'Delete',
},
login: {
title: 'Sign in with your access token',
@@ -254,6 +256,50 @@ export default {
pullRebase: 'Pull --rebase',
alreadyPushed: 'Cannot amend: this commit was already pushed.',
},
gitconn: {
title: 'Git connections',
hint: 'Connect to Gitea, GitLab or GitHub to clone private repositories. Tokens are encrypted at rest and never shown again.',
empty: 'No connection yet.',
addConnection: 'Add a connection',
add: 'Add',
service: 'Service',
authType: 'Authentication',
pat: 'Personal access token',
appPassword: 'App password',
label: 'Label',
labelPlaceholder: 'e.g. My Gitea',
baseUrl: 'Instance URL',
username: 'Username',
usernamePlaceholder: 'optional',
token: 'Token',
password: 'Password',
test: 'Test',
added: 'Connection added',
deleted: 'Connection deleted',
testOk: 'Connected as {user}',
testFailed: 'Connection failed: {error}',
},
clone: {
button: 'Clone',
title: 'Clone a repository',
noCredential: 'No git connection configured.',
goSettings: 'Add one in Settings',
credential: 'Connection',
search: 'Search',
searchPlaceholder: 'Filter repositories…',
private: 'private',
noRepos: 'No repository found.',
more: 'Load more',
dest: 'Destination folder',
destPlaceholder: '/path/to/clone',
destHint: 'Must be inside a configured scan folder; the folder is created by the clone.',
clone: 'Clone',
starting: 'Starting…',
cloning: 'Cloning…',
done: 'Clone complete',
failed: 'Clone failed',
back: 'Back',
},
terminal: {
observer: 'observer (read-only)',
sessionEnded: 'Session ended',