Skip to content

Commit

Permalink
feat: add user email support
Browse files Browse the repository at this point in the history
  • Loading branch information
aymanbagabas committed Dec 11, 2023
1 parent caca389 commit ba597bc
Show file tree
Hide file tree
Showing 12 changed files with 343 additions and 21 deletions.
122 changes: 121 additions & 1 deletion pkg/backend/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func (d *Backend) User(ctx context.Context, username string) (proto.User, error)
var m models.User
var pks []ssh.PublicKey
var hl models.Handle
var ems []proto.UserEmail
if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
var err error
m, err = d.store.FindUserByUsername(ctx, tx, username)
Expand All @@ -38,6 +39,15 @@ func (d *Backend) User(ctx context.Context, username string) (proto.User, error)
return err
}

emails, err := d.store.ListUserEmails(ctx, tx, m.ID)
if err != nil {
return err
}

for _, e := range emails {
ems = append(ems, &userEmail{e})
}

hl, err = d.store.GetHandleByUserID(ctx, tx, m.ID)
return err
}); err != nil {
Expand All @@ -53,6 +63,7 @@ func (d *Backend) User(ctx context.Context, username string) (proto.User, error)
user: m,
publicKeys: pks,
handle: hl,
emails: ems,
}, nil
}

Expand All @@ -61,6 +72,7 @@ func (d *Backend) UserByID(ctx context.Context, id int64) (proto.User, error) {
var m models.User
var pks []ssh.PublicKey
var hl models.Handle
var ems []proto.UserEmail
if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
var err error
m, err = d.store.GetUserByID(ctx, tx, id)
Expand All @@ -73,6 +85,15 @@ func (d *Backend) UserByID(ctx context.Context, id int64) (proto.User, error) {
return err
}

emails, err := d.store.ListUserEmails(ctx, tx, m.ID)
if err != nil {
return err
}

for _, e := range emails {
ems = append(ems, &userEmail{e})
}

hl, err = d.store.GetHandleByUserID(ctx, tx, m.ID)
return err
}); err != nil {
Expand All @@ -88,6 +109,7 @@ func (d *Backend) UserByID(ctx context.Context, id int64) (proto.User, error) {
user: m,
publicKeys: pks,
handle: hl,
emails: ems,
}, nil
}

Expand All @@ -98,6 +120,7 @@ func (d *Backend) UserByPublicKey(ctx context.Context, pk ssh.PublicKey) (proto.
var m models.User
var pks []ssh.PublicKey
var hl models.Handle
var ems []proto.UserEmail
if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
var err error
m, err = d.store.FindUserByPublicKey(ctx, tx, pk)
Expand All @@ -110,6 +133,15 @@ func (d *Backend) UserByPublicKey(ctx context.Context, pk ssh.PublicKey) (proto.
return err
}

emails, err := d.store.ListUserEmails(ctx, tx, m.ID)
if err != nil {
return err
}

for _, e := range emails {
ems = append(ems, &userEmail{e})
}

hl, err = d.store.GetHandleByUserID(ctx, tx, m.ID)
return err
}); err != nil {
Expand All @@ -125,6 +157,7 @@ func (d *Backend) UserByPublicKey(ctx context.Context, pk ssh.PublicKey) (proto.
user: m,
publicKeys: pks,
handle: hl,
emails: ems,
}, nil
}

Expand All @@ -134,6 +167,7 @@ func (d *Backend) UserByAccessToken(ctx context.Context, token string) (proto.Us
var m models.User
var pks []ssh.PublicKey
var hl models.Handle
var ems []proto.UserEmail
token = HashToken(token)

if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
Expand All @@ -156,6 +190,15 @@ func (d *Backend) UserByAccessToken(ctx context.Context, token string) (proto.Us
return err
}

emails, err := d.store.ListUserEmails(ctx, tx, m.ID)
if err != nil {
return err
}

for _, e := range emails {
ems = append(ems, &userEmail{e})
}

hl, err = d.store.GetHandleByUserID(ctx, tx, m.ID)
return err
}); err != nil {
Expand All @@ -171,6 +214,7 @@ func (d *Backend) UserByAccessToken(ctx context.Context, token string) (proto.Us
user: m,
publicKeys: pks,
handle: hl,
emails: ems,
}, nil
}

Expand Down Expand Up @@ -228,7 +272,7 @@ func (d *Backend) AddPublicKey(ctx context.Context, username string, pk ssh.Publ
// It implements backend.Backend.
func (d *Backend) CreateUser(ctx context.Context, username string, opts proto.UserOptions) (proto.User, error) {
if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
return d.store.CreateUser(ctx, tx, username, opts.Admin, opts.PublicKeys)
return d.store.CreateUser(ctx, tx, username, opts.Admin, opts.PublicKeys, opts.Emails)
}); err != nil {
return nil, db.WrapError(err)
}
Expand Down Expand Up @@ -335,10 +379,60 @@ func (d *Backend) SetPassword(ctx context.Context, username string, rawPassword
)
}

