Antonio Mika
·
2025-04-02
pubkeys.go
1package tui
2
3import (
4 "fmt"
5 "time"
6
7 "git.sr.ht/~rockorager/vaxis"
8 "git.sr.ht/~rockorager/vaxis/vxfw"
9 "git.sr.ht/~rockorager/vaxis/vxfw/button"
10 "git.sr.ht/~rockorager/vaxis/vxfw/list"
11 "git.sr.ht/~rockorager/vaxis/vxfw/richtext"
12 "git.sr.ht/~rockorager/vaxis/vxfw/text"
13 "github.com/picosh/pico/pkg/db"
14 "github.com/picosh/utils"
15 "golang.org/x/crypto/ssh"
16)
17
18type PubkeysPage struct {
19 shared *SharedModel
20 list list.Dynamic
21
22 keys []*db.PublicKey
23 err error
24 confirm bool
25}
26
27func NewPubkeysPage(shrd *SharedModel) *PubkeysPage {
28 m := &PubkeysPage{
29 shared: shrd,
30 }
31 m.list = list.Dynamic{DrawCursor: true, Builder: m.getWidget, Gap: 1}
32 return m
33}
34
35type FetchPubkeys struct{}
36
37func (m *PubkeysPage) Footer() []Shortcut {
38 return []Shortcut{
39 {Shortcut: "j/k", Text: "choose"},
40 {Shortcut: "x", Text: "delete"},
41 {Shortcut: "c", Text: "create"},
42 }
43}
44
45func (m *PubkeysPage) fetchKeys() error {
46 keys, err := m.shared.Dbpool.FindKeysForUser(m.shared.User)
47 if err != nil {
48 return err
49
50 }
51 m.keys = keys
52 return nil
53}
54
55func (m *PubkeysPage) HandleEvent(ev vaxis.Event, phase vxfw.EventPhase) (vxfw.Command, error) {
56 switch msg := ev.(type) {
57 case PageIn:
58 m.err = m.fetchKeys()
59 return vxfw.FocusWidgetCmd(&m.list), nil
60 case vaxis.Key:
61 if msg.Matches('c') {
62 m.shared.App.PostEvent(Navigate{To: "add-pubkey"})
63 }
64 if msg.Matches('x') {
65 if len(m.keys) < 2 {
66 m.err = fmt.Errorf("cannot delete last key")
67 } else {
68 m.confirm = true
69 }
70 return vxfw.RedrawCmd{}, nil
71 }
72 if msg.Matches('y') {
73 if m.confirm {
74 m.confirm = false
75 err := m.shared.Dbpool.RemoveKeys([]string{m.keys[m.list.Cursor()].ID})
76 if err != nil {
77 m.err = err
78 return nil, nil
79 }
80 m.err = m.fetchKeys()
81 return vxfw.RedrawCmd{}, nil
82 }
83 }
84 if msg.Matches('n') {
85 m.confirm = false
86 return vxfw.RedrawCmd{}, nil
87 }
88 }
89
90 return nil, nil
91}
92
93func (m *PubkeysPage) getWidget(i uint, cursor uint) vxfw.Widget {
94 if int(i) >= len(m.keys) {
95 return nil
96 }
97
98 style := vaxis.Style{Foreground: grey}
99 isSelected := i == cursor
100 if isSelected {
101 style = vaxis.Style{Foreground: fuschia}
102 }
103
104 pubkey := m.keys[i]
105 key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pubkey.Key))
106 if err != nil {
107 m.shared.Logger.Error("parse pubkey", "err", err)
108 return nil
109 }
110
111 txt := richtext.New([]vaxis.Segment{
112 {Text: "Name: ", Style: style},
113 {Text: pubkey.Name + "\n"},
114
115 {Text: "Key: ", Style: style},
116 {Text: ssh.FingerprintSHA256(key) + "\n"},
117
118 {Text: "Created: ", Style: style},
119 {Text: pubkey.CreatedAt.Format(time.DateOnly)},
120 })
121
122 return txt
123}
124
125func (m *PubkeysPage) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
126 w := ctx.Max.Width
127 h := ctx.Max.Height
128 root := vxfw.NewSurface(w, h, m)
129 ah := 0
130
131 info := text.New("Pubkeys are SSH public keys which grant access to your pico account. You can have many pubkeys associated with your account and they all have the same level of access. You cannot delete all pubkeys since it will revoke all access to the account.")
132 brd := NewBorder(info)
133 brd.Label = "desc"
134 brdSurf, _ := brd.Draw(ctx)
135 root.AddChild(0, ah, brdSurf)
136 ah += int(brdSurf.Size.Height)
137
138 header := text.New(fmt.Sprintf("%d pubkeys\n\n", len(m.keys)))
139 headerSurf, _ := header.Draw(ctx)
140 root.AddChild(0, ah, headerSurf)
141 ah += int(headerSurf.Size.Height)
142
143 listSurf, _ := m.list.Draw(ctx)
144 root.AddChild(0, ah, listSurf)
145
146 segs := []vaxis.Segment{}
147 if m.confirm {
148 segs = append(segs, vaxis.Segment{
149 Text: "are you sure? y/n\n",
150 Style: vaxis.Style{Foreground: red},
151 })
152 }
153 if m.err != nil {
154 segs = append(segs, vaxis.Segment{
155 Text: m.err.Error() + "\n",
156 Style: vaxis.Style{Foreground: red},
157 })
158 }
159 segs = append(segs, vaxis.Segment{Text: "\n"})
160
161 footer := richtext.New(segs)
162 footerSurf, _ := footer.Draw(ctx)
163 root.AddChild(0, int(h)-3, footerSurf)
164
165 return root, nil
166}
167
168type AddKeyPage struct {
169 shared *SharedModel
170
171 err error
172 focus string
173 input *TextInput
174 btn *button.Button
175}
176
177func NewAddPubkeyPage(shrd *SharedModel) *AddKeyPage {
178 btn := button.New("ADD", func() (vxfw.Command, error) { return nil, nil })
179 btn.Style = button.StyleSet{
180 Default: vaxis.Style{Background: grey},
181 Focus: vaxis.Style{Background: oj, Foreground: black},
182 }
183 return &AddKeyPage{
184 shared: shrd,
185
186 input: NewTextInput("add pubkey"),
187 btn: btn,
188 }
189}
190
191func (m *AddKeyPage) Footer() []Shortcut {
192 return []Shortcut{
193 {Shortcut: "tab", Text: "focus"},
194 {Shortcut: "shift+click", Text: "select text"},
195 {Shortcut: "enter", Text: "add public key"},
196 }
197}
198
199func (m *AddKeyPage) CaptureEvent(ev vaxis.Event) (vxfw.Command, error) {
200 switch msg := ev.(type) {
201 case vaxis.Key:
202 if msg.Matches(vaxis.KeyEnter) {
203 err := m.addPubkey(m.input.GetValue())
204 m.err = err
205 if err == nil {
206 m.input.Reset()
207 m.shared.App.PostEvent(Navigate{To: "pubkeys"})
208 return nil, nil
209 }
210 return vxfw.RedrawCmd{}, nil
211 }
212 }
213 return nil, nil
214}
215
216func (m *AddKeyPage) HandleEvent(ev vaxis.Event, phase vxfw.EventPhase) (vxfw.Command, error) {
217 switch msg := ev.(type) {
218 case PageIn:
219 m.focus = "input"
220 m.input.Reset()
221 return m.input.FocusIn()
222 case vaxis.Key:
223 if msg.Matches(vaxis.KeyTab) {
224 if m.focus == "input" {
225 m.focus = "button"
226 cmd, _ := m.input.FocusOut()
227 return vxfw.BatchCmd([]vxfw.Command{
228 vxfw.FocusWidgetCmd(m.btn),
229 cmd,
230 }), nil
231 }
232 m.focus = "input"
233 return m.input.FocusIn()
234 }
235 }
236
237 return nil, nil
238}
239
240func (m *AddKeyPage) addPubkey(pubkey string) error {
241 pk, comment, _, _, err := ssh.ParseAuthorizedKey([]byte(pubkey))
242 if err != nil {
243 return err
244 }
245
246 key := utils.KeyForKeyText(pk)
247
248 return m.shared.Dbpool.InsertPublicKey(
249 m.shared.User.ID, key, comment, nil,
250 )
251}
252
253func (m *AddKeyPage) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
254 w := ctx.Max.Width
255 h := ctx.Max.Height
256 root := vxfw.NewSurface(w, h, m)
257 ah := 0
258
259 header := text.New("Enter a new public key. You can typically grab an SSH pubkey in the `.ssh` folder: cat ~/.ssh/id_ed25519.pub. You can include the comment as well.")
260 headerSurf, _ := header.Draw(ctx)
261 root.AddChild(0, ah, headerSurf)
262 ah += int(headerSurf.Size.Height) + 1
263
264 inputSurf, _ := m.input.Draw(ctx)
265 root.AddChild(0, ah, inputSurf)
266 ah += int(headerSurf.Size.Height) + 1
267
268 btnSurf, _ := m.btn.Draw(vxfw.DrawContext{
269 Characters: ctx.Characters,
270 Max: vxfw.Size{Width: 5, Height: 1},
271 })
272 root.AddChild(0, ah, btnSurf)
273
274 if m.err != nil {
275 e := richtext.New([]vaxis.Segment{
276 {
277 Text: m.err.Error(),
278 Style: vaxis.Style{Foreground: red},
279 },
280 })
281 errSurf, _ := e.Draw(createDrawCtx(ctx, 1))
282 root.AddChild(0, ah, errSurf)
283 }
284
285 return root, nil
286}