Eric Bower
·
2025-05-05
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 cursor := int(m.list.Cursor())
75 if cursor >= len(m.keys) {
76 return nil, nil
77 }
78 m.confirm = false
79 err := m.shared.Dbpool.RemoveKeys([]string{m.keys[m.list.Cursor()].ID})
80 if err != nil {
81 m.err = err
82 return nil, nil
83 }
84 m.err = m.fetchKeys()
85 return vxfw.RedrawCmd{}, nil
86 }
87 }
88 if msg.Matches('n') {
89 m.confirm = false
90 return vxfw.RedrawCmd{}, nil
91 }
92 }
93
94 return nil, nil
95}
96
97func (m *PubkeysPage) getWidget(i uint, cursor uint) vxfw.Widget {
98 if int(i) >= len(m.keys) {
99 return nil
100 }
101
102 style := vaxis.Style{Foreground: grey}
103 isSelected := i == cursor
104 if isSelected {
105 style = vaxis.Style{Foreground: fuschia}
106 }
107
108 pubkey := m.keys[i]
109 key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pubkey.Key))
110 if err != nil {
111 m.shared.Logger.Error("parse pubkey", "err", err)
112 return nil
113 }
114
115 keyStr := pubkey.Key[0:25] + "..." + pubkey.Key[len(pubkey.Key)-15:]
116
117 txt := richtext.New([]vaxis.Segment{
118 {Text: "Name: ", Style: style},
119 {Text: pubkey.Name + "\n"},
120
121 {Text: "Key: ", Style: style},
122 {Text: keyStr + "\n"},
123
124 {Text: "Sha: ", Style: style},
125 {Text: ssh.FingerprintSHA256(key) + "\n"},
126
127 {Text: "Created: ", Style: style},
128 {Text: pubkey.CreatedAt.Format(time.DateOnly)},
129 })
130
131 return txt
132}
133
134func (m *PubkeysPage) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
135 w := ctx.Max.Width
136 h := ctx.Max.Height
137 root := vxfw.NewSurface(w, h, m)
138 ah := 0
139
140 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.")
141 brd := NewBorder(info)
142 brd.Label = "desc"
143 brdSurf, _ := brd.Draw(ctx)
144 root.AddChild(0, ah, brdSurf)
145 ah += int(brdSurf.Size.Height)
146
147 header := text.New(fmt.Sprintf("%d pubkeys\n\n", len(m.keys)))
148 headerSurf, _ := header.Draw(ctx)
149 root.AddChild(0, ah, headerSurf)
150 ah += int(headerSurf.Size.Height)
151
152 footerHeight := 3
153 listSurf, _ := m.list.Draw(createDrawCtx(ctx, ctx.Max.Height-uint16(ah)-uint16(footerHeight)))
154 root.AddChild(0, ah, listSurf)
155
156 segs := []vaxis.Segment{}
157 if m.confirm {
158 segs = append(segs, vaxis.Segment{
159 Text: "are you sure? y/n\n",
160 Style: vaxis.Style{Foreground: red},
161 })
162 }
163 if m.err != nil {
164 segs = append(segs, vaxis.Segment{
165 Text: m.err.Error() + "\n",
166 Style: vaxis.Style{Foreground: red},
167 })
168 }
169 segs = append(segs, vaxis.Segment{Text: "\n"})
170
171 footer := richtext.New(segs)
172 footerSurf, _ := footer.Draw(ctx)
173 root.AddChild(0, int(h)-footerHeight, footerSurf)
174
175 return root, nil
176}
177
178type AddKeyPage struct {
179 shared *SharedModel
180
181 err error
182 focus string
183 input *TextInput
184 btn *button.Button
185}
186
187func NewAddPubkeyPage(shrd *SharedModel) *AddKeyPage {
188 btn := button.New("ADD", func() (vxfw.Command, error) { return nil, nil })
189 btn.Style = button.StyleSet{
190 Default: vaxis.Style{Background: grey},
191 Focus: vaxis.Style{Background: oj, Foreground: black},
192 }
193 return &AddKeyPage{
194 shared: shrd,
195
196 input: NewTextInput("add pubkey"),
197 btn: btn,
198 }
199}
200
201func (m *AddKeyPage) Footer() []Shortcut {
202 return []Shortcut{
203 {Shortcut: "tab", Text: "focus"},
204 {Shortcut: "shift+click", Text: "select text"},
205 {Shortcut: "enter", Text: "add public key"},
206 }
207}
208
209func (m *AddKeyPage) CaptureEvent(ev vaxis.Event) (vxfw.Command, error) {
210 switch msg := ev.(type) {
211 case vaxis.Key:
212 if msg.Matches(vaxis.KeyEnter) {
213 err := m.addPubkey(m.input.GetValue())
214 m.err = err
215 if err == nil {
216 m.input.Reset()
217 m.shared.App.PostEvent(Navigate{To: "pubkeys"})
218 return nil, nil
219 }
220 return vxfw.RedrawCmd{}, nil
221 }
222 }
223 return nil, nil
224}
225
226func (m *AddKeyPage) HandleEvent(ev vaxis.Event, phase vxfw.EventPhase) (vxfw.Command, error) {
227 switch msg := ev.(type) {
228 case PageIn:
229 m.focus = "input"
230 m.input.Reset()
231 return m.input.FocusIn()
232 case vaxis.Key:
233 if msg.Matches(vaxis.KeyTab) {
234 if m.focus == "input" {
235 m.focus = "button"
236 cmd, _ := m.input.FocusOut()
237 return vxfw.BatchCmd([]vxfw.Command{
238 vxfw.FocusWidgetCmd(m.btn),
239 cmd,
240 }), nil
241 }
242 m.focus = "input"
243 return m.input.FocusIn()
244 }
245 }
246
247 return nil, nil
248}
249
250func (m *AddKeyPage) addPubkey(pubkey string) error {
251 pk, comment, _, _, err := ssh.ParseAuthorizedKey([]byte(pubkey))
252 if err != nil {
253 return err
254 }
255
256 key := utils.KeyForKeyText(pk)
257
258 return m.shared.Dbpool.InsertPublicKey(
259 m.shared.User.ID, key, comment, nil,
260 )
261}
262
263func (m *AddKeyPage) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
264 w := ctx.Max.Width
265 h := ctx.Max.Height
266 root := vxfw.NewSurface(w, h, m)
267 ah := 0
268
269 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.")
270 headerSurf, _ := header.Draw(ctx)
271 root.AddChild(0, ah, headerSurf)
272 ah += int(headerSurf.Size.Height) + 1
273
274 inputSurf, _ := m.input.Draw(ctx)
275 root.AddChild(0, ah, inputSurf)
276 ah += int(headerSurf.Size.Height) + 1
277
278 btnSurf, _ := m.btn.Draw(vxfw.DrawContext{
279 Characters: ctx.Characters,
280 Max: vxfw.Size{Width: 5, Height: 1},
281 })
282 root.AddChild(0, ah, btnSurf)
283
284 if m.err != nil {
285 e := richtext.New([]vaxis.Segment{
286 {
287 Text: m.err.Error(),
288 Style: vaxis.Style{Foreground: red},
289 },
290 })
291 errSurf, _ := e.Draw(createDrawCtx(ctx, 1))
292 root.AddChild(0, ah, errSurf)
293 }
294
295 return root, nil
296}