From 553e389943ca33a550e604adba8c15b20010c69f Mon Sep 17 00:00:00 2001 From: pomidorry Date: Tue, 9 Jun 2026 02:34:35 +0300 Subject: [PATCH] add fingerprint view to both scenarios --- routers/web/repo/migrate.go | 36 ++++++++++++++++++++++ templates/repo/migrate/ssh_options.tmpl | 26 ++++++++++++++-- web_src/css/repo.css | 4 +++ web_src/js/features/repo-migrate.ts | 40 ++++++++++++++++--------- 4 files changed, 89 insertions(+), 17 deletions(-) diff --git a/routers/web/repo/migrate.go b/routers/web/repo/migrate.go index fcd94bb2b1..0f094fcb81 100644 --- a/routers/web/repo/migrate.go +++ b/routers/web/repo/migrate.go @@ -11,6 +11,7 @@ import ( admin_model "gitea.dev/models/admin" "gitea.dev/models/db" + "gitea.dev/models/organization" repo_model "gitea.dev/models/repo" user_model "gitea.dev/models/user" "gitea.dev/modules/git" @@ -18,6 +19,7 @@ import ( "gitea.dev/modules/lfs" "gitea.dev/modules/log" "gitea.dev/modules/setting" + ssh_module "gitea.dev/modules/ssh" "gitea.dev/modules/structs" "gitea.dev/modules/templates" "gitea.dev/modules/util" @@ -67,10 +69,43 @@ func Migrate(ctx *context.Context) { return } ctx.Data["ContextUser"] = ctxUser + setManagedSSHKeyFingerprints(ctx, ctxUser) ctx.HTML(http.StatusOK, templates.TplName("repo/migrate/"+serviceType.Name())) } +// setManagedSSHKeyFingerprints pre-loads the SignedUser's and each candidate +// org's managed SSH key fingerprint, so the migrate form can display the +// fingerprint that will be used to authenticate an SSH clone. The keypair is +// created lazily by GetOrCreateSSHKeypair, so this also doubles as a primer. +func setManagedSSHKeyFingerprints(ctx *context.Context, ctxUser *user_model.User) { + if ctx.Doer == nil { + return + } + if kp, err := ssh_module.GetOrCreateSSHKeypair(ctx, ctx.Doer.ID); err == nil { + ctx.Data["SignedUserSSHFingerprint"] = kp.Fingerprint + } + + fingerprints := map[int64]string{} + if orgs, ok := ctx.Data["Orgs"].([]*organization.Organization); ok { + for _, org := range orgs { + if kp, err := ssh_module.GetOrCreateSSHKeypair(ctx, org.ID); err == nil { + fingerprints[org.ID] = kp.Fingerprint + } + } + } + if ctxUser != nil && ctxUser.ID != ctx.Doer.ID { + if _, seen := fingerprints[ctxUser.ID]; !seen { + if kp, err := ssh_module.GetOrCreateSSHKeypair(ctx, ctxUser.ID); err == nil { + fingerprints[ctxUser.ID] = kp.Fingerprint + } + } + } + if data, err := json.Marshal(fingerprints); err == nil { + ctx.Data["OwnerSSHFingerprintsJSON"] = string(data) + } +} + func handleMigrateError(ctx *context.Context, owner *user_model.User, err error, name string, tpl templates.TplName, form *forms.MigrateRepoForm) { if setting.Repository.DisableMigrations { ctx.HTTPError(http.StatusForbidden, "MigrateError: the site administrator has disabled migrations") @@ -169,6 +204,7 @@ func MigratePost(ctx *context.Context) { return } ctx.Data["ContextUser"] = ctxUser + setManagedSSHKeyFingerprints(ctx, ctxUser) tpl := templates.TplName("repo/migrate/" + form.Service.Name()) diff --git a/templates/repo/migrate/ssh_options.tmpl b/templates/repo/migrate/ssh_options.tmpl index a19b5d3d29..49605dfcc6 100644 --- a/templates/repo/migrate/ssh_options.tmpl +++ b/templates/repo/migrate/ssh_options.tmpl @@ -5,15 +5,35 @@ {{ctx.Locale.Tr "repo.migrate.ssh_helper_link"}} -
+
+ +
+ {{ctx.Locale.Tr "repo.migrate.ssh_key_owner_personal" .SignedUser.Name}} + {{.SignedUserSSHFingerprint}} +
+
+
diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 0682290e1a..f3dade44ea 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -1967,3 +1967,7 @@ tbody.commit-list { .branch-selector-dropdown .scrolling.menu .loading-indicator { height: 4em; } + +.ssh-key-owner-selector .item-fingerprint:empty { + display: none; +} diff --git a/web_src/js/features/repo-migrate.ts b/web_src/js/features/repo-migrate.ts index 287352111b..e2bc3b13b8 100644 --- a/web_src/js/features/repo-migrate.ts +++ b/web_src/js/features/repo-migrate.ts @@ -99,35 +99,47 @@ export function initRepoMigrationForm() { initSSHKeyOwnerSelector(cloneAddrInput); } -// initSSHKeyOwnerSelector wires the "managed SSH key owner" selector. It is -// hidden by default and only shown when an SSH URL is entered AND the chosen -// target owner is an organisation (i.e. not the signed-in user) — in that case -// the user can pick between the org's managed key (default) and their personal -// managed key. The hidden #ssh_key_owner_id field is submitted with the form. +// initSSHKeyOwnerSelector wires the managed SSH key UI on the migrate form. +// For SSH URLs it shows either a single fingerprint line (personal target — +// no choice) or a dropdown with "this org's key" vs "your personal key", and +// updates the org default item with the current org's fingerprint so the user +// always sees which key will be used. For non-SSH URLs everything stays hidden. function initSSHKeyOwnerSelector(cloneAddrInput: HTMLInputElement) { - const container = document.querySelector('.ssh-key-owner-selector'); + const selector = document.querySelector('.ssh-key-owner-selector'); + const fingerprintOnly = document.querySelector('.ssh-key-fingerprint-only'); const hiddenId = document.querySelector('#ssh_key_owner_id'); const uidInput = document.querySelector('#uid'); - if (!container || !hiddenId || !uidInput) return; + if (!selector || !hiddenId || !uidInput) return; - const signedUserID = container.getAttribute('data-signed-user-id') ?? ''; + const signedUserID = selector.getAttribute('data-signed-user-id') ?? ''; + const ownerFingerprints: Record = JSON.parse(selector.getAttribute('data-owner-fingerprints') || '{}'); + const orgDefaultFingerprintEl = selector.querySelector('.menu .item[data-value="0"] .item-fingerprint'); function update() { const isSSH = isSSHURL(cloneAddrInput.value.trim()); const targetUid = uidInput!.value; - // No choice: non-SSH URL, or migrating into the user's own account - if (!isSSH || targetUid === signedUserID) { - hideElem(container!); + if (!isSSH) { + hideElem(selector!); + if (fingerprintOnly) hideElem(fingerprintOnly); hiddenId!.value = '0'; return; } - // Target is an organisation — show selector (Fomantic dropdown wires the hidden input itself) - showElem(container!); + // Personal target — no choice to make, just surface the fingerprint. + if (targetUid === signedUserID) { + hideElem(selector!); + if (fingerprintOnly) showElem(fingerprintOnly); + hiddenId!.value = '0'; + return; + } + + // Org target — show dropdown; populate the org-default item's fingerprint. + if (fingerprintOnly) hideElem(fingerprintOnly); + if (orgDefaultFingerprintEl) orgDefaultFingerprintEl.textContent = ownerFingerprints[targetUid] ?? ''; + showElem(selector!); } - // Semantic UI updates the #uid hidden input via menu item clicks for (const item of document.querySelectorAll('.owner.dropdown .menu .item')) { item.addEventListener('click', () => setTimeout(update, 0)); }