1package backend
2
3import (
4 "context"
5 "errors"
6 "strings"
7 "time"
8
9 "github.com/charmbracelet/soft-serve/pkg/db"
10 "github.com/charmbracelet/soft-serve/pkg/db/models"
11 "github.com/charmbracelet/soft-serve/pkg/proto"
12 "github.com/charmbracelet/soft-serve/pkg/sshutils"
13 "github.com/charmbracelet/soft-serve/pkg/utils"
14 "golang.org/x/crypto/ssh"
15)
16
17// User finds a user by username.
18//
19// It implements backend.Backend.
20func (d *Backend) User(ctx context.Context, username string) (proto.User, error) {
21 username = strings.ToLower(username)
22 if err := utils.ValidateHandle(username); err != nil {
23 return nil, err
24 }
25
26 var m models.User
27 var pks []ssh.PublicKey
28 var hl models.Handle
29 var ems []proto.UserEmail
30 if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
31 var err error
32 m, err = d.store.FindUserByUsername(ctx, tx, username)
33 if err != nil {
34 return err
35 }
36
37 pks, err = d.store.ListPublicKeysByUserID(ctx, tx, m.ID)
38 if err != nil {
39 return err
40 }
41
42 emails, err := d.store.ListUserEmails(ctx, tx, m.ID)
43 if err != nil {
44 return err
45 }
46
47 for _, e := range emails {
48 ems = append(ems, &userEmail{e})
49 }
50
51 hl, err = d.store.GetHandleByUserID(ctx, tx, m.ID)
52 return err
53 }); err != nil {
54 err = db.WrapError(err)
55 if errors.Is(err, db.ErrRecordNotFound) {
56 return nil, proto.ErrUserNotFound
57 }
58 d.logger.Error("error finding user", "username", username, "error", err)
59 return nil, err
60 }
61
62 return &user{
63 user: m,
64 publicKeys: pks,
65 handle: hl,
66 emails: ems,
67 }, nil
68}
69
70// UserByID finds a user by ID.
71func (d *Backend) UserByID(ctx context.Context, id int64) (proto.User, error) {
72 var m models.User
73 var pks []ssh.PublicKey
74 var hl models.Handle
75 var ems []proto.UserEmail
76 if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
77 var err error
78 m, err = d.store.GetUserByID(ctx, tx, id)
79 if err != nil {
80 return err
81 }
82
83 pks, err = d.store.ListPublicKeysByUserID(ctx, tx, m.ID)
84 if err != nil {
85 return err
86 }
87
88 emails, err := d.store.ListUserEmails(ctx, tx, m.ID)
89 if err != nil {
90 return err
91 }
92
93 for _, e := range emails {
94 ems = append(ems, &userEmail{e})
95 }
96
97 hl, err = d.store.GetHandleByUserID(ctx, tx, m.ID)
98 return err
99 }); err != nil {
100 err = db.WrapError(err)
101 if errors.Is(err, db.ErrRecordNotFound) {
102 return nil, proto.ErrUserNotFound
103 }
104 d.logger.Error("error finding user", "id", id, "error", err)
105 return nil, err
106 }
107
108 return &user{
109 user: m,
110 publicKeys: pks,
111 handle: hl,
112 emails: ems,
113 }, nil
114}
115
116// UserByPublicKey finds a user by public key.
117//
118// It implements backend.Backend.
119func (d *Backend) UserByPublicKey(ctx context.Context, pk ssh.PublicKey) (proto.User, error) {
120 var m models.User
121 var pks []ssh.PublicKey
122 var hl models.Handle
123 var ems []proto.UserEmail
124 if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
125 var err error
126 m, err = d.store.FindUserByPublicKey(ctx, tx, pk)
127 if err != nil {
128 return db.WrapError(err)
129 }
130
131 pks, err = d.store.ListPublicKeysByUserID(ctx, tx, m.ID)
132 if err != nil {
133 return err
134 }
135
136 emails, err := d.store.ListUserEmails(ctx, tx, m.ID)
137 if err != nil {
138 return err
139 }
140
141 for _, e := range emails {
142 ems = append(ems, &userEmail{e})
143 }
144
145 hl, err = d.store.GetHandleByUserID(ctx, tx, m.ID)
146 return err
147 }); err != nil {
148 err = db.WrapError(err)
149 if errors.Is(err, db.ErrRecordNotFound) {
150 return nil, proto.ErrUserNotFound
151 }
152 d.logger.Error("error finding user", "pk", sshutils.MarshalAuthorizedKey(pk), "error", err)
153 return nil, err
154 }
155
156 return &user{
157 user: m,
158 publicKeys: pks,
159 handle: hl,
160 emails: ems,
161 }, nil
162}
163
164// UserByAccessToken finds a user by access token.
165// This also validates the token for expiration and returns proto.ErrTokenExpired.
166func (d *Backend) UserByAccessToken(ctx context.Context, token string) (proto.User, error) {
167 var m models.User
168 var pks []ssh.PublicKey
169 var hl models.Handle
170 var ems []proto.UserEmail
171 token = HashToken(token)
172
173 if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
174 t, err := d.store.GetAccessTokenByToken(ctx, tx, token)
175 if err != nil {
176 return db.WrapError(err)
177 }
178
179 if t.ExpiresAt.Valid && t.ExpiresAt.Time.Before(time.Now()) {
180 return proto.ErrTokenExpired
181 }
182
183 m, err = d.store.FindUserByAccessToken(ctx, tx, token)
184 if err != nil {
185 return db.WrapError(err)
186 }
187
188 pks, err = d.store.ListPublicKeysByUserID(ctx, tx, m.ID)
189 if err != nil {
190 return err
191 }
192
193 emails, err := d.store.ListUserEmails(ctx, tx, m.ID)
194 if err != nil {
195 return err
196 }
197
198 for _, e := range emails {
199 ems = append(ems, &userEmail{e})
200 }
201
202 hl, err = d.store.GetHandleByUserID(ctx, tx, m.ID)
203 return err
204 }); err != nil {
205 err = db.WrapError(err)
206 if errors.Is(err, db.ErrRecordNotFound) {
207 return nil, proto.ErrUserNotFound
208 }
209 d.logger.Error("failed to find user by access token", "err", err, "token", token)
210 return nil, err
211 }
212
213 return &user{
214 user: m,
215 publicKeys: pks,
216 handle: hl,
217 emails: ems,
218 }, nil
219}
220
221// Users returns all users.
222//
223// It implements backend.Backend.
224func (d *Backend) Users(ctx context.Context) ([]string, error) {
225 var users []string
226 if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
227 ms, err := d.store.GetAllUsers(ctx, tx)
228 if err != nil {
229 return err
230 }
231
232 ids := make([]int64, len(ms))
233 for i, m := range ms {
234 ids[i] = m.ID
235 }
236
237 handles, err := d.store.ListHandlesForIDs(ctx, tx, ids)
238 if err != nil {
239 return err
240 }
241
242 for _, h := range handles {
243 users = append(users, h.Handle)
244 }
245
246 return nil
247 }); err != nil {
248 return nil, db.WrapError(err)
249 }
250
251 return users, nil
252}
253
254// AddPublicKey adds a public key to a user.
255//
256// It implements backend.Backend.
257func (d *Backend) AddPublicKey(ctx context.Context, username string, pk ssh.PublicKey) error {
258 username = strings.ToLower(username)
259 if err := utils.ValidateHandle(username); err != nil {
260 return err
261 }
262
263 return db.WrapError(
264 d.db.TransactionContext(ctx, func(tx *db.Tx) error {
265 return d.store.AddPublicKeyByUsername(ctx, tx, username, pk)
266 }),
267 )
268}
269
270// CreateUser creates a new user.
271//
272// It implements backend.Backend.
273func (d *Backend) CreateUser(ctx context.Context, username string, opts proto.UserOptions) (proto.User, error) {
274 if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
275 return d.store.CreateUser(ctx, tx, username, opts.Admin, opts.PublicKeys, opts.Emails)
276 }); err != nil {
277 return nil, db.WrapError(err)
278 }
279
280 return d.User(ctx, username)
281}
282
283// DeleteUser deletes a user.
284//
285// It implements backend.Backend.
286func (d *Backend) DeleteUser(ctx context.Context, username string) error {
287 username = strings.ToLower(username)
288 if err := utils.ValidateHandle(username); err != nil {
289 return err
290 }
291
292 return d.db.TransactionContext(ctx, func(tx *db.Tx) error {
293 if err := d.store.DeleteUserByUsername(ctx, tx, username); err != nil {
294 return db.WrapError(err)
295 }
296
297 return d.DeleteUserRepositories(ctx, username)
298 })
299}
300
301// RemovePublicKey removes a public key from a user.
302//
303// It implements backend.Backend.
304func (d *Backend) RemovePublicKey(ctx context.Context, username string, pk ssh.PublicKey) error {
305 return db.WrapError(
306 d.db.TransactionContext(ctx, func(tx *db.Tx) error {
307 return d.store.RemovePublicKeyByUsername(ctx, tx, username, pk)
308 }),
309 )
310}
311
312// ListPublicKeys lists the public keys of a user.
313func (d *Backend) ListPublicKeys(ctx context.Context, username string) ([]ssh.PublicKey, error) {
314 username = strings.ToLower(username)
315 if err := utils.ValidateHandle(username); err != nil {
316 return nil, err
317 }
318
319 var keys []ssh.PublicKey
320 if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
321 var err error
322 keys, err = d.store.ListPublicKeysByUsername(ctx, tx, username)
323 return err
324 }); err != nil {
325 return nil, db.WrapError(err)
326 }
327
328 return keys, nil
329}
330
331// SetUsername sets the username of a user.
332//
333// It implements backend.Backend.
334func (d *Backend) SetUsername(ctx context.Context, username string, newUsername string) error {
335 username = strings.ToLower(username)
336 if err := utils.ValidateHandle(username); err != nil {
337 return err
338 }
339
340 return db.WrapError(
341 d.db.TransactionContext(ctx, func(tx *db.Tx) error {
342 return d.store.SetUsernameByUsername(ctx, tx, username, newUsername)
343 }),
344 )
345}
346
347// SetAdmin sets the admin flag of a user.
348//
349// It implements backend.Backend.
350func (d *Backend) SetAdmin(ctx context.Context, username string, admin bool) error {
351 username = strings.ToLower(username)
352 if err := utils.ValidateHandle(username); err != nil {
353 return err
354 }
355
356 return db.WrapError(
357 d.db.TransactionContext(ctx, func(tx *db.Tx) error {
358 return d.store.SetAdminByUsername(ctx, tx, username, admin)
359 }),
360 )
361}
362
363// SetPassword sets the password of a user.
364func (d *Backend) SetPassword(ctx context.Context, username string, rawPassword string) error {
365 username = strings.ToLower(username)
366 if err := utils.ValidateHandle(username); err != nil {
367 return err
368 }
369
370 password, err := HashPassword(rawPassword)
371 if err != nil {
372 return err
373 }
374
375 return db.WrapError(
376 d.db.TransactionContext(ctx, func(tx *db.Tx) error {
377 return d.store.SetUserPasswordByUsername(ctx, tx, username, password)
378 }),
379 )
380}
381
382// AddUserEmail adds an email to a user.
383func (d *Backend) AddUserEmail(ctx context.Context, user proto.User, email string) error {
384 return db.WrapError(
385 d.db.TransactionContext(ctx, func(tx *db.Tx) error {
386 return d.store.AddUserEmail(ctx, tx, user.ID(), email, false)
387 }),
388 )
389}
390
391// ListUserEmails lists the emails of a user.
392func (d *Backend) ListUserEmails(ctx context.Context, user proto.User) ([]proto.UserEmail, error) {
393 var ems []proto.UserEmail
394 if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
395 emails, err := d.store.ListUserEmails(ctx, tx, user.ID())
396 if err != nil {
397 return err
398 }
399
400 for _, e := range emails {
401 ems = append(ems, &userEmail{e})
402 }
403
404 return nil
405 }); err != nil {
406 return nil, db.WrapError(err)
407 }
408
409 return ems, nil
410}
411
412// RemoveUserEmail deletes an email for a user.
413// The deleted email must not be the primary email.
414func (d *Backend) RemoveUserEmail(ctx context.Context, user proto.User, email string) error {
415 return db.WrapError(
416 d.db.TransactionContext(ctx, func(tx *db.Tx) error {
417 return d.store.RemoveUserEmail(ctx, tx, user.ID(), email)
418 }),
419 )
420}
421
422// SetUserPrimaryEmail sets the primary email of a user.
423func (d *Backend) SetUserPrimaryEmail(ctx context.Context, user proto.User, email string) error {
424 return db.WrapError(
425 d.db.TransactionContext(ctx, func(tx *db.Tx) error {
426 return d.store.SetUserPrimaryEmail(ctx, tx, user.ID(), email)
427 }),
428 )
429}
430
431type user struct {
432 user models.User
433 publicKeys []ssh.PublicKey
434 handle models.Handle
435 emails []proto.UserEmail
436}
437
438var _ proto.User = (*user)(nil)
439
440// IsAdmin implements proto.User
441func (u *user) IsAdmin() bool {
442 return u.user.Admin
443}
444
445// PublicKeys implements proto.User
446func (u *user) PublicKeys() []ssh.PublicKey {
447 return u.publicKeys
448}
449
450// Username implements proto.User
451func (u *user) Username() string {
452 return u.handle.Handle
453}
454
455// ID implements proto.User.
456func (u *user) ID() int64 {
457 return u.user.ID
458}
459
460// Password implements proto.User.
461func (u *user) Password() string {
462 if u.user.Password.Valid {
463 return u.user.Password.String
464 }
465
466 return ""
467}
468
469// Emails implements proto.User.
470func (u *user) Emails() []proto.UserEmail {
471 return u.emails
472}
473
474type userEmail struct {
475 email models.UserEmail
476}
477
478var _ proto.UserEmail = (*userEmail)(nil)
479
480// Email implements proto.UserEmail.
481func (e *userEmail) Email() string {
482 return e.email.Email
483}
484
485// ID implements proto.UserEmail.
486func (e *userEmail) ID() int64 {
487 return e.email.ID
488}
489
490// IsPrimary implements proto.UserEmail.
491func (e *userEmail) IsPrimary() bool {
492 return e.email.IsPrimary
493}