mirror of
https://github.com/nathonius/obsidian-github-link.git
synced 2026-07-22 09:20:25 +00:00
✨ feat: Use Obsidian's SecretComponent for token management
Switch to Obsidian's built-in SecretComponent UI widget for token selection, letting users pick or create named secrets through the standard Obsidian keychain interface. - Add tokenSecret field to GithubAccount to store the secret name - Replace plain text token input with SecretComponent via addComponent - Auto-generate secret names from account name during migration - Read token values at runtime via secretStorage.getSecret(tokenSecret)
This commit is contained in:
parent
b477774e1c
commit
2f6ec2637e
4 changed files with 71 additions and 40 deletions
|
|
@ -5,11 +5,22 @@ import { PluginSettingTabMock } from "./obsidian/PluginSettingTab";
|
|||
import { NoticeMock } from "./obsidian/Notice";
|
||||
import { setIconMock } from "./obsidian/setIcon";
|
||||
|
||||
class SecretComponentMock {
|
||||
constructor(_app: unknown, _el: unknown) {}
|
||||
setValue(_value: string) {
|
||||
return this;
|
||||
}
|
||||
onChange(_cb: (value: string) => unknown) {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
App: AppMock,
|
||||
Plugin: PluginMock,
|
||||
Modal: ModalMock,
|
||||
PluginSettingTab: PluginSettingTabMock,
|
||||
Notice: NoticeMock,
|
||||
SecretComponent: SecretComponentMock,
|
||||
setIcon: setIconMock,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,44 +2,59 @@ import type { SecretStorage } from "obsidian";
|
|||
import type { GithubAccount } from "./settings/types";
|
||||
import { logger } from "./plugin";
|
||||
|
||||
const KEY_PREFIX = "gh-link-";
|
||||
|
||||
let storage: SecretStorage;
|
||||
|
||||
function keyFor(accountId: string): string {
|
||||
return `${KEY_PREFIX}${accountId}`;
|
||||
}
|
||||
|
||||
export function initKeychain(secretStorage: SecretStorage): void {
|
||||
storage = secretStorage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the token value from SecretStorage using the account's secret name.
|
||||
*/
|
||||
export function getToken(account: GithubAccount): string | null {
|
||||
return storage.getSecret(keyFor(account.id));
|
||||
}
|
||||
|
||||
export function setToken(account: GithubAccount, token: string): void {
|
||||
storage.setSecret(keyFor(account.id), token);
|
||||
}
|
||||
|
||||
export function clearToken(account: GithubAccount): void {
|
||||
storage.setSecret(keyFor(account.id), "");
|
||||
if (!account.tokenSecret) {
|
||||
return null;
|
||||
}
|
||||
return storage.getSecret(account.tokenSecret);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate plain-text tokens from data.json into the keychain.
|
||||
* Store a token under a named secret and update the account's tokenSecret reference.
|
||||
*/
|
||||
export function setToken(account: GithubAccount, token: string, secretName?: string): void {
|
||||
if (!secretName && !account.tokenSecret) {
|
||||
secretName = `github-link-${account.name.toLowerCase().replace(/[^a-z0-9]/g, "-")}`;
|
||||
}
|
||||
if (secretName) {
|
||||
account.tokenSecret = secretName;
|
||||
}
|
||||
storage.setSecret(account.tokenSecret, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the token value from SecretStorage (the entry remains with an empty value).
|
||||
*/
|
||||
export function clearToken(account: GithubAccount): void {
|
||||
if (account.tokenSecret) {
|
||||
storage.setSecret(account.tokenSecret, "");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate plain-text tokens from data.json into named secrets.
|
||||
* Called once on data version upgrade.
|
||||
*/
|
||||
export function migrateTokens(accounts: GithubAccount[]): void {
|
||||
for (const account of accounts) {
|
||||
if (account.token) {
|
||||
if (account.token && !account.tokenSecret) {
|
||||
setToken(account, account.token);
|
||||
logger.info(`Migrated token for account "${account.name}" to Obsidian Keychain.`);
|
||||
logger.info(`Migrated token for account "${account.name}" to Obsidian Keychain as "${account.tokenSecret}".`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate in-memory account tokens from the keychain.
|
||||
* Populate in-memory account tokens from SecretStorage on startup.
|
||||
*/
|
||||
export function loadTokens(accounts: GithubAccount[]): void {
|
||||
for (const account of accounts) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { App } from "obsidian";
|
||||
import { Setting } from "obsidian";
|
||||
import { SecretComponent, Setting } from "obsidian";
|
||||
import type { Verification } from "@octokit/auth-oauth-device/dist-types/types";
|
||||
import { AuthModal } from "../auth-modal";
|
||||
import { auth } from "../github/auth";
|
||||
|
|
@ -29,7 +29,7 @@ export class AccountSettings {
|
|||
saveNewAccountCallback: (account: GithubAccount) => Promise<void>,
|
||||
): void {
|
||||
if (!this.newAccount) {
|
||||
this.newAccount = { id: crypto.randomUUID(), name: "", orgs: [], token: "", customOAuth: false };
|
||||
this.newAccount = { id: crypto.randomUUID(), name: "", orgs: [], token: "", tokenSecret: "", customOAuth: false };
|
||||
}
|
||||
// TODO: Combine the new account and existing account rendering to reduce duplication
|
||||
const accountContainer = container.createDiv();
|
||||
|
|
@ -99,7 +99,7 @@ export class AccountSettings {
|
|||
new Setting(accountContainer)
|
||||
.setName("Token")
|
||||
.setDesc(
|
||||
"A GitHub token, which can be generated automatically (recommended) or by creating a personal access token (not recommended unless org does not allow OAuth tokens). Required.",
|
||||
"A GitHub token stored in Obsidian's keychain. Generate one automatically (recommended) or link an existing secret.",
|
||||
)
|
||||
.addButton((button) => {
|
||||
button.setButtonText("Generate Token");
|
||||
|
|
@ -118,14 +118,15 @@ export class AccountSettings {
|
|||
this.displayCallback();
|
||||
});
|
||||
})
|
||||
.addText((text) => {
|
||||
text.setPlaceholder("Personal Access Token / OAuth Token");
|
||||
text.setValue(this.newAccount!.token);
|
||||
text.onChange((value) => {
|
||||
this.newAccount!.token = value.trim();
|
||||
setToken(this.newAccount!, value.trim());
|
||||
});
|
||||
});
|
||||
.addComponent((el) =>
|
||||
new SecretComponent(this.app, el)
|
||||
.setValue(this.newAccount!.tokenSecret)
|
||||
.onChange((secretName) => {
|
||||
this.newAccount!.tokenSecret = secretName;
|
||||
const token = this.app.secretStorage.getSecret(secretName);
|
||||
this.newAccount!.token = token ?? "";
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(accountContainer).addButton((button) => {
|
||||
button.setButtonText("Save account");
|
||||
|
|
@ -207,7 +208,7 @@ export class AccountSettings {
|
|||
new Setting(accountContainer)
|
||||
.setName("Token")
|
||||
.setDesc(
|
||||
"A GitHub token, which can be generated automatically (recommended) or by creating a personal access token (not recommended unless org does not allow OAuth tokens).",
|
||||
"A GitHub token stored in Obsidian's keychain. Generate one automatically (recommended) or link an existing secret.",
|
||||
)
|
||||
.addButton((button) => {
|
||||
button.setButtonText("Generate Token");
|
||||
|
|
@ -227,15 +228,16 @@ export class AccountSettings {
|
|||
this.displayCallback();
|
||||
});
|
||||
})
|
||||
.addText((text) => {
|
||||
text.setPlaceholder("Personal Access Token / OAuth Token");
|
||||
text.setValue(account.token);
|
||||
text.onChange((value) => {
|
||||
account.token = value.trim();
|
||||
setToken(account, value.trim());
|
||||
void this.saveCallback();
|
||||
});
|
||||
});
|
||||
.addComponent((el) =>
|
||||
new SecretComponent(this.app, el)
|
||||
.setValue(account.tokenSecret)
|
||||
.onChange(async (secretName) => {
|
||||
account.tokenSecret = secretName;
|
||||
const token = this.app.secretStorage.getSecret(secretName);
|
||||
account.token = token ?? "";
|
||||
await this.saveCallback();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private tokenVerification(verification: Verification) {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ export interface GithubAccount {
|
|||
id: string;
|
||||
name: string;
|
||||
orgs: string[];
|
||||
/** Runtime-only: populated from secretStorage on load. Not persisted. */
|
||||
token: string;
|
||||
/** Name of the secret in Obsidian's SecretStorage. Persisted in data.json. */
|
||||
tokenSecret: string;
|
||||
customOAuth?: boolean;
|
||||
clientId?: string;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue