fix(gitea): 建仓需 write:user scope——实例支持改 Token + 权限提示补全

- Gitea 的 POST /user/repos 归 write:user scope(403 实测,R10:以实际
  行为为准),此前权限指导只给了 read:user 导致新建仓库失败
- GiteaInstancesSection 新增「改 Token」内联编辑(此前无入口,删除实例
  重建会级联丢失全部仓库绑定)
- 添加实例的 placeholder 补权限要求说明

Rules-Applied: R08, R10
This commit is contained in:
lanrtop 2026-07-03 09:38:31 +09:00
parent 1d068d188c
commit b7008a132e

View File

@ -8,6 +8,7 @@ import {
giteaSaveInstance,
giteaListInstances,
giteaDeleteInstance,
giteaUpdateInstance,
giteaTestConnection,
giteaGetRepoLink,
giteaListLinkedRepos,
@ -46,6 +47,21 @@ export function GiteaInstancesSection() {
const [instUrl, setInstUrl] = useState("");
const [instToken, setInstToken] = useState("");
const [testing, setTesting] = useState<string | null>(null);
const [editingTokenId, setEditingTokenId] = useState<string | null>(null);
const [newToken, setNewToken] = useState("");
const handleUpdateToken = async (id: string) => {
if (!newToken.trim()) return;
try {
await giteaUpdateInstance(id, { access_token: newToken.trim() });
qc.invalidateQueries({ queryKey: ["gitea-instances"] });
setEditingTokenId(null);
setNewToken("");
showToast("Token 已更新 ✓ 可点「测试连接」验证");
} catch (e) {
showToast(`${e}`);
}
};
const handleAdd = async () => {
if (!instName.trim() || !instUrl.trim() || !instToken.trim()) return;
@ -105,28 +121,55 @@ export function GiteaInstancesSection() {
<p className="text-xs text-gray-400"> Gitea </p>
)}
{instances.map((inst) => (
<div key={inst.id} className="flex items-center gap-2 text-xs">
<span className="font-medium text-gray-700 shrink-0">{inst.name}</span>
{inst.is_default && (
<span className="text-[10px] px-1 rounded bg-blue-50 text-blue-500 shrink-0"></span>
<div key={inst.id} className="space-y-1.5">
<div className="flex items-center gap-2 text-xs">
<span className="font-medium text-gray-700 shrink-0">{inst.name}</span>
{inst.is_default && (
<span className="text-[10px] px-1 rounded bg-blue-50 text-blue-500 shrink-0"></span>
)}
<span className="text-gray-400 font-mono truncate flex-1">{inst.url}</span>
<button onClick={() => handleTest(inst)} disabled={testing === inst.id} className={btnCls}>
{testing === inst.id ? "…" : "测试连接"}
</button>
<button
onClick={() => { setEditingTokenId(editingTokenId === inst.id ? null : inst.id); setNewToken(""); }}
className={btnCls}
>
Token
</button>
<button
onClick={() => handleDelete(inst)}
className="px-2 py-1 rounded text-[11px] border border-gray-200 text-red-400 hover:bg-red-50"
>
</button>
</div>
{editingTokenId === inst.id && (
<div className="flex gap-1.5 pl-2">
<input
className={inputCls}
type="password"
placeholder="新 Access Token需 repository 读写 + user 读写权限)"
value={newToken}
onChange={(e) => setNewToken(e.target.value)}
autoFocus
/>
<button
onClick={() => handleUpdateToken(inst.id)}
disabled={!newToken.trim()}
className="px-3 py-1.5 rounded text-xs bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50 shrink-0"
>
</button>
</div>
)}
<span className="text-gray-400 font-mono truncate flex-1">{inst.url}</span>
<button onClick={() => handleTest(inst)} disabled={testing === inst.id} className={btnCls}>
{testing === inst.id ? "…" : "测试连接"}
</button>
<button
onClick={() => handleDelete(inst)}
className="px-2 py-1 rounded text-[11px] border border-gray-200 text-red-400 hover:bg-red-50"
>
</button>
</div>
))}
{showAdd && (
<div className="space-y-2 border-t border-gray-100 pt-3">
<input className={inputCls} placeholder="实例名称(如:家里 NAS" value={instName} onChange={(e) => setInstName(e.target.value)} />
<input className={inputCls} placeholder="https://gitea.example.com" value={instUrl} onChange={(e) => setInstUrl(e.target.value)} />
<input className={inputCls} type="password" placeholder="Access Token用户设置 → 应用 → 生成令牌)" value={instToken} onChange={(e) => setInstToken(e.target.value)} />
<input className={inputCls} type="password" placeholder="Access Token用户设置 → 应用 → 生成令牌权限repository 读写 + user 读写)" value={instToken} onChange={(e) => setInstToken(e.target.value)} />
<div className="flex gap-1.5">
<button onClick={handleAdd} className="px-3 py-1.5 rounded text-xs bg-blue-600 text-white hover:bg-blue-700"></button>
<button onClick={() => setShowAdd(false)} className="px-3 py-1.5 rounded text-xs border border-gray-200 text-gray-500 hover:bg-gray-50"></button>