diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go
index b72db00912ff7..981be686df469 100644
--- a/coderd/apidoc/docs.go
+++ b/coderd/apidoc/docs.go
@@ -9811,6 +9811,9 @@ const docTemplate = `{
"avatar_url": {
"type": "string"
},
+ "id": {
+ "type": "integer"
+ },
"login": {
"type": "string"
},
diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json
index bf7216f1f313b..14efc71711687 100644
--- a/coderd/apidoc/swagger.json
+++ b/coderd/apidoc/swagger.json
@@ -8801,6 +8801,9 @@
"avatar_url": {
"type": "string"
},
+ "id": {
+ "type": "integer"
+ },
"login": {
"type": "string"
},
diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go
index b7cff64e2a57b..941ab4caccfac 100644
--- a/coderd/database/dbauthz/dbauthz.go
+++ b/coderd/database/dbauthz/dbauthz.go
@@ -3260,6 +3260,23 @@ func (q *querier) UpdateUserDeletedByID(ctx context.Context, id uuid.UUID) error
return deleteQ(q.log, q.auth, q.db.GetUserByID, q.db.UpdateUserDeletedByID)(ctx, id)
}
+func (q *querier) UpdateUserGithubComUserID(ctx context.Context, arg database.UpdateUserGithubComUserIDParams) error {
+ user, err := q.db.GetUserByID(ctx, arg.ID)
+ if err != nil {
+ return err
+ }
+
+ err = q.authorizeContext(ctx, policy.ActionUpdatePersonal, user)
+ if err != nil {
+ // System user can also update
+ err = q.authorizeContext(ctx, policy.ActionUpdate, user)
+ if err != nil {
+ return err
+ }
+ }
+ return q.db.UpdateUserGithubComUserID(ctx, arg)
+}
+
func (q *querier) UpdateUserHashedPassword(ctx context.Context, arg database.UpdateUserHashedPasswordParams) error {
user, err := q.db.GetUserByID(ctx, arg.ID)
if err != nil {
diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go
index 876c0d797f64a..627558dbe1f73 100644
--- a/coderd/database/dbauthz/dbauthz_test.go
+++ b/coderd/database/dbauthz/dbauthz_test.go
@@ -1105,6 +1105,12 @@ func (s *MethodTestSuite) TestUser() {
u := dbgen.User(s.T(), db, database.User{})
check.Args(u.ID).Asserts(u, policy.ActionDelete).Returns()
}))
+ s.Run("UpdateUserGithubComUserID", s.Subtest(func(db database.Store, check *expects) {
+ u := dbgen.User(s.T(), db, database.User{})
+ check.Args(database.UpdateUserGithubComUserIDParams{
+ ID: u.ID,
+ }).Asserts(u, policy.ActionUpdatePersonal)
+ }))
s.Run("UpdateUserHashedPassword", s.Subtest(func(db database.Store, check *expects) {
u := dbgen.User(s.T(), db, database.User{})
check.Args(database.UpdateUserHashedPasswordParams{
diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go
index f32de78b72714..09c0585964795 100644
--- a/coderd/database/dbmem/dbmem.go
+++ b/coderd/database/dbmem/dbmem.go
@@ -7985,6 +7985,26 @@ func (q *FakeQuerier) UpdateUserDeletedByID(_ context.Context, id uuid.UUID) err
return sql.ErrNoRows
}
+func (q *FakeQuerier) UpdateUserGithubComUserID(_ context.Context, arg database.UpdateUserGithubComUserIDParams) error {
+ err := validateDatabaseType(arg)
+ if err != nil {
+ return err
+ }
+
+ q.mutex.Lock()
+ defer q.mutex.Unlock()
+
+ for i, user := range q.users {
+ if user.ID != arg.ID {
+ continue
+ }
+ user.GithubComUserID = arg.GithubComUserID
+ q.users[i] = user
+ return nil
+ }
+ return sql.ErrNoRows
+}
+
func (q *FakeQuerier) UpdateUserHashedPassword(_ context.Context, arg database.UpdateUserHashedPasswordParams) error {
if err := validateDatabaseType(arg); err != nil {
return err
diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go
index 2b25591568f8c..1a13ff7f0b5a9 100644
--- a/coderd/database/dbmetrics/dbmetrics.go
+++ b/coderd/database/dbmetrics/dbmetrics.go
@@ -2097,6 +2097,13 @@ func (m metricsStore) UpdateUserDeletedByID(ctx context.Context, id uuid.UUID) e
return r0
}
+func (m metricsStore) UpdateUserGithubComUserID(ctx context.Context, arg database.UpdateUserGithubComUserIDParams) error {
+ start := time.Now()
+ r0 := m.s.UpdateUserGithubComUserID(ctx, arg)
+ m.queryLatencies.WithLabelValues("UpdateUserGithubComUserID").Observe(time.Since(start).Seconds())
+ return r0
+}
+
func (m metricsStore) UpdateUserHashedPassword(ctx context.Context, arg database.UpdateUserHashedPasswordParams) error {
start := time.Now()
err := m.s.UpdateUserHashedPassword(ctx, arg)
diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go
index b91ba6c8bd5d8..b4aa6043510f1 100644
--- a/coderd/database/dbmock/dbmock.go
+++ b/coderd/database/dbmock/dbmock.go
@@ -4416,6 +4416,20 @@ func (mr *MockStoreMockRecorder) UpdateUserDeletedByID(arg0, arg1 any) *gomock.C
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserDeletedByID", reflect.TypeOf((*MockStore)(nil).UpdateUserDeletedByID), arg0, arg1)
}
+// UpdateUserGithubComUserID mocks base method.
+func (m *MockStore) UpdateUserGithubComUserID(arg0 context.Context, arg1 database.UpdateUserGithubComUserIDParams) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "UpdateUserGithubComUserID", arg0, arg1)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// UpdateUserGithubComUserID indicates an expected call of UpdateUserGithubComUserID.
+func (mr *MockStoreMockRecorder) UpdateUserGithubComUserID(arg0, arg1 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserGithubComUserID", reflect.TypeOf((*MockStore)(nil).UpdateUserGithubComUserID), arg0, arg1)
+}
+
// UpdateUserHashedPassword mocks base method.
func (m *MockStore) UpdateUserHashedPassword(arg0 context.Context, arg1 database.UpdateUserHashedPasswordParams) error {
m.ctrl.T.Helper()
diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql
index dc15cf9bd4af8..c3b74732dd825 100644
--- a/coderd/database/dump.sql
+++ b/coderd/database/dump.sql
@@ -974,7 +974,8 @@ CREATE TABLE users (
last_seen_at timestamp without time zone DEFAULT '0001-01-01 00:00:00'::timestamp without time zone NOT NULL,
quiet_hours_schedule text DEFAULT ''::text NOT NULL,
theme_preference text DEFAULT ''::text NOT NULL,
- name text DEFAULT ''::text NOT NULL
+ name text DEFAULT ''::text NOT NULL,
+ github_com_user_id bigint
);
COMMENT ON COLUMN users.quiet_hours_schedule IS 'Daily (!) cron schedule (with optional CRON_TZ) signifying the start of the user''s quiet hours. If empty, the default quiet hours on the instance is used instead.';
@@ -983,6 +984,8 @@ COMMENT ON COLUMN users.theme_preference IS '"" can be interpreted as "the user
COMMENT ON COLUMN users.name IS 'Name of the Coder user';
+COMMENT ON COLUMN users.github_com_user_id IS 'The GitHub.com numerical user ID. At time of implementation, this is used to check if the user has starred the Coder repository.';
+
CREATE VIEW visible_users AS
SELECT users.id,
users.username,
diff --git a/coderd/database/migrations/000237_github_com_user_id.down.sql b/coderd/database/migrations/000237_github_com_user_id.down.sql
new file mode 100644
index 0000000000000..bf3cddc82e5e4
--- /dev/null
+++ b/coderd/database/migrations/000237_github_com_user_id.down.sql
@@ -0,0 +1 @@
+ALTER TABLE users DROP COLUMN github_com_user_id;
diff --git a/coderd/database/migrations/000237_github_com_user_id.up.sql b/coderd/database/migrations/000237_github_com_user_id.up.sql
new file mode 100644
index 0000000000000..81495695b644f
--- /dev/null
+++ b/coderd/database/migrations/000237_github_com_user_id.up.sql
@@ -0,0 +1,3 @@
+ALTER TABLE users ADD COLUMN github_com_user_id BIGINT;
+
+COMMENT ON COLUMN users.github_com_user_id IS 'The GitHub.com numerical user ID. At time of implementation, this is used to check if the user has starred the Coder repository.';
diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go
index 78826ea7cc8b5..532449089535f 100644
--- a/coderd/database/modelqueries.go
+++ b/coderd/database/modelqueries.go
@@ -361,6 +361,7 @@ func (q *sqlQuerier) GetAuthorizedUsers(ctx context.Context, arg GetUsersParams,
&i.QuietHoursSchedule,
&i.ThemePreference,
&i.Name,
+ &i.GithubComUserID,
&i.Count,
); err != nil {
return nil, err
diff --git a/coderd/database/models.go b/coderd/database/models.go
index 0ee78e286516e..70350f54a704f 100644
--- a/coderd/database/models.go
+++ b/coderd/database/models.go
@@ -2475,6 +2475,8 @@ type User struct {
ThemePreference string `db:"theme_preference" json:"theme_preference"`
// Name of the Coder user
Name string `db:"name" json:"name"`
+ // The GitHub.com numerical user ID. At time of implementation, this is used to check if the user has starred the Coder repository.
+ GithubComUserID sql.NullInt64 `db:"github_com_user_id" json:"github_com_user_id"`
}
type UserLink struct {
diff --git a/coderd/database/querier.go b/coderd/database/querier.go
index 9d0494813e306..95015aa706348 100644
--- a/coderd/database/querier.go
+++ b/coderd/database/querier.go
@@ -421,6 +421,7 @@ type sqlcQuerier interface {
UpdateTemplateWorkspacesLastUsedAt(ctx context.Context, arg UpdateTemplateWorkspacesLastUsedAtParams) error
UpdateUserAppearanceSettings(ctx context.Context, arg UpdateUserAppearanceSettingsParams) (User, error)
UpdateUserDeletedByID(ctx context.Context, id uuid.UUID) error
+ UpdateUserGithubComUserID(ctx context.Context, arg UpdateUserGithubComUserIDParams) error
UpdateUserHashedPassword(ctx context.Context, arg UpdateUserHashedPasswordParams) error
UpdateUserLastSeenAt(ctx context.Context, arg UpdateUserLastSeenAtParams) (User, error)
UpdateUserLink(ctx context.Context, arg UpdateUserLinkParams) (UserLink, error)
diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go
index 904f304bd25a9..4e7e0ceb3150d 100644
--- a/coderd/database/queries.sql.go
+++ b/coderd/database/queries.sql.go
@@ -1350,7 +1350,7 @@ func (q *sqlQuerier) GetGroupMembers(ctx context.Context) ([]GroupMember, error)
const getGroupMembersByGroupID = `-- name: GetGroupMembersByGroupID :many
SELECT
- users.id, users.email, users.username, users.hashed_password, users.created_at, users.updated_at, users.status, users.rbac_roles, users.login_type, users.avatar_url, users.deleted, users.last_seen_at, users.quiet_hours_schedule, users.theme_preference, users.name
+ users.id, users.email, users.username, users.hashed_password, users.created_at, users.updated_at, users.status, users.rbac_roles, users.login_type, users.avatar_url, users.deleted, users.last_seen_at, users.quiet_hours_schedule, users.theme_preference, users.name, users.github_com_user_id
FROM
users
LEFT JOIN
@@ -1399,6 +1399,7 @@ func (q *sqlQuerier) GetGroupMembersByGroupID(ctx context.Context, groupID uuid.
&i.QuietHoursSchedule,
&i.ThemePreference,
&i.Name,
+ &i.GithubComUserID,
); err != nil {
return nil, err
}
@@ -9222,7 +9223,7 @@ func (q *sqlQuerier) GetAuthorizationUserRoles(ctx context.Context, userID uuid.
const getUserByEmailOrUsername = `-- name: GetUserByEmailOrUsername :one
SELECT
- id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name
+ id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, github_com_user_id
FROM
users
WHERE
@@ -9256,13 +9257,14 @@ func (q *sqlQuerier) GetUserByEmailOrUsername(ctx context.Context, arg GetUserBy
&i.QuietHoursSchedule,
&i.ThemePreference,
&i.Name,
+ &i.GithubComUserID,
)
return i, err
}
const getUserByID = `-- name: GetUserByID :one
SELECT
- id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name
+ id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, github_com_user_id
FROM
users
WHERE
@@ -9290,6 +9292,7 @@ func (q *sqlQuerier) GetUserByID(ctx context.Context, id uuid.UUID) (User, error
&i.QuietHoursSchedule,
&i.ThemePreference,
&i.Name,
+ &i.GithubComUserID,
)
return i, err
}
@@ -9312,7 +9315,7 @@ func (q *sqlQuerier) GetUserCount(ctx context.Context) (int64, error) {
const getUsers = `-- name: GetUsers :many
SELECT
- id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, COUNT(*) OVER() AS count
+ id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, github_com_user_id, COUNT(*) OVER() AS count
FROM
users
WHERE
@@ -9411,6 +9414,7 @@ type GetUsersRow struct {
QuietHoursSchedule string `db:"quiet_hours_schedule" json:"quiet_hours_schedule"`
ThemePreference string `db:"theme_preference" json:"theme_preference"`
Name string `db:"name" json:"name"`
+ GithubComUserID sql.NullInt64 `db:"github_com_user_id" json:"github_com_user_id"`
Count int64 `db:"count" json:"count"`
}
@@ -9449,6 +9453,7 @@ func (q *sqlQuerier) GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUse
&i.QuietHoursSchedule,
&i.ThemePreference,
&i.Name,
+ &i.GithubComUserID,
&i.Count,
); err != nil {
return nil, err
@@ -9465,7 +9470,7 @@ func (q *sqlQuerier) GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUse
}
const getUsersByIDs = `-- name: GetUsersByIDs :many
-SELECT id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name FROM users WHERE id = ANY($1 :: uuid [ ])
+SELECT id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, github_com_user_id FROM users WHERE id = ANY($1 :: uuid [ ])
`
// This shouldn't check for deleted, because it's frequently used
@@ -9496,6 +9501,7 @@ func (q *sqlQuerier) GetUsersByIDs(ctx context.Context, ids []uuid.UUID) ([]User
&i.QuietHoursSchedule,
&i.ThemePreference,
&i.Name,
+ &i.GithubComUserID,
); err != nil {
return nil, err
}
@@ -9524,7 +9530,7 @@ INSERT INTO
login_type
)
VALUES
- ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name
+ ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, github_com_user_id
`
type InsertUserParams struct {
@@ -9568,6 +9574,7 @@ func (q *sqlQuerier) InsertUser(ctx context.Context, arg InsertUserParams) (User
&i.QuietHoursSchedule,
&i.ThemePreference,
&i.Name,
+ &i.GithubComUserID,
)
return i, err
}
@@ -9626,7 +9633,7 @@ SET
updated_at = $3
WHERE
id = $1
-RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name
+RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, github_com_user_id
`
type UpdateUserAppearanceSettingsParams struct {
@@ -9654,6 +9661,7 @@ func (q *sqlQuerier) UpdateUserAppearanceSettings(ctx context.Context, arg Updat
&i.QuietHoursSchedule,
&i.ThemePreference,
&i.Name,
+ &i.GithubComUserID,
)
return i, err
}
@@ -9672,6 +9680,25 @@ func (q *sqlQuerier) UpdateUserDeletedByID(ctx context.Context, id uuid.UUID) er
return err
}
+const updateUserGithubComUserID = `-- name: UpdateUserGithubComUserID :exec
+UPDATE
+ users
+SET
+ github_com_user_id = $2
+WHERE
+ id = $1
+`
+
+type UpdateUserGithubComUserIDParams struct {
+ ID uuid.UUID `db:"id" json:"id"`
+ GithubComUserID sql.NullInt64 `db:"github_com_user_id" json:"github_com_user_id"`
+}
+
+func (q *sqlQuerier) UpdateUserGithubComUserID(ctx context.Context, arg UpdateUserGithubComUserIDParams) error {
+ _, err := q.db.ExecContext(ctx, updateUserGithubComUserID, arg.ID, arg.GithubComUserID)
+ return err
+}
+
const updateUserHashedPassword = `-- name: UpdateUserHashedPassword :exec
UPDATE
users
@@ -9698,7 +9725,7 @@ SET
last_seen_at = $2,
updated_at = $3
WHERE
- id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name
+ id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, github_com_user_id
`
type UpdateUserLastSeenAtParams struct {
@@ -9726,6 +9753,7 @@ func (q *sqlQuerier) UpdateUserLastSeenAt(ctx context.Context, arg UpdateUserLas
&i.QuietHoursSchedule,
&i.ThemePreference,
&i.Name,
+ &i.GithubComUserID,
)
return i, err
}
@@ -9743,7 +9771,7 @@ SET
'':: bytea
END
WHERE
- id = $2 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name
+ id = $2 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, github_com_user_id
`
type UpdateUserLoginTypeParams struct {
@@ -9770,6 +9798,7 @@ func (q *sqlQuerier) UpdateUserLoginType(ctx context.Context, arg UpdateUserLogi
&i.QuietHoursSchedule,
&i.ThemePreference,
&i.Name,
+ &i.GithubComUserID,
)
return i, err
}
@@ -9785,7 +9814,7 @@ SET
name = $6
WHERE
id = $1
-RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name
+RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, github_com_user_id
`
type UpdateUserProfileParams struct {
@@ -9823,6 +9852,7 @@ func (q *sqlQuerier) UpdateUserProfile(ctx context.Context, arg UpdateUserProfil
&i.QuietHoursSchedule,
&i.ThemePreference,
&i.Name,
+ &i.GithubComUserID,
)
return i, err
}
@@ -9834,7 +9864,7 @@ SET
quiet_hours_schedule = $2
WHERE
id = $1
-RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name
+RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, github_com_user_id
`
type UpdateUserQuietHoursScheduleParams struct {
@@ -9861,6 +9891,7 @@ func (q *sqlQuerier) UpdateUserQuietHoursSchedule(ctx context.Context, arg Updat
&i.QuietHoursSchedule,
&i.ThemePreference,
&i.Name,
+ &i.GithubComUserID,
)
return i, err
}
@@ -9873,7 +9904,7 @@ SET
rbac_roles = ARRAY(SELECT DISTINCT UNNEST($1 :: text[]))
WHERE
id = $2
-RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name
+RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, github_com_user_id
`
type UpdateUserRolesParams struct {
@@ -9900,6 +9931,7 @@ func (q *sqlQuerier) UpdateUserRoles(ctx context.Context, arg UpdateUserRolesPar
&i.QuietHoursSchedule,
&i.ThemePreference,
&i.Name,
+ &i.GithubComUserID,
)
return i, err
}
@@ -9911,7 +9943,7 @@ SET
status = $2,
updated_at = $3
WHERE
- id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name
+ id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, theme_preference, name, github_com_user_id
`
type UpdateUserStatusParams struct {
@@ -9939,6 +9971,7 @@ func (q *sqlQuerier) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusP
&i.QuietHoursSchedule,
&i.ThemePreference,
&i.Name,
+ &i.GithubComUserID,
)
return i, err
}
diff --git a/coderd/database/queries/users.sql b/coderd/database/queries/users.sql
index 6bbfdac112d7a..44148eb936a33 100644
--- a/coderd/database/queries/users.sql
+++ b/coderd/database/queries/users.sql
@@ -85,6 +85,14 @@ WHERE
id = $1
RETURNING *;
+-- name: UpdateUserGithubComUserID :exec
+UPDATE
+ users
+SET
+ github_com_user_id = $2
+WHERE
+ id = $1;
+
-- name: UpdateUserAppearanceSettings :one
UPDATE
users
diff --git a/coderd/externalauth/externalauth.go b/coderd/externalauth/externalauth.go
index b626a5e28fb1f..d93120fc5da14 100644
--- a/coderd/externalauth/externalauth.go
+++ b/coderd/externalauth/externalauth.go
@@ -154,7 +154,7 @@ func (c *Config) RefreshToken(ctx context.Context, db database.Store, externalAu
retryCtx, retryCtxCancel := context.WithTimeout(ctx, time.Second)
defer retryCtxCancel()
validate:
- valid, _, err := c.ValidateToken(ctx, token)
+ valid, user, err := c.ValidateToken(ctx, token)
if err != nil {
return externalAuthLink, xerrors.Errorf("validate external auth token: %w", err)
}
@@ -189,7 +189,22 @@ validate:
return updatedAuthLink, xerrors.Errorf("update external auth link: %w", err)
}
externalAuthLink = updatedAuthLink
+
+ // Update the associated users github.com username if the token is for github.com.
+ if IsGithubDotComURL(c.AuthCodeURL("")) && user != nil {
+ err = db.UpdateUserGithubComUserID(ctx, database.UpdateUserGithubComUserIDParams{
+ ID: externalAuthLink.UserID,
+ GithubComUserID: sql.NullInt64{
+ Int64: user.ID,
+ Valid: true,
+ },
+ })
+ if err != nil {
+ return externalAuthLink, xerrors.Errorf("update user github com user id: %w", err)
+ }
+ }
}
+
return externalAuthLink, nil
}
@@ -233,6 +248,7 @@ func (c *Config) ValidateToken(ctx context.Context, link *oauth2.Token) (bool, *
err = json.NewDecoder(res.Body).Decode(&ghUser)
if err == nil {
user = &codersdk.ExternalAuthUser{
+ ID: ghUser.GetID(),
Login: ghUser.GetLogin(),
AvatarURL: ghUser.GetAvatarURL(),
ProfileURL: ghUser.GetHTMLURL(),
@@ -291,6 +307,7 @@ func (c *Config) AppInstallations(ctx context.Context, token string) ([]codersdk
ID: int(installation.GetID()),
ConfigureURL: installation.GetHTMLURL(),
Account: codersdk.ExternalAuthUser{
+ ID: account.GetID(),
Login: account.GetLogin(),
AvatarURL: account.GetAvatarURL(),
ProfileURL: account.GetHTMLURL(),
@@ -947,3 +964,13 @@ type roundTripper func(req *http.Request) (*http.Response, error)
func (r roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
return r(req)
}
+
+// IsGithubDotComURL returns true if the given URL is a github.com URL.
+func IsGithubDotComURL(str string) bool {
+ str = strings.ToLower(str)
+ ghURL, err := url.Parse(str)
+ if err != nil {
+ return false
+ }
+ return ghURL.Host == "github.com"
+}
diff --git a/coderd/telemetry/telemetry.go b/coderd/telemetry/telemetry.go
index f269e856067c5..c89276a2ffa28 100644
--- a/coderd/telemetry/telemetry.go
+++ b/coderd/telemetry/telemetry.go
@@ -660,11 +660,12 @@ func ConvertUser(dbUser database.User) User {
emailHashed = fmt.Sprintf("%x%s", hash[:], dbUser.Email[atSymbol:])
}
return User{
- ID: dbUser.ID,
- EmailHashed: emailHashed,
- RBACRoles: dbUser.RBACRoles,
- CreatedAt: dbUser.CreatedAt,
- Status: dbUser.Status,
+ ID: dbUser.ID,
+ EmailHashed: emailHashed,
+ RBACRoles: dbUser.RBACRoles,
+ CreatedAt: dbUser.CreatedAt,
+ Status: dbUser.Status,
+ GithubComUserID: dbUser.GithubComUserID.Int64,
}
}
@@ -836,10 +837,11 @@ type User struct {
ID uuid.UUID `json:"id"`
CreatedAt time.Time `json:"created_at"`
// Email is only filled in for the first/admin user!
- Email *string `json:"email"`
- EmailHashed string `json:"email_hashed"`
- RBACRoles []string `json:"rbac_roles"`
- Status database.UserStatus `json:"status"`
+ Email *string `json:"email"`
+ EmailHashed string `json:"email_hashed"`
+ RBACRoles []string `json:"rbac_roles"`
+ Status database.UserStatus `json:"status"`
+ GithubComUserID int64 `json:"github_com_user_id"`
}
type Group struct {
diff --git a/coderd/userauth.go b/coderd/userauth.go
index 303f8a3473bea..f876bf7686341 100644
--- a/coderd/userauth.go
+++ b/coderd/userauth.go
@@ -31,6 +31,7 @@ import (
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbtime"
+ "github.com/coder/coder/v2/coderd/externalauth"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/httpmw"
"github.com/coder/coder/v2/coderd/promoauth"
@@ -661,7 +662,7 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) {
}).SetInitAuditRequest(func(params *audit.RequestParams) (*audit.Request[database.User], func()) {
return audit.InitRequest[database.User](rw, params)
})
- cookies, key, err := api.oauthLogin(r, params)
+ cookies, user, key, err := api.oauthLogin(r, params)
defer params.CommitAuditLogs()
var httpErr httpError
if xerrors.As(err, &httpErr) {
@@ -676,6 +677,25 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) {
})
return
}
+ // If the user is logging in with github.com we update their associated
+ // GitHub user ID to the new one.
+ if externalauth.IsGithubDotComURL(api.GithubOAuth2Config.AuthCodeURL("")) && user.GithubComUserID.Int64 != ghUser.GetID() {
+ err = api.Database.UpdateUserGithubComUserID(ctx, database.UpdateUserGithubComUserIDParams{
+ ID: user.ID,
+ GithubComUserID: sql.NullInt64{
+ Int64: ghUser.GetID(),
+ Valid: true,
+ },
+ })
+ if err != nil {
+ logger.Error(ctx, "oauth2: unable to update user github id", slog.F("user", user.Username), slog.Error(err))
+ httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
+ Message: "Failed to update user GitHub ID.",
+ Detail: err.Error(),
+ })
+ return
+ }
+ }
aReq.New = key
aReq.UserID = key.UserID
@@ -1030,7 +1050,7 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
}).SetInitAuditRequest(func(params *audit.RequestParams) (*audit.Request[database.User], func()) {
return audit.InitRequest[database.User](rw, params)
})
- cookies, key, err := api.oauthLogin(r, params)
+ cookies, user, key, err := api.oauthLogin(r, params)
defer params.CommitAuditLogs()
var httpErr httpError
if xerrors.As(err, &httpErr) {
@@ -1320,7 +1340,7 @@ func (e httpError) Error() string {
return e.msg
}
-func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.Cookie, database.APIKey, error) {
+func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.Cookie, database.User, database.APIKey, error) {
var (
ctx = r.Context()
user database.User
@@ -1610,7 +1630,7 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C
return nil
}, nil)
if err != nil {
- return nil, database.APIKey{}, xerrors.Errorf("in tx: %w", err)
+ return nil, database.User{}, database.APIKey{}, xerrors.Errorf("in tx: %w", err)
}
var key database.APIKey
@@ -1647,13 +1667,13 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C
RemoteAddr: r.RemoteAddr,
})
if err != nil {
- return nil, database.APIKey{}, xerrors.Errorf("create API key: %w", err)
+ return nil, database.User{}, database.APIKey{}, xerrors.Errorf("create API key: %w", err)
}
cookies = append(cookies, cookie)
key = *newKey
}
- return cookies, key, nil
+ return cookies, user, key, nil
}
// convertUserToOauth will convert a user from password base loginType to
diff --git a/codersdk/externalauth.go b/codersdk/externalauth.go
index 49e1a8f262be5..475c55b91bed3 100644
--- a/codersdk/externalauth.go
+++ b/codersdk/externalauth.go
@@ -103,6 +103,7 @@ type ExternalAuthAppInstallation struct {
}
type ExternalAuthUser struct {
+ ID int64 `json:"id"`
Login string `json:"login"`
AvatarURL string `json:"avatar_url"`
ProfileURL string `json:"profile_url"`
diff --git a/docs/admin/audit-logs.md b/docs/admin/audit-logs.md
index 87f07cf243125..a6f8e4e5117da 100644
--- a/docs/admin/audit-logs.md
+++ b/docs/admin/audit-logs.md
@@ -24,7 +24,7 @@ We track the following resources:
| Organization
|
Field | Tracked |
---|
created_at | false |
description | true |
display_name | true |
icon | true |
id | false |
is_default | true |
name | true |
updated_at | true |
|
| Template
write, delete | Field | Tracked |
---|
active_version_id | true |
activity_bump | true |
allow_user_autostart | true |
allow_user_autostop | true |
allow_user_cancel_workspace_jobs | true |
autostart_block_days_of_week | true |
autostop_requirement_days_of_week | true |
autostop_requirement_weeks | true |
created_at | false |
created_by | true |
created_by_avatar_url | false |
created_by_username | false |
default_ttl | true |
deleted | false |
deprecated | true |
description | true |
display_name | true |
failure_ttl | true |
group_acl | true |
icon | true |
id | true |
max_port_sharing_level | true |
name | true |
organization_display_name | false |
organization_icon | false |
organization_id | false |
organization_name | false |
provisioner | true |
require_active_version | true |
time_til_dormant | true |
time_til_dormant_autodelete | true |
updated_at | false |
user_acl | true |
|
| TemplateVersion
create, write | Field | Tracked |
---|
archived | true |
created_at | false |
created_by | true |
created_by_avatar_url | false |
created_by_username | false |
external_auth_providers | false |
id | true |
job_id | false |
message | false |
name | true |
organization_id | false |
readme | true |
template_id | true |
updated_at | false |
|
-| User
create, write, delete | Field | Tracked |
---|
avatar_url | false |
created_at | false |
deleted | true |
email | true |
hashed_password | true |
id | true |
last_seen_at | false |
login_type | true |
name | true |
quiet_hours_schedule | true |
rbac_roles | true |
status | true |
theme_preference | false |
updated_at | false |
username | true |
|
+| User
create, write, delete | Field | Tracked |
---|
avatar_url | false |
created_at | false |
deleted | true |
email | true |
github_com_user_id | false |
hashed_password | true |
id | true |
last_seen_at | false |
login_type | true |
name | true |
quiet_hours_schedule | true |
rbac_roles | true |
status | true |
theme_preference | false |
updated_at | false |
username | true |
|
| Workspace
create, write, delete | Field | Tracked |
---|
automatic_updates | true |
autostart_schedule | true |
created_at | false |
deleted | false |
deleting_at | true |
dormant_at | true |
favorite | true |
id | true |
last_used_at | false |
name | true |
organization_id | false |
owner_id | true |
template_id | true |
ttl | true |
updated_at | false |
|
| WorkspaceBuild
start, stop | Field | Tracked |
---|
build_number | false |
created_at | false |
daily_cost | false |
deadline | false |
id | false |
initiator_by_avatar_url | false |
initiator_by_username | false |
initiator_id | false |
job_id | false |
max_deadline | false |
provisioner_state | false |
reason | false |
template_version_id | true |
transition | false |
updated_at | false |
workspace_id | false |
|
| WorkspaceProxy
| Field | Tracked |
---|
created_at | true |
deleted | false |
derp_enabled | true |
derp_only | true |
display_name | true |
icon | true |
id | true |
name | true |
region_id | true |
token_hashed_secret | true |
updated_at | false |
url | true |
version | true |
wildcard_hostname | true |
|
diff --git a/docs/api/git.md b/docs/api/git.md
index 71a0d2921f5fa..929ab3e868b8f 100644
--- a/docs/api/git.md
+++ b/docs/api/git.md
@@ -71,6 +71,7 @@ curl -X GET http://coder-server:8080/api/v2/external-auth/{externalauth} \
{
"account": {
"avatar_url": "string",
+ "id": 0,
"login": "string",
"name": "string",
"profile_url": "string"
@@ -81,6 +82,7 @@ curl -X GET http://coder-server:8080/api/v2/external-auth/{externalauth} \
],
"user": {
"avatar_url": "string",
+ "id": 0,
"login": "string",
"name": "string",
"profile_url": "string"
diff --git a/docs/api/schemas.md b/docs/api/schemas.md
index c1ec9979a0a13..53ad820daf60c 100644
--- a/docs/api/schemas.md
+++ b/docs/api/schemas.md
@@ -2521,6 +2521,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
{
"account": {
"avatar_url": "string",
+ "id": 0,
"login": "string",
"name": "string",
"profile_url": "string"
@@ -2531,6 +2532,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
],
"user": {
"avatar_url": "string",
+ "id": 0,
"login": "string",
"name": "string",
"profile_url": "string"
@@ -2556,6 +2558,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
{
"account": {
"avatar_url": "string",
+ "id": 0,
"login": "string",
"name": "string",
"profile_url": "string"
@@ -2669,6 +2672,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
```json
{
"avatar_url": "string",
+ "id": 0,
"login": "string",
"name": "string",
"profile_url": "string"
@@ -2677,12 +2681,13 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
### Properties
-| Name | Type | Required | Restrictions | Description |
-| ------------- | ------ | -------- | ------------ | ----------- |
-| `avatar_url` | string | false | | |
-| `login` | string | false | | |
-| `name` | string | false | | |
-| `profile_url` | string | false | | |
+| Name | Type | Required | Restrictions | Description |
+| ------------- | ------- | -------- | ------------ | ----------- |
+| `avatar_url` | string | false | | |
+| `id` | integer | false | | |
+| `login` | string | false | | |
+| `name` | string | false | | |
+| `profile_url` | string | false | | |
## codersdk.Feature
diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go
index 0a3608dae7169..dcecd88971af8 100644
--- a/enterprise/audit/table.go
+++ b/enterprise/audit/table.go
@@ -144,6 +144,7 @@ var auditableResourcesTypes = map[any]map[string]Action{
"quiet_hours_schedule": ActionTrack,
"theme_preference": ActionIgnore,
"name": ActionTrack,
+ "github_com_user_id": ActionIgnore,
},
&database.Workspace{}: {
"id": ActionTrack,
diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts
index 0ad30e1310cff..08bd69cd58448 100644
--- a/site/src/api/typesGenerated.ts
+++ b/site/src/api/typesGenerated.ts
@@ -572,6 +572,7 @@ export interface ExternalAuthLinkProvider {
// From codersdk/externalauth.go
export interface ExternalAuthUser {
+ readonly id: number;
readonly login: string;
readonly avatar_url: string;
readonly profile_url: string;
diff --git a/site/src/pages/ExternalAuthPage/ExternalAuthPageView.stories.tsx b/site/src/pages/ExternalAuthPage/ExternalAuthPageView.stories.tsx
index 676597bdb4617..4b1d07ddb2734 100644
--- a/site/src/pages/ExternalAuthPage/ExternalAuthPageView.stories.tsx
+++ b/site/src/pages/ExternalAuthPage/ExternalAuthPageView.stories.tsx
@@ -22,6 +22,7 @@ WebAuthenticated.args = {
app_installable: false,
display_name: "BitBucket",
user: {
+ id: 0,
avatar_url: "https://avatars.githubusercontent.com/u/7122116?v=4",
login: "kylecarbs",
name: "Kyle Carberry",
@@ -104,6 +105,7 @@ DeviceAuthenticatedNotInstalled.args = {
app_install_url: "https://example.com",
app_installable: true,
user: {
+ id: 0,
avatar_url: "https://avatars.githubusercontent.com/u/7122116?v=4",
login: "kylecarbs",
name: "Kyle Carberry",
@@ -123,6 +125,7 @@ DeviceAuthenticatedInstalled.args = {
configure_url: "https://example.com",
id: 1,
account: {
+ id: 0,
avatar_url: "https://github.com/coder.png",
login: "coder",
name: "Coder",
@@ -133,6 +136,7 @@ DeviceAuthenticatedInstalled.args = {
app_install_url: "https://example.com",
app_installable: true,
user: {
+ id: 0,
avatar_url: "https://avatars.githubusercontent.com/u/7122116?v=4",
login: "kylecarbs",
name: "Kyle Carberry",