Files
unibus_admin/internal/admin/repo_mock.go
T
Egutierrez 8d893d216b feat: scaffold unibus_admin gateway (Go REST + embed SPA placeholder)
Single Go binary: serves an embedded Mantine SPA and a small REST API over the
unibus control plane. Holds the operator ADMIN identity, signs every
control-plane request, never exposes a private key to the browser.

- internal/admin: Repo interface + mock + bus implementations, REST server
- repo_bus: rooms via pkg/client, members via signed GET (CanonicalRequest +
  SignEd25519), cluster via /healthz (CA-pinned), users via membership.Store
- identity loaded from pass entry or 0600 file (operator-identity JSON)
- go build CGO_ENABLED=0 green; go vet clean

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 19:27:49 +02:00

158 lines
5.0 KiB
Go

package admin
import (
"context"
"fmt"
"sync"
)
// mockRepo serves sample data so the SPA can be iterated and demoed without a
// live bus. It is selected with --mock. All mutations are kept in memory so the
// UI feels real during a session (create a room, see it appear) without touching
// any control plane.
type mockRepo struct {
mu sync.Mutex
rooms []RoomView
users []UserView
mem map[string][]MemberView
}
// NewMockRepo returns a Repo backed by in-memory sample data (--mock).
func NewMockRepo() Repo { return newMockRepo() }
func newMockRepo() *mockRepo {
return &mockRepo{
rooms: []RoomView{
{RoomID: "01HV...GENERAL", Subject: "team.general", Epoch: 1, Encrypt: true, Persist: true, SignMsgs: true, Role: "owner"},
{RoomID: "01HV...BOARD", Subject: "board.private", Epoch: 3, Encrypt: true, Persist: true, SignMsgs: true, Role: "owner"},
{RoomID: "01HV...BOTS", Subject: "bots.echo", Epoch: 1, Encrypt: false, Persist: false, SignMsgs: false, Role: "member"},
{RoomID: "01HV...INFRA", Subject: "infra.alerts", Epoch: 2, Encrypt: true, Persist: true, SignMsgs: true, Role: "owner"},
},
users: []UserView{
{SignPub: "48bc0dc829571a1332b4b2deb0bd78326a06bcf149e7d560728d8dc0b98173fa", Handle: "operator", Role: "admin", Status: "active", CreatedAt: "2026-06-01T10:00:00Z"},
{SignPub: "a1b2c3d4e5f60718293a4b5c6d7e8f90112233445566778899aabbccddeeff00", Handle: "ana", Role: "member", Status: "active", CreatedAt: "2026-06-02T11:30:00Z"},
{SignPub: "ffeeddccbbaa99887766554433221100f0e1d2c3b4a5968778695a4b3c2d1e0f", Handle: "lucas", Role: "member", Status: "active", CreatedAt: "2026-06-03T09:15:00Z"},
{SignPub: "0011223344556677889900aabbccddeeff112233445566778899aabbccddeeff", Handle: "leo-revoked", Role: "member", Status: "revoked", CreatedAt: "2026-05-20T08:00:00Z", RevokedAt: "2026-06-04T14:00:00Z"},
},
mem: map[string][]MemberView{
"01HV...GENERAL": {
{Endpoint: "ep-operator", Role: "owner", SignPub: "48bc0dc8...", KexPub: "9f3a..."},
{Endpoint: "ep-ana", Role: "member", SignPub: "a1b2c3d4...", KexPub: "7c2b..."},
{Endpoint: "ep-lucas", Role: "member", SignPub: "ffeeddcc...", KexPub: "5e1d..."},
},
},
}
}
func (m *mockRepo) Me(context.Context) MeInfo {
return MeInfo{
Endpoint: "ep-operator",
SignPub: "48bc0dc829571a1332b4b2deb0bd78326a06bcf149e7d560728d8dc0b98173fa",
UsersBackend: "sqlite",
Mock: true,
}
}
func (m *mockRepo) Cluster(context.Context) []NodeHealth {
p := Posture{Enforce: true, ACL: true, TLS: true, Cluster: true, Store: "kv"}
return []NodeHealth{
{Name: "magnus", URL: "https://127.0.0.1:8470", Up: true, Posture: p, LatencyMs: 4},
{Name: "homer", URL: "https://10.0.0.2:8470", Up: true, Posture: p, LatencyMs: 11},
{Name: "datardos", URL: "https://10.0.0.3:8470", Up: false, Posture: Posture{}, LatencyMs: 0, Error: "dial tcp: i/o timeout"},
}
}
func (m *mockRepo) ListRooms(context.Context) ([]RoomView, error) {
m.mu.Lock()
defer m.mu.Unlock()
out := make([]RoomView, len(m.rooms))
copy(out, m.rooms)
return out, nil
}
func (m *mockRepo) CreateRoom(_ context.Context, req CreateRoomReq) (RoomView, error) {
m.mu.Lock()
defer m.mu.Unlock()
rv := RoomView{
RoomID: fmt.Sprintf("01HV...NEW%d", len(m.rooms)),
Subject: req.Subject,
Epoch: 1,
Encrypt: req.Encrypt,
Persist: req.Persist,
SignMsgs: req.SignMsgs,
Role: "owner",
}
m.rooms = append(m.rooms, rv)
return rv, nil
}
func (m *mockRepo) ListMembers(_ context.Context, roomID string) ([]MemberView, error) {
m.mu.Lock()
defer m.mu.Unlock()
if ms, ok := m.mem[roomID]; ok {
return ms, nil
}
return []MemberView{{Endpoint: "ep-operator", Role: "owner", SignPub: "48bc0dc8...", KexPub: "9f3a..."}}, nil
}
func (m *mockRepo) Invite(context.Context, string, InviteReq) error { return nil }
func (m *mockRepo) KickMember(_ context.Context, roomID, endpoint string) error {
m.mu.Lock()
defer m.mu.Unlock()
if ms, ok := m.mem[roomID]; ok {
kept := ms[:0]
for _, mv := range ms {
if mv.Endpoint != endpoint {
kept = append(kept, mv)
}
}
m.mem[roomID] = kept
}
for i := range m.rooms {
if m.rooms[i].RoomID == roomID {
m.rooms[i].Epoch++
}
}
return nil
}
func (m *mockRepo) UsersWritable() bool { return true }
func (m *mockRepo) ListUsers(context.Context) ([]UserView, error) {
m.mu.Lock()
defer m.mu.Unlock()
out := make([]UserView, len(m.users))
copy(out, m.users)
return out, nil
}
func (m *mockRepo) AddUser(_ context.Context, req AddUserReq) error {
m.mu.Lock()
defer m.mu.Unlock()
role := req.Role
if role == "" {
role = "member"
}
m.users = append(m.users, UserView{
SignPub: req.SignPub,
Handle: req.Handle,
Role: role,
Status: "active",
CreatedAt: "2026-06-07T12:00:00Z",
})
return nil
}
func (m *mockRepo) RevokeUser(_ context.Context, signPub string) error {
m.mu.Lock()
defer m.mu.Unlock()
for i := range m.users {
if m.users[i].SignPub == signPub {
m.users[i].Status = "revoked"
m.users[i].RevokedAt = "2026-06-07T12:30:00Z"
}
}
return nil
}