Skip to content

Commit 8fbd41a

Browse files
committed
feat: add user email support
1 parent caca389 commit 8fbd41a

File tree

12 files changed

+345
-23
lines changed

12 files changed

+345
-23
lines changed

pkg/backend/user.go

+121-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ func (d *Backend) User(ctx context.Context, username string) (proto.User, error)
2626
var m models.User
2727
var pks []ssh.PublicKey
2828
var hl models.Handle
29+
var ems []proto.UserEmail
2930
if err := d.db.TransactionContext(ctx, func(tx *db.Tx) error {
3031
var err error
3132
m, err = d.store.FindUserByUsername(ctx, tx, username)
@@ -38,6 +39,15 @@ func (d *Backend) User(ctx context.Context, username string) (proto.User, error)
3839
return err
3940
}
4041

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+
4151
hl, err = d.store.GetHandleByUserID(ctx, tx, m.ID)
4252
return err
4353
}); err != nil {
@@ -53,6 +63,7 @@ func (d *Backend) User(ctx context.Context, username string) (proto.User, error)
5363
user: m,
5464
publicKeys: pks,
5565
handle: hl,
66+
emails: ems,
5667
}, nil
5768
}
5869

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

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+
7697
hl, err = d.store.GetHandleByUserID(ctx, tx, m.ID)
7798
return err
7899
}); err != nil {
@@ -88,6 +109,7 @@ func (d *Backend) UserByID(ctx context.Context, id int64) (proto.User, error) {
88109
user: m,
89110
publicKeys: pks,
90111
handle: hl,
112+
emails: ems,
91113
}, nil
92114
}
93115

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

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+
113145
hl, err = d.store.GetHandleByUserID(ctx, tx, m.ID)
114146
return err
115147
}); err != nil {
@@ -125,6 +157,7 @@ func (d *Backend) UserByPublicKey(ctx context.Context, pk ssh.PublicKey) (proto.
125157
user: m,
126158
publicKeys: pks,
127159
handle: hl,
160+
emails: ems,
128161
}, nil
129162
}
130163

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

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

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+
159202
hl, err = d.store.GetHandleByUserID(ctx, tx, m.ID)
160203
return err
161204
}); err != nil {
@@ -171,6 +214,7 @@ func (d *Backend) UserByAccessToken(ctx context.Context, token string) (proto.Us
171214
user: m,
172215
publicKeys: pks,
173216
handle: hl,
217+
emails: ems,
174218
}, nil
175219
}
176220

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

