feat: first-run claim wizard (no default admin, claim code from the backend log); owner list shows account and passkey; container install first [CI]
ci/woodpecker/push/main-amd64-build Pipeline was successful
ci/woodpecker/push/main-amd64-build Pipeline was successful
This commit is contained in:
1 parent
2ae63653c4
commit
f22fdf8b68
31 files changed
+799
-49
No files matched your search
@@ -92,7 +92,7 @@ function AddAgentDialog({ onClose, onCreated }: { onClose: () => void; onCreated
|
||||
<input className="input" type="number" min={1} max={720} value={hours} onChange={(e) => setHours(Number(e.target.value))} />
|
||||
</Field>
|
||||
<div className="wide">
|
||||
<Field label="属主(passkey)" hint="必选(至少一把):这个节点的敏感操作(web ssh 等)和加密内容只认它们。只是选,不用触碰;装上后固定在节点本地,面板改不了。">
|
||||
<Field label="属主(passkey)" hint="必选(至少一把):这个节点绑定到哪几个账户的哪几把 Passkey 上,它的敏感操作(web ssh 等)和加密内容只认它们。只是选,不用触碰;装上后固定在节点本地,面板改不了。">
|
||||
<OwnerPicker value={controllers} onChange={setControllers} />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
@@ -21,21 +21,21 @@ export function JoinTokenCreated({ created, kind, onClose }: { created: CreatedJ
|
||||
<div className="stack">
|
||||
<div className="notice warn">命令里的代码只显示这一次,请现在就复制。命令里只有公开的值(面板地址、一次性码、信任根指纹和属主),没有任何私钥。</div>
|
||||
<Field
|
||||
label={`在要加入的机器上运行(${kind})`}
|
||||
label={`容器安装(推荐):在要加入的机器上运行(${kind})`}
|
||||
hint="需要 Docker 或 Podman。命令会先下载安装脚本并核对它的 SHA-256,不一致就不执行;节点名字默认取主机名;身份和配置存在命名卷里,重启、换新镜像重建容器都不用再给参数。名字想自己定,或者提示名字被占用,在命令里加上 MOSHIRO_NAME=名字(Linux 和 macOS 写在 bash 或 zsh 前面,Windows 先执行 $env:MOSHIRO_NAME='名字')。"
|
||||
>
|
||||
<CommandTabs
|
||||
storageKey="moshiro.install_tab"
|
||||
initial={guessPlatform()}
|
||||
tabs={[
|
||||
{ id: 'linux', label: 'Linux', command: commands.linux },
|
||||
{ id: 'macos', label: 'macOS', command: commands.macos },
|
||||
{ id: 'windows', label: 'Windows', command: commands.windows },
|
||||
{ id: 'linux', label: 'Linux · 容器', command: commands.linux },
|
||||
{ id: 'macos', label: 'macOS · 容器', command: commands.macos },
|
||||
{ id: 'windows', label: 'Windows · 容器', command: commands.windows },
|
||||
]}
|
||||
/>
|
||||
</Field>
|
||||
<p className="muted">
|
||||
{kind === 'Agent' ? (created.token.auto_approve ? '使用它注册的 Agent 会自动启用。' : '使用它注册的 Agent 需要你在列表里审批之后才会启用。') : ''}
|
||||
容器方式是首选:镜像按 CI 签名的清单固定到 digest,宿主机更新器能自己更新它,属主和信任根在注册时固定。{kind === 'Agent' ? (created.token.auto_approve ? '使用它注册的 Agent 会自动启用。' : '使用它注册的 Agent 需要你在列表里审批之后才会启用。') : ''}
|
||||
可用 {created.token.max_uses} 次,{fmtTime(created.token.expires_at)} 过期。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { api } from '@shared/api/client'
|
||||
import type { Page, PasskeyDto, TrustStateDto } from '@shared/api/types'
|
||||
import type { TrustStateDto } from '@shared/api/types'
|
||||
|
||||
/**
|
||||
* 选这个节点的属主(必选,至少一把):允许授权它 extension 会话(web ssh 等,以后)、解密它加密内容的 passkey。只是选,不需要触碰 Passkey:
|
||||
* 选这个节点的属主(必选,至少一把):节点绑定在哪几个账户的哪几把 passkey 上,它的 extension 会话(web ssh 等,以后)和加密内容只认它们。只是选,不需要触碰 Passkey:
|
||||
* 选定的凭证 id 会写进安装命令,节点装上时固定在本地,之后面板改不了。默认是操作员集合里的全部。
|
||||
* 还没有建立信任根时不显示选项,只提示去建立。
|
||||
*/
|
||||
export function OwnerPicker({ value, onChange }: { value: string[] | null; onChange: (next: string[]) => void }) {
|
||||
const [trust, setTrust] = useState<TrustStateDto>()
|
||||
const [own, setOwn] = useState<PasskeyDto[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true
|
||||
@@ -21,10 +20,6 @@ export function OwnerPicker({ value, onChange }: { value: string[] | null; onCha
|
||||
if (value === null) onChange(t.operators ?? [])
|
||||
})
|
||||
.catch(() => undefined)
|
||||
api
|
||||
.get<Page<PasskeyDto>>('/api/admin/auth/passkeys')
|
||||
.then((list) => alive && setOwn(list.items))
|
||||
.catch(() => undefined)
|
||||
return () => {
|
||||
alive = false
|
||||
}
|
||||
@@ -33,11 +28,20 @@ export function OwnerPicker({ value, onChange }: { value: string[] | null; onCha
|
||||
|
||||
if (!trust) return null
|
||||
if (!trust.initialized) {
|
||||
return <div className="notice warn">还没有建立信任根:每个节点都必须有属主(passkey),先到「系统 → 信任」用 Passkey 建立信任根,才能添加节点。</div>
|
||||
return (
|
||||
<div className="notice warn">
|
||||
还没有建立信任根,所以现在无法添加 Agent 和 Probe:每个节点都要绑定到属主的 Passkey 上,节点只信任由 Passkey 签名的信任根。没有信任根时只能进入面板做基础管理,这是架构如此。请先到「系统 → 信任」注册 Passkey 并建立信任根。
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const chosen = new Set(value ?? [])
|
||||
const name = (id: string) => own.find((p) => p.id === id)?.name ?? `其他账号的 Passkey(${id.slice(0, 8)}…)`
|
||||
const details = new Map((trust.operator_details ?? []).map((d) => [d.credential_id, d] as const))
|
||||
// 绑定到哪个账户的哪把 Passkey:「Passkey 名称 · 账户」;对不上任何已注册的 Passkey 时标明
|
||||
const name = (id: string) => {
|
||||
const d = details.get(id)
|
||||
return d?.passkey ? `${d.passkey}${d.account ? ` · ${d.account}` : ''}` : `未知 Passkey(${id.slice(0, 8)}…)`
|
||||
}
|
||||
return (
|
||||
<div className="stack" style={{ gap: 4 }}>
|
||||
{(trust.operators ?? []).map((id) => (
|
||||
|
||||
@@ -147,7 +147,7 @@ function AddProbeDialog({ onClose, onCreated }: { onClose: () => void; onCreated
|
||||
<input className="input" type="number" min={1} max={720} value={hours} onChange={(e) => setHours(Number(e.target.value))} />
|
||||
</Field>
|
||||
<div className="wide">
|
||||
<Field label="属主(passkey)" hint="必选(至少一把):这个节点的敏感操作(web ssh 等)和加密内容只认它们。只是选,不用触碰;装上后固定在节点本地,面板改不了。">
|
||||
<Field label="属主(passkey)" hint="必选(至少一把):这个节点绑定到哪几个账户的哪几把 Passkey 上,它的敏感操作(web ssh 等)和加密内容只认它们。只是选,不用触碰;装上后固定在节点本地,面板改不了。">
|
||||
<OwnerPicker value={controllers} onChange={setControllers} />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ShieldCheck } from 'lucide-react'
|
||||
import { useEffect } from 'react'
|
||||
import { api } from '@shared/api/client'
|
||||
import type { TrustStateDto } from '@shared/api/types'
|
||||
import { signStatement } from '@shared/api/webauthn'
|
||||
@@ -49,11 +50,15 @@ function Changes({ proposal }: { proposal: Proposal }) {
|
||||
* 信任根:你的 Passkey 是整个系统的根。节点安装时把下面这个指纹和属主(passkey)固定在本地,之后只认从它出发、由你的 Passkey 签过名的
|
||||
* 操作员集合,面板自己改不了,也签不出新的版本。
|
||||
*/
|
||||
export function Trust() {
|
||||
export function Trust({ onInitialized }: { onInitialized?: () => void } = {}) {
|
||||
const trust = useQuery(() => api.get<TrustStateDto>('/api/admin/trust'), [])
|
||||
const { busy, run } = useAction()
|
||||
const toast = useToast()
|
||||
const state = trust.data
|
||||
useEffect(() => {
|
||||
if (state?.initialized) onInitialized?.()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [state?.initialized])
|
||||
|
||||
const sign = async () => {
|
||||
if (!state?.proposal) return
|
||||
@@ -80,7 +85,7 @@ export function Trust() {
|
||||
<b>还没有建立信任根</b>
|
||||
</div>
|
||||
<p className="muted">
|
||||
建立之后,新装的节点会把你的信任根指纹和属主固定在本地:以后 web ssh 等敏感操作只认你的 Passkey,Backend 被攻破也改不了。添加节点本身不需要触碰 Passkey。
|
||||
<b>不建立信任根就无法添加 Agent 和 Probe,只能进入面板做基础管理——这是架构如此:节点只信任由你的 Passkey 签名的信任根,面板自己没法代签。</b>建立之后,新装的节点会把你的信任根指纹和属主固定在本地:以后 web ssh 等敏感操作只认你的 Passkey,Backend 被攻破也改不了。添加节点本身不需要触碰 Passkey。
|
||||
这一步会把下面列出的 Passkey 登记为操作员,需要你触碰一次 Passkey 给集合签名。
|
||||
</p>
|
||||
{state.proposal ? (
|
||||
|
||||
Reference in new issue
Block a user