CI / config (push) Canceled after 0s
CI / test-frontend (push) Canceled after 0s
CI / test-rust (push) Canceled after 0s
CI / test-e2e (push) Canceled after 0s
CI / test-e2e-release (push) Canceled after 0s
CI / build (push) Canceled after 0s
CI / release (push) Canceled after 0s
CI / docker (push) Canceled after 0s
CI / docker-website (push) Canceled after 0s
补齐普通用户自助账号、中文设置与快捷键提示,修复大型扩展安装和移动端弹窗,并按服务器能力限制不兼容扩展。 Co-Authored-By: Codex <noreply@anthropic.com>
161 lines
4.9 KiB
TypeScript
161 lines
4.9 KiB
TypeScript
import { Alert, Badge, Button, Input } from "@silverbulletmd/silverbullet/ui";
|
|
import { useEffect, useState } from "preact/hooks";
|
|
import {
|
|
changeOwnPassword,
|
|
createOwnToken,
|
|
deleteOwnToken,
|
|
formatApiError,
|
|
getOwnAccount,
|
|
} from "../api.ts";
|
|
import { spacesUrl } from "../routes.ts";
|
|
import type { AuthState, UserInfo } from "../types.ts";
|
|
|
|
export function AccountView({
|
|
auth,
|
|
onUnauthorized,
|
|
}: {
|
|
auth: Extract<AuthState, { phase: "authed" }>;
|
|
onUnauthorized: () => void;
|
|
}) {
|
|
const [user, setUser] = useState<UserInfo>();
|
|
const [error, setError] = useState("");
|
|
const [currentPassword, setCurrentPassword] = useState("");
|
|
const [newPassword, setNewPassword] = useState("");
|
|
const [tokenName, setTokenName] = useState("");
|
|
const [shownToken, setShownToken] = useState<string>();
|
|
|
|
async function reload() {
|
|
try {
|
|
const account = await getOwnAccount();
|
|
setUser(account.user);
|
|
setError("");
|
|
} catch (error: any) {
|
|
if (error.unauthorized) onUnauthorized();
|
|
else setError(formatApiError(error));
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
void reload();
|
|
}, []);
|
|
|
|
async function run(action: () => Promise<void>) {
|
|
try {
|
|
await action();
|
|
setError("");
|
|
} catch (error: any) {
|
|
if (error.unauthorized) onUnauthorized();
|
|
else setError(formatApiError(error));
|
|
}
|
|
}
|
|
|
|
if (!user && !error) return <p>正在加载…</p>;
|
|
return (
|
|
<div>
|
|
<h1>
|
|
{auth.username} <Badge>{auth.admin ? "管理员" : "普通用户"}</Badge>
|
|
</h1>
|
|
{error && <Alert variant="error">{error}</Alert>}
|
|
{auth.admin && (
|
|
<p>
|
|
<a class="sb-button" href={spacesUrl("/users")}>
|
|
管理全部用户
|
|
</a>
|
|
</p>
|
|
)}
|
|
<section>
|
|
<h2>修改密码</h2>
|
|
<p class="sb-help-text">修改后其他设备上的登录会话将失效。</p>
|
|
<div class="row">
|
|
<Input
|
|
type="password"
|
|
aria-label="当前密码"
|
|
placeholder="当前密码"
|
|
value={currentPassword}
|
|
onInput={(event) => setCurrentPassword(event.currentTarget.value)}
|
|
/>
|
|
<Input
|
|
type="password"
|
|
aria-label="新密码"
|
|
placeholder="新密码"
|
|
value={newPassword}
|
|
onInput={(event) => setNewPassword(event.currentTarget.value)}
|
|
/>
|
|
<Button
|
|
variant="primary"
|
|
disabled={!currentPassword || !newPassword}
|
|
onClick={() =>
|
|
void run(async () => {
|
|
await changeOwnPassword(currentPassword, newPassword);
|
|
location.assign(
|
|
`${spacesUrl("/login")}?next=${encodeURIComponent(spacesUrl("/account"))}`,
|
|
);
|
|
})
|
|
}
|
|
>
|
|
更新密码
|
|
</Button>
|
|
</div>
|
|
</section>
|
|
<section>
|
|
<h2>我的 API 令牌</h2>
|
|
<p class="sb-help-text">用于外部工具访问,请像密码一样妥善保管。</p>
|
|
{user && Object.keys(user.tokens).length === 0 && <p>还没有令牌。</p>}
|
|
{user && Object.keys(user.tokens).length > 0 && (
|
|
<ul class="sb-token-list">
|
|
{Object.entries(user.tokens).map(([name, token]) => (
|
|
<li key={name}>
|
|
<strong>{name}</strong>
|
|
<span>创建于 {new Date(token.createdAt).toLocaleString()}</span>
|
|
<Button
|
|
onClick={() => {
|
|
if (!confirm(`确定撤销令牌“${name}”吗?`)) return;
|
|
void run(async () => {
|
|
await deleteOwnToken(name);
|
|
await reload();
|
|
});
|
|
}}
|
|
>
|
|
撤销
|
|
</Button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
<div class="row">
|
|
<Input
|
|
aria-label="令牌名称"
|
|
placeholder="令牌名称,例如:自动备份"
|
|
value={tokenName}
|
|
onInput={(event) => setTokenName(event.currentTarget.value)}
|
|
/>
|
|
<Button
|
|
variant="primary"
|
|
disabled={!tokenName.trim()}
|
|
onClick={() =>
|
|
void run(async () => {
|
|
setShownToken(await createOwnToken(tokenName.trim()));
|
|
setTokenName("");
|
|
await reload();
|
|
})
|
|
}
|
|
>
|
|
创建令牌
|
|
</Button>
|
|
</div>
|
|
{shownToken && (
|
|
<div class="sb-token-reveal">
|
|
<Alert variant="warning">此令牌只显示一次,请立即妥善保存。</Alert>
|
|
<Input
|
|
readOnly
|
|
value={shownToken}
|
|
onClick={(event) => event.currentTarget.select()}
|
|
/>
|
|
<Button onClick={() => setShownToken(undefined)}>我已保存</Button>
|
|
</div>
|
|
)}
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|