// AddUserEmail adds an email to a user.
func (d *Backend) AddUserEmail(ctx context.Context, user proto.User, email string) error {
return db.WrapError(
d.db.TransactionContext(ctx, func(tx *db.Tx) error {
return d.store.AddUserEmail(ctx, tx, user.ID(), email, false)
}),
)
}

// ListUserEmails lists the emails of a user.
func (d *Backend) ListUserEmails(ctx context.Context, user proto.User) ([]proto.UserEmail, error) {
var ems []proto.UserEmail
if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
emails, err := d.store.ListUserEmails(ctx, tx, user.ID())
if err != nil {
return err
}

for _, e := range emails {
ems = append(ems, &userEmail{e})
}

return nil
}); err != nil {
return nil, db.WrapError(err)
}

return ems, nil
}

// RemoveUserEmail deletes an email for a user.
// The deleted email must not be the primary email.
func (d *Backend) RemoveUserEmail(ctx context.Context, user proto.User, email string) error {
return db.WrapError(
d.db.TransactionContext(ctx, func(tx *db.Tx) error {
return d.store.RemoveUserEmail(ctx, tx, user.ID(), email)
}),
)
}

// SetUserPrimaryEmail sets the primary email of a user.
func (d *Backend) SetUserPrimaryEmail(ctx context.Context, user proto.User, email string) error {
return db.WrapError(
d.db.TransactionContext(ctx, func(tx *db.Tx) error {
return d.store.SetUserPrimaryEmail(ctx, tx, user.ID(), email)
}),
)
}

type user struct {
user models.User
publicKeys []ssh.PublicKey
handle models.Handle
emails []proto.UserEmail
}

var _ proto.User = (*user)(nil)
Expand Down Expand Up @@ -371,3 +465,29 @@ func (u *user) Password() string {

return ""
}

// Emails implements proto.User.
func (u *user) Emails() []proto.UserEmail {
return u.emails
}

type userEmail struct {
email models.UserEmail
}

var _ proto.UserEmail = (*userEmail)(nil)

// Email implements proto.UserEmail.
func (e *userEmail) Email() string {
return e.email.Email
}

// ID implements proto.UserEmail.
func (e *userEmail) ID() int64 {
return e.email.ID
}

// IsPrimary implements proto.UserEmail.
func (e *userEmail) IsPrimary() bool {
return e.email.IsPrimary
}
5 changes: 4 additions & 1 deletion pkg/db/migrate/0004_create_orgs_teams_postgres.up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ CREATE TABLE IF NOT EXISTS user_emails (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
email TEXT NOT NULL UNIQUE,
is_primary BOOLEAN NOT NULL,
is_primary BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL,
CONSTRAINT user_id_fk
Expand All @@ -78,6 +78,9 @@ CREATE TABLE IF NOT EXISTS user_emails (
ON UPDATE CASCADE
);

-- Create unique index for primary email
CREATE UNIQUE INDEX user_emails_user_id_is_primary_idx ON user_emails (user_id) WHERE is_primary;

-- Add name to users table
ALTER TABLE users ADD COLUMN name TEXT;

Expand Down
5 changes: 4 additions & 1 deletion pkg/db/migrate/0004_create_orgs_teams_sqlite.up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ CREATE TABLE IF NOT EXISTS user_emails (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
email TEXT NOT NULL UNIQUE,
is_primary BOOLEAN NOT NULL,
is_primary BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL,
CONSTRAINT user_id_fk
Expand All @@ -80,6 +80,9 @@ CREATE TABLE IF NOT EXISTS user_emails (
ON UPDATE CASCADE
);

-- Create unique index for primary email
CREATE UNIQUE INDEX user_emails_user_id_is_primary_idx ON user_emails (user_id) WHERE is_primary;

ALTER TABLE users RENAME TO _users_old;

CREATE TABLE IF NOT EXISTS users (
Expand Down
17 changes: 17 additions & 0 deletions pkg/proto/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ type User interface {
PublicKeys() []ssh.PublicKey
// Password returns the user's password hash.
Password() string
// Emails returns the user's emails.
Emails() []UserEmail
}

// UserOptions are options for creating a user.
Expand All @@ -22,4 +24,19 @@ type UserOptions struct {
Admin bool
// PublicKeys are the user's public keys.
PublicKeys []ssh.PublicKey
// Emails are the user's emails.
// The first email in the slice will be set as the user's primary email.
Emails []string
}

// UserEmail represents a user's email address.
type UserEmail interface {
// ID returns the email's ID.
ID() int64

// Email returns the email address.
Email() string

// IsPrimary returns whether the email is the user's primary email.
IsPrimary() bool
}
2 changes: 1 addition & 1 deletion pkg/ssh/cmd/org.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func OrgCommand() *cobra.Command {
Use: "list",
Short: "List organizations",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
be := backend.FromContext(ctx)
user := proto.UserFromContext(ctx)
Expand Down
Loading

0 comments on commit ba597bc

Please sign in to comment.