382+
// AddUserEmail adds an email to a user.
383+
func (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.
392+
func (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.
414+
func (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.
423+
func (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+
338431
type user struct {
339432
user models.User
340433
publicKeys []ssh.PublicKey
341434
handle models.Handle
435+
emails []proto.UserEmail
342436
}
343437

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

372466
return ""
373467
}
468+
469+
// Emails implements proto.User.
470+
func (u *user) Emails() []proto.UserEmail {
471+
return u.emails
472+
}
473+
474+
type userEmail struct {
475+
email models.UserEmail
476+
}
477+
478+
var _ proto.UserEmail = (*userEmail)(nil)
479+
480+
// Email implements proto.UserEmail.
481+
func (e *userEmail) Email() string {
482+
return e.email.Email
483+
}
484+
485+
// ID implements proto.UserEmail.
486+
func (e *userEmail) ID() int64 {
487+
return e.email.ID
488+
}
489+
490+
// IsPrimary implements proto.UserEmail.
491+
func (e *userEmail) IsPrimary() bool {
492+
return e.email.IsPrimary
493+
}

pkg/db/migrate/0004_create_orgs_teams_postgres.up.sql

+6-3
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ CREATE TABLE IF NOT EXISTS user_emails (
6969
id SERIAL PRIMARY KEY,
7070
user_id INTEGER NOT NULL,
7171
email TEXT NOT NULL UNIQUE,
72-
is_primary BOOLEAN NOT NULL,
72+
is_primary BOOLEAN NOT NULL DEFAULT false,
7373
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
7474
updated_at TIMESTAMP NOT NULL,
7575
CONSTRAINT user_id_fk
@@ -78,6 +78,9 @@ CREATE TABLE IF NOT EXISTS user_emails (
7878
ON UPDATE CASCADE
7979
);
8080

81+
-- Create unique index for primary email
82+
CREATE UNIQUE INDEX user_emails_user_id_is_primary_idx ON user_emails (user_id) WHERE is_primary;
83+
8184
-- Add name to users table
8285
ALTER TABLE users ADD COLUMN name TEXT;
8386

@@ -112,7 +115,7 @@ ALTER TABLE repos ADD CONSTRAINT org_id_fk
112115
ALTER TABLE repos ALTER COLUMN user_id DROP NOT NULL;
113116

114117
-- Check that both user_id and org_id can't be null
115-
ALTER TABLE repos ADD CONSTRAINT user_id_org_id_not_null CHECK (user_id IS NULL <> org_id IS NULL);
118+
ALTER TABLE repos ADD CONSTRAINT user_id_org_id_not_null CHECK ((user_id IS NULL) <> (org_id IS NULL));
116119

117120
-- Add team_id to collabs table
118121
ALTER TABLE collabs ADD COLUMN team_id INTEGER;
@@ -125,7 +128,7 @@ ALTER TABLE collabs ADD CONSTRAINT team_id_fk
125128
ALTER TABLE collabs ALTER COLUMN user_id DROP NOT NULL;
126129

127130
-- Check that both user_id and team_id can't be null
128-
ALTER TABLE collabs ADD CONSTRAINT user_id_team_id_not_null CHECK (user_id IS NULL <> team_id IS NULL);
131+
ALTER TABLE collabs ADD CONSTRAINT user_id_team_id_not_null CHECK ((user_id IS NULL) <> (team_id IS NULL));
129132

130133
-- Alter unique constraint on collabs table
131134
ALTER TABLE collabs DROP CONSTRAINT collabs_user_id_repo_id_key;

pkg/db/migrate/0004_create_orgs_teams_sqlite.up.sql

+4-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ CREATE TABLE IF NOT EXISTS user_emails (
7171
id INTEGER PRIMARY KEY AUTOINCREMENT,
7272
user_id INTEGER NOT NULL,
7373
email TEXT NOT NULL UNIQUE,
74-
is_primary BOOLEAN NOT NULL,
74+
is_primary BOOLEAN NOT NULL DEFAULT false,
7575
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
7676
updated_at TIMESTAMP NOT NULL,
7777
CONSTRAINT user_id_fk
@@ -80,6 +80,9 @@ CREATE TABLE IF NOT EXISTS user_emails (
8080
ON UPDATE CASCADE
8181
);
8282

83+
-- Create unique index for primary email
84+
CREATE UNIQUE INDEX user_emails_user_id_is_primary_idx ON user_emails (user_id) WHERE is_primary;
85+
8386
ALTER TABLE users RENAME TO _users_old;
8487

8588
CREATE TABLE IF NOT EXISTS users (

pkg/proto/user.go

+17
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ type User interface {
1414
PublicKeys() []ssh.PublicKey
1515
// Password returns the user's password hash.
1616
Password() string
17+
// Emails returns the user's emails.
18+
Emails() []UserEmail
1719
}
1820

1921
// UserOptions are options for creating a user.
@@ -22,4 +24,19 @@ type UserOptions struct {
2224
Admin bool
2325
// PublicKeys are the user's public keys.
2426
PublicKeys []ssh.PublicKey
27+
// Emails are the user's emails.
28+
// The first email in the slice will be set as the user's primary email.
29+
Emails []string
30+
}
31+
32+
// UserEmail represents a user's email address.
33+
type UserEmail interface {
34+
// ID returns the email's ID.
35+
ID() int64
36+
37+
// Email returns the email address.
38+
Email() string
39+
40+
// IsPrimary returns whether the email is the user's primary email.
41+
IsPrimary() bool
2542
}

pkg/ssh/cmd/org.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func OrgCommand() *cobra.Command {
3333
Use: "list",
3434
Short: "List organizations",
3535
Args: cobra.NoArgs,
36-
RunE: func(cmd *cobra.Command, args []string) error {
36+
RunE: func(cmd *cobra.Command, _ []string) error {
3737
ctx := cmd.Context()
3838
be := backend.FromContext(ctx)
3939
user := proto.UserFromContext(ctx)

0 commit comments

Comments
 (0)