user.go

  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}