add fingerprint view to both scenarios

This commit is contained in:
pomidorry
2026-06-09 02:34:35 +03:00
parent d324fa340d
commit 553e389943
4 changed files with 89 additions and 17 deletions
+36
View File
@@ -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())
+23 -3
View File
@@ -5,15 +5,35 @@
<a href="{{AppSubUrl}}/user/settings/keys" target="_blank">{{ctx.Locale.Tr "repo.migrate.ssh_helper_link"}}</a>
</span>
</div>
<div class="inline field ssh-key-owner-selector tw-hidden" data-signed-user-id="{{.SignedUser.ID}}">
<div class="inline field ssh-key-fingerprint-only tw-hidden">
<label>{{ctx.Locale.Tr "repo.migrate.ssh_key_owner_label"}}</label>
<div class="inline-right">
<span>{{ctx.Locale.Tr "repo.migrate.ssh_key_owner_personal" .SignedUser.Name}}</span>
<code class="tw-text-12">{{.SignedUserSSHFingerprint}}</code>
</div>
</div>
<div class="inline field ssh-key-owner-selector tw-hidden"
data-signed-user-id="{{.SignedUser.ID}}"
data-signed-user-fingerprint="{{.SignedUserSSHFingerprint}}"
data-owner-fingerprints="{{.OwnerSSHFingerprintsJSON}}">
<label>{{ctx.Locale.Tr "repo.migrate.ssh_key_owner_label"}}</label>
<div class="ui selection dropdown">
<input id="ssh_key_owner_id" name="ssh_key_owner_id" type="hidden" value="0">
<div class="default text">{{ctx.Locale.Tr "repo.migrate.ssh_key_owner_org_default"}}</div>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu">
<div class="item" data-value="0">{{ctx.Locale.Tr "repo.migrate.ssh_key_owner_org_default"}}</div>
<div class="item" data-value="{{.SignedUser.ID}}">{{ctx.Locale.Tr "repo.migrate.ssh_key_owner_personal" .SignedUser.Name}}</div>
<div class="item" data-value="0" data-text="{{ctx.Locale.Tr "repo.migrate.ssh_key_owner_org_default"}}">
<div class="tw-flex tw-flex-col tw-gap-1">
<span>{{ctx.Locale.Tr "repo.migrate.ssh_key_owner_org_default"}}</span>
<code class="item-fingerprint tw-text-12"></code>
</div>
</div>
<div class="item" data-value="{{.SignedUser.ID}}" data-text="{{ctx.Locale.Tr "repo.migrate.ssh_key_owner_personal" .SignedUser.Name}}">
<div class="tw-flex tw-flex-col tw-gap-1">
<span>{{ctx.Locale.Tr "repo.migrate.ssh_key_owner_personal" .SignedUser.Name}}</span>
<code class="item-fingerprint tw-text-12">{{.SignedUserSSHFingerprint}}</code>
</div>
</div>
</div>
</div>
</div>
+4
View File
@@ -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;
}
+26 -14
View File
@@ -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<HTMLElement>('.ssh-key-owner-selector');
const selector = document.querySelector<HTMLElement>('.ssh-key-owner-selector');
const fingerprintOnly = document.querySelector<HTMLElement>('.ssh-key-fingerprint-only');
const hiddenId = document.querySelector<HTMLInputElement>('#ssh_key_owner_id');
const uidInput = document.querySelector<HTMLInputElement>('#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<string, string> = JSON.parse(selector.getAttribute('data-owner-fingerprints') || '{}');
const orgDefaultFingerprintEl = selector.querySelector<HTMLElement>('.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<HTMLElement>('.owner.dropdown .menu .item')) {
item.addEventListener('click', () => setTimeout(update, 0));
}