mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
chore: add github.com user id association (#14045)
* chore: add github.com user id association This will eventually be used to show an indicator in the UI to star the repository if you've been using Coder for a while and have not starred the repo. If you have, we'll never show a thing! * gen * Fix model query * Fix linting * Ignore auditing github.com user id * Add test * Fix gh url var name * Update migration * Update coderd/database/dbauthz/dbauthz.go Co-authored-by: Steven Masley <Emyrk@users.noreply.github.com> * Fix updating to when the token changes * Fix migration --------- Co-authored-by: Steven Masley <Emyrk@users.noreply.github.com>
This commit is contained in:
Generated
+3
@@ -9811,6 +9811,9 @@ const docTemplate = `{
|
||||
"avatar_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"login": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
Generated
+3
@@ -8801,6 +8801,9 @@
|
||||
"avatar_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"login": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Generated
+4
-1
@@ -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,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE users DROP COLUMN github_com_user_id;
|
||||
@@ -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.';
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+26
-6
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -24,7 +24,7 @@ We track the following resources:
|
||||
| Organization<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>false</td></tr><tr><td>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>is_default</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>updated_at</td><td>true</td></tr></tbody></table> |
|
||||
| Template<br><i>write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>active_version_id</td><td>true</td></tr><tr><td>activity_bump</td><td>true</td></tr><tr><td>allow_user_autostart</td><td>true</td></tr><tr><td>allow_user_autostop</td><td>true</td></tr><tr><td>allow_user_cancel_workspace_jobs</td><td>true</td></tr><tr><td>autostart_block_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_weeks</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>default_ttl</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deprecated</td><td>true</td></tr><tr><td>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>failure_ttl</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>max_port_sharing_level</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_display_name</td><td>false</td></tr><tr><td>organization_icon</td><td>false</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>organization_name</td><td>false</td></tr><tr><td>provisioner</td><td>true</td></tr><tr><td>require_active_version</td><td>true</td></tr><tr><td>time_til_dormant</td><td>true</td></tr><tr><td>time_til_dormant_autodelete</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
|
||||
| TemplateVersion<br><i>create, write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>archived</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>external_auth_providers</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>message</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>readme</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
||||
| User<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>avatar_url</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>email</td><td>true</td></tr><tr><td>hashed_password</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_seen_at</td><td>false</td></tr><tr><td>login_type</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>quiet_hours_schedule</td><td>true</td></tr><tr><td>rbac_roles</td><td>true</td></tr><tr><td>status</td><td>true</td></tr><tr><td>theme_preference</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
|
||||
| User<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>avatar_url</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>email</td><td>true</td></tr><tr><td>github_com_user_id</td><td>false</td></tr><tr><td>hashed_password</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_seen_at</td><td>false</td></tr><tr><td>login_type</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>quiet_hours_schedule</td><td>true</td></tr><tr><td>rbac_roles</td><td>true</td></tr><tr><td>status</td><td>true</td></tr><tr><td>theme_preference</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
|
||||
| Workspace<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>automatic_updates</td><td>true</td></tr><tr><td>autostart_schedule</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deleting_at</td><td>true</td></tr><tr><td>dormant_at</td><td>true</td></tr><tr><td>favorite</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>ttl</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
||||
| WorkspaceBuild<br><i>start, stop</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>build_number</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>daily_cost</td><td>false</td></tr><tr><td>deadline</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>initiator_by_avatar_url</td><td>false</td></tr><tr><td>initiator_by_username</td><td>false</td></tr><tr><td>initiator_id</td><td>false</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>max_deadline</td><td>false</td></tr><tr><td>provisioner_state</td><td>false</td></tr><tr><td>reason</td><td>false</td></tr><tr><td>template_version_id</td><td>true</td></tr><tr><td>transition</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>workspace_id</td><td>false</td></tr></tbody></table> |
|
||||
| WorkspaceProxy<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>derp_enabled</td><td>true</td></tr><tr><td>derp_only</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>region_id</td><td>true</td></tr><tr><td>token_hashed_secret</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>url</td><td>true</td></tr><tr><td>version</td><td>true</td></tr><tr><td>wildcard_hostname</td><td>true</td></tr></tbody></table> |
|
||||
|
||||
Generated
+2
@@ -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"
|
||||
|
||||
Generated
+11
-6
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Generated
+1
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user