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

This commit is contained in:
justme committed 2026-10-08 06:47:10 +08:00
1 parent 2ae63653c4
commit f22fdf8b68
31 files changed
+799 -49

No files matched your search

@@ -15,7 +15,7 @@ public sealed record ProposedCredential(string Id, string Name, long CreatedAtMs
/// <summary><c>InSync</c>:当前生效的集合和现有的 passkey、域名是否一致;不一致就需要签一个新版本。</summary>
/// <param name="ServerKey">这个面板的服务器身份密钥的指纹;<c>ServerKeyListed</c>:当前生效的操作员集合里已经登记了它(节点据此认证服务器、加密链路)。</param>
public sealed record TrustState(bool Initialized, string? Genesis, int Version, bool InSync, IReadOnlyList<SignedStatementDto> Chain, TrustProposal? Proposal, IReadOnlyList<string>? Operators = null, string ServerKey = "", bool ServerKeyListed = false);
public sealed record TrustState(bool Initialized, string? Genesis, int Version, bool InSync, IReadOnlyList<SignedStatementDto> Chain, TrustProposal? Proposal, IReadOnlyList<string>? Operators = null, string ServerKey = "", bool ServerKeyListed = false, IReadOnlyList<NodeOwner>? OperatorDetails = null);
/// <summary>
/// 信任根的 Backend 一侧:保存操作员集合的链、给操作员出下一版的语句、验证他签回来的版本。
@@ -56,7 +56,16 @@ public sealed class TrustService : IOperatorTrustSource, IDisposable
TrustProposal? proposal = await ProposeAsync(chain, current, context, ct);
OperatorSet? proposed = proposal is null ? null : OperatorTrust.ParseSet(proposal.Statement);
bool inSync = current is not null && proposed is not null && Same(current, proposed);
return new TrustState(current is not null, chain.Count == 0 ? null : OperatorTrust.Fingerprint(chain[0].Statement), current?.Version ?? 0, inSync, chain, inSync ? null : proposal, current is null ? [] : [.. current.Keys.Select(k => k.CredentialId)], _server.Fingerprint, current?.Servers?.Any(k => k.AsSpan().SequenceEqual(_server.PublicKeySpki)) == true);
// 每个操作员是哪个账户的哪把 passkey:添加节点时选属主、信任页都要显示
var details = new List<NodeOwner>();
foreach (OperatorKey key in current?.Keys ?? [])
{
PasskeyRecord? passkey = await _auth.FindPasskeyAsync(key.CredentialId, ct);
string? account = passkey is null ? null : (await _auth.GetUserAsync(passkey.UserId, ct))?.Username;
details.Add(new NodeOwner(key.CredentialId, passkey?.Name, account));
}
return new TrustState(current is not null, chain.Count == 0 ? null : OperatorTrust.Fingerprint(chain[0].Statement), current?.Version ?? 0, inSync, chain, inSync ? null : proposal, current is null ? [] : [.. current.Keys.Select(k => k.CredentialId)], _server.Fingerprint, current?.Servers?.Any(k => k.AsSpan().SequenceEqual(_server.PublicKeySpki)) == true, details);
}
private async ValueTask<TrustProposal?> ProposeAsync(IReadOnlyList<SignedStatementDto> chain, OperatorSet? current, PasskeyContext context, CancellationToken ct)