Add: multi token management
This commit is contained in:
@@ -25,6 +25,13 @@ type UiSpritePackOption = {
|
||||
pack_id_or_path: string;
|
||||
};
|
||||
|
||||
type UiApiToken = {
|
||||
id: string;
|
||||
label: string;
|
||||
token: string;
|
||||
created_at_ms: number;
|
||||
};
|
||||
|
||||
const WINDOW_PADDING = 16;
|
||||
const WINDOW_WORKAREA_MARGIN = 80;
|
||||
const MIN_WINDOW_SIZE = 64;
|
||||
@@ -62,6 +69,22 @@ async function invokeSetAnimationSlowdownFactor(factor: number): Promise<number>
|
||||
return invoke<number>("set_tauri_animation_slowdown_factor", { factor });
|
||||
}
|
||||
|
||||
async function invokeListApiTokens(): Promise<UiApiToken[]> {
|
||||
return invoke<UiApiToken[]>("list_api_tokens");
|
||||
}
|
||||
|
||||
async function invokeCreateApiToken(label?: string): Promise<UiApiToken> {
|
||||
return invoke<UiApiToken>("create_api_token", { label });
|
||||
}
|
||||
|
||||
async function invokeRenameApiToken(id: string, label: string): Promise<UiApiToken[]> {
|
||||
return invoke<UiApiToken[]>("rename_api_token", { id, label });
|
||||
}
|
||||
|
||||
async function invokeRevokeApiToken(id: string): Promise<UiApiToken[]> {
|
||||
return invoke<UiApiToken[]>("revoke_api_token", { id });
|
||||
}
|
||||
|
||||
function fittedWindowSize(
|
||||
scale: number
|
||||
): { width: number; height: number } {
|
||||
@@ -396,6 +419,9 @@ function MainOverlayWindow(): JSX.Element {
|
||||
function SettingsWindow(): JSX.Element {
|
||||
const [settings, setSettings] = React.useState<UiSettingsSnapshot | null>(null);
|
||||
const [packs, setPacks] = React.useState<UiSpritePackOption[]>([]);
|
||||
const [tokens, setTokens] = React.useState<UiApiToken[]>([]);
|
||||
const [tokenDrafts, setTokenDrafts] = React.useState<Record<string, string>>({});
|
||||
const [newTokenLabel, setNewTokenLabel] = React.useState("");
|
||||
const [activePack, setActivePack] = React.useState<UiSpritePack | null>(null);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const [pending, setPending] = React.useState(false);
|
||||
@@ -406,15 +432,23 @@ function SettingsWindow(): JSX.Element {
|
||||
Promise.all([
|
||||
invoke<UiSettingsSnapshot>("settings_snapshot"),
|
||||
invoke<UiSpritePackOption[]>("list_sprite_packs"),
|
||||
invoke<UiSpritePack>("load_active_sprite_pack")
|
||||
invoke<UiSpritePack>("load_active_sprite_pack"),
|
||||
invokeListApiTokens()
|
||||
])
|
||||
.then(async ([snapshot, options, pack]) => {
|
||||
.then(async ([snapshot, options, pack, authTokens]) => {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setSettings(snapshot);
|
||||
setPacks(options);
|
||||
setActivePack(pack);
|
||||
setTokens(authTokens);
|
||||
setTokenDrafts(
|
||||
authTokens.reduce<Record<string, string>>((acc, token) => {
|
||||
acc[token.id] = token.label;
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
unlisten = await listen<UiSnapshot>("runtime:snapshot", (event) => {
|
||||
if (!mounted) {
|
||||
return;
|
||||
@@ -574,6 +608,85 @@ function SettingsWindow(): JSX.Element {
|
||||
[withPending]
|
||||
);
|
||||
|
||||
const onNewTokenLabelChange = React.useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setNewTokenLabel(event.target.value);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const onCreateToken = React.useCallback(async () => {
|
||||
const created = await withPending(() => invokeCreateApiToken(newTokenLabel || undefined));
|
||||
if (created === null) {
|
||||
return;
|
||||
}
|
||||
const refreshed = await withPending(() => invokeListApiTokens());
|
||||
if (refreshed === null) {
|
||||
return;
|
||||
}
|
||||
setTokens(refreshed);
|
||||
setTokenDrafts(
|
||||
refreshed.reduce<Record<string, string>>((acc, token) => {
|
||||
acc[token.id] = token.label;
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
setNewTokenLabel("");
|
||||
}, [newTokenLabel, withPending]);
|
||||
|
||||
const onTokenDraftChange = React.useCallback((id: string, value: string) => {
|
||||
setTokenDrafts((prev) => ({
|
||||
...prev,
|
||||
[id]: value
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const onRenameToken = React.useCallback(
|
||||
async (id: string) => {
|
||||
const nextLabel = tokenDrafts[id] ?? "";
|
||||
const refreshed = await withPending(() => invokeRenameApiToken(id, nextLabel));
|
||||
if (refreshed === null) {
|
||||
return;
|
||||
}
|
||||
setTokens(refreshed);
|
||||
setTokenDrafts(
|
||||
refreshed.reduce<Record<string, string>>((acc, token) => {
|
||||
acc[token.id] = token.label;
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
},
|
||||
[tokenDrafts, withPending]
|
||||
);
|
||||
|
||||
const onCopyToken = React.useCallback(async (token: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(token);
|
||||
} catch (err) {
|
||||
setError(String(err));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onRevokeToken = React.useCallback(
|
||||
async (id: string) => {
|
||||
if (!window.confirm("Revoke this API token?")) {
|
||||
return;
|
||||
}
|
||||
const refreshed = await withPending(() => invokeRevokeApiToken(id));
|
||||
if (refreshed === null) {
|
||||
return;
|
||||
}
|
||||
setTokens(refreshed);
|
||||
setTokenDrafts(
|
||||
refreshed.reduce<Record<string, string>>((acc, token) => {
|
||||
acc[token.id] = token.label;
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
},
|
||||
[withPending]
|
||||
);
|
||||
|
||||
return (
|
||||
<main className="settings-root">
|
||||
<section className="settings-card">
|
||||
@@ -642,6 +755,68 @@ function SettingsWindow(): JSX.Element {
|
||||
/>
|
||||
<span>Always on top</span>
|
||||
</label>
|
||||
<section className="token-section">
|
||||
<h2>API Tokens</h2>
|
||||
<p className="token-help">
|
||||
Use any listed token as `Authorization: Bearer <token>`.
|
||||
</p>
|
||||
<div className="token-create">
|
||||
<input
|
||||
type="text"
|
||||
value={newTokenLabel}
|
||||
placeholder="New token label"
|
||||
disabled={pending}
|
||||
onChange={onNewTokenLabelChange}
|
||||
/>
|
||||
<button type="button" disabled={pending} onClick={onCreateToken}>
|
||||
Create token
|
||||
</button>
|
||||
</div>
|
||||
<div className="token-list">
|
||||
{tokens.map((entry) => (
|
||||
<article className="token-item" key={entry.id}>
|
||||
<label className="field">
|
||||
<span>Label</span>
|
||||
<input
|
||||
type="text"
|
||||
value={tokenDrafts[entry.id] ?? ""}
|
||||
disabled={pending}
|
||||
onChange={(event) =>
|
||||
onTokenDraftChange(entry.id, event.target.value)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>Token</span>
|
||||
<input type="text" value={entry.token} readOnly />
|
||||
</label>
|
||||
<div className="token-actions">
|
||||
<button
|
||||
type="button"
|
||||
disabled={pending}
|
||||
onClick={() => onCopyToken(entry.token)}
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={pending}
|
||||
onClick={() => onRenameToken(entry.id)}
|
||||
>
|
||||
Save label
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={pending || tokens.length <= 1}
|
||||
onClick={() => onRevokeToken(entry.id)}
|
||||
>
|
||||
Revoke
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user