{"record":{"id":"84947e0a45039708","repo":"AlistGo/alist","slug":"key-with-the-same-title-already-exists","errorCode":null,"errorMessage":"key with the same title already exists","messagePattern":"key with the same title already exists","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/op/sshkey.go","lineNumber":14,"sourceCode":"package op\n\nimport (\n\t\"github.com/alist-org/alist/v3/internal/db\"\n\t\"github.com/alist-org/alist/v3/internal/model\"\n\t\"github.com/pkg/errors\"\n\t\"golang.org/x/crypto/ssh\"\n\t\"time\"\n)\n\nfunc CreateSSHPublicKey(k *model.SSHPublicKey) (error, bool) {\n\t_, err := db.GetSSHPublicKeyByUserTitle(k.UserId, k.Title)\n\tif err == nil {\n\t\treturn errors.New(\"key with the same title already exists\"), true\n\t}\n\tpubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(k.KeyStr))\n\tif err != nil {\n\t\treturn err, false\n\t}\n\tk.Fingerprint = ssh.FingerprintSHA256(pubKey)\n\tk.AddedTime = time.Now()\n\tk.LastUsedTime = k.AddedTime\n\treturn db.CreateSSHPublicKey(k), true\n}\n\nfunc GetSSHPublicKeyByUserId(userId uint, pageIndex, pageSize int) (keys []model.SSHPublicKey, count int64, err error) {\n\treturn db.GetSSHPublicKeyByUserId(userId, pageIndex, pageSize)\n}\n\nfunc GetSSHPublicKeyByIdAndUserId(id uint, userId uint) (*model.SSHPublicKey, error) {\n\tkey, err := db.GetSSHPublicKeyById(id)\n\tif err != nil {","sourceCodeStart":1,"sourceCodeEnd":32,"githubUrl":"https://github.com/AlistGo/alist/blob/843d9dc8149126976b2625911e45a4d3ffd6f2f5/internal/op/sshkey.go#L1-L32","documentation":"CreateSSHPublicKey enforces per-user title uniqueness: db.GetSSHPublicKeyByUserTitle(userId, title) succeeding means a key with that title already exists for this user, so creation is refused (the bool return is true, indicating the key was not created but no internal failure occurred). Titles are the user-facing handle for keys, so duplicates would be ambiguous.","triggerScenarios":"Calling op.CreateSSHPublicKey with a title the same user already used for another key, regardless of the key material being different.","commonSituations":"Importing a rotated key under the old title (e.g. 'laptop') without deleting the previous entry; scripted key setup running twice; importing a keyset where titles collide.","solutions":["Delete or rename the existing key with that title first, then create the new one","Use a distinct title (e.g. 'laptop-2026') for rotated keys","Make setup scripts idempotent: check GetSSHPublicKeyByUserTitle before creating"],"exampleFix":"// before\nerr, created := op.CreateSSHPublicKey(&model.SSHPublicKey{UserId: uid, Title: \"laptop\", KeyStr: newKey})\n// after: delete the stale entry with that title first\nif _, err := db.GetSSHPublicKeyByUserTitle(uid, \"laptop\"); err == nil {\n    _ = db.DeleteSSHPublicKeyByTitle(uid, \"laptop\") // or update its KeyStr\n}\nerr, created := op.CreateSSHPublicKey(&model.SSHPublicKey{UserId: uid, Title: \"laptop\", KeyStr: newKey})","handlingStrategy":"validation","validationCode":"// Before creating a key\nif _, err := db.GetSSHPublicKeyByUserTitle(k.UserId, k.Title); err == nil {\n    return errors.New(\"title in use; choose another or delete the old key\")\n}","typeGuard":null,"tryCatchPattern":"if err, created := op.CreateSSHPublicKey(k); err != nil {\n    if strings.Contains(err.Error(), \"same title\") {\n        // prompt user to pick a new title or replace the existing key\n    }\n}","preventionTips":["Make key-provisioning scripts check title availability first","Use dated/suffixed titles for rotated keys","Expose existing titles in the UI's add-key form"],"tags":["ssh-keys","uniqueness","user-management"],"backgroundTag":null,"analyzedSha":"843d9dc8149126976b2625911e45a4d3ffd6f2f5","analyzedAt":"2026-08-15T12:14:11.722Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}