From 841a99c8334ca1655737c14ade3ac7670e5daf6e Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Fri, 23 Sep 2022 21:21:24 +0000 Subject: [PATCH 1/5] Add database --- coderd/database/dump.sql | 3 +- .../migrations/000053_last_seen_at.down.sql | 3 + .../migrations/000053_last_seen_at.up.sql | 2 + coderd/database/models.go | 1 + coderd/database/querier.go | 1 + coderd/database/queries.sql.go | 60 ++++++++++++++++--- coderd/database/queries/users.sql | 9 +++ 7 files changed, 70 insertions(+), 9 deletions(-) create mode 100644 coderd/database/migrations/000053_last_seen_at.down.sql create mode 100644 coderd/database/migrations/000053_last_seen_at.up.sql diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 5f39aa2d24d33..6bf0961b37356 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -317,7 +317,8 @@ CREATE TABLE users ( rbac_roles text[] DEFAULT '{}'::text[] NOT NULL, login_type login_type DEFAULT 'password'::public.login_type NOT NULL, avatar_url text, - deleted boolean DEFAULT false NOT NULL + deleted boolean DEFAULT false NOT NULL, + last_seen_at timestamp without time zone DEFAULT '0001-01-01 00:00:00'::timestamp without time zone NOT NULL ); CREATE TABLE workspace_agents ( diff --git a/coderd/database/migrations/000053_last_seen_at.down.sql b/coderd/database/migrations/000053_last_seen_at.down.sql new file mode 100644 index 0000000000000..6aa86f8684d02 --- /dev/null +++ b/coderd/database/migrations/000053_last_seen_at.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE ONLY users + DROP COLUMN last_seen_at; + diff --git a/coderd/database/migrations/000053_last_seen_at.up.sql b/coderd/database/migrations/000053_last_seen_at.up.sql new file mode 100644 index 0000000000000..14c370a1e7c63 --- /dev/null +++ b/coderd/database/migrations/000053_last_seen_at.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE ONLY users + ADD COLUMN last_seen_at timestamp NOT NULL DEFAULT '0001-01-01 00:00:00+00:00'; diff --git a/coderd/database/models.go b/coderd/database/models.go index deb1704c6eacb..bfd7d3f7af1ad 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -549,6 +549,7 @@ type User struct { LoginType LoginType `db:"login_type" json:"login_type"` AvatarURL sql.NullString `db:"avatar_url" json:"avatar_url"` Deleted bool `db:"deleted" json:"deleted"` + LastSeenAt time.Time `db:"last_seen_at" json:"last_seen_at"` } type UserLink struct { diff --git a/coderd/database/querier.go b/coderd/database/querier.go index caf3f4ad27b55..110c5d4410efe 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -141,6 +141,7 @@ type querier interface { UpdateTemplateVersionDescriptionByJobID(ctx context.Context, arg UpdateTemplateVersionDescriptionByJobIDParams) error UpdateUserDeletedByID(ctx context.Context, arg UpdateUserDeletedByIDParams) error UpdateUserHashedPassword(ctx context.Context, arg UpdateUserHashedPasswordParams) error + UpdateUserLastSeenAt(ctx context.Context, arg UpdateUserLastSeenAtParams) (User, error) UpdateUserLink(ctx context.Context, arg UpdateUserLinkParams) (UserLink, error) UpdateUserLinkedID(ctx context.Context, arg UpdateUserLinkedIDParams) (UserLink, error) UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) (User, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 424d72f4efddb..ae7b420a054af 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -3067,7 +3067,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 + id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at FROM users WHERE @@ -3098,13 +3098,14 @@ func (q *sqlQuerier) GetUserByEmailOrUsername(ctx context.Context, arg GetUserBy &i.LoginType, &i.AvatarURL, &i.Deleted, + &i.LastSeenAt, ) 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 + id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at FROM users WHERE @@ -3128,6 +3129,7 @@ func (q *sqlQuerier) GetUserByID(ctx context.Context, id uuid.UUID) (User, error &i.LoginType, &i.AvatarURL, &i.Deleted, + &i.LastSeenAt, ) return i, err } @@ -3148,7 +3150,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 + id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at FROM users WHERE @@ -3246,6 +3248,7 @@ func (q *sqlQuerier) GetUsers(ctx context.Context, arg GetUsersParams) ([]User, &i.LoginType, &i.AvatarURL, &i.Deleted, + &i.LastSeenAt, ); err != nil { return nil, err } @@ -3261,7 +3264,7 @@ func (q *sqlQuerier) GetUsers(ctx context.Context, arg GetUsersParams) ([]User, } const getUsersByIDs = `-- name: GetUsersByIDs :many -SELECT id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted FROM users WHERE id = ANY($1 :: uuid [ ]) AND deleted = $2 +SELECT id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at FROM users WHERE id = ANY($1 :: uuid [ ]) AND deleted = $2 ` type GetUsersByIDsParams struct { @@ -3290,6 +3293,7 @@ func (q *sqlQuerier) GetUsersByIDs(ctx context.Context, arg GetUsersByIDsParams) &i.LoginType, &i.AvatarURL, &i.Deleted, + &i.LastSeenAt, ); err != nil { return nil, err } @@ -3317,7 +3321,7 @@ INSERT INTO login_type ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted + ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at ` type InsertUserParams struct { @@ -3355,6 +3359,7 @@ func (q *sqlQuerier) InsertUser(ctx context.Context, arg InsertUserParams) (User &i.LoginType, &i.AvatarURL, &i.Deleted, + &i.LastSeenAt, ) return i, err } @@ -3397,6 +3402,42 @@ func (q *sqlQuerier) UpdateUserHashedPassword(ctx context.Context, arg UpdateUse return err } +const updateUserLastSeenAt = `-- name: UpdateUserLastSeenAt :one +UPDATE + users +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 +` + +type UpdateUserLastSeenAtParams struct { + ID uuid.UUID `db:"id" json:"id"` + LastSeenAt time.Time `db:"last_seen_at" json:"last_seen_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` +} + +func (q *sqlQuerier) UpdateUserLastSeenAt(ctx context.Context, arg UpdateUserLastSeenAtParams) (User, error) { + row := q.db.QueryRowContext(ctx, updateUserLastSeenAt, arg.ID, arg.LastSeenAt, arg.UpdatedAt) + var i User + err := row.Scan( + &i.ID, + &i.Email, + &i.Username, + &i.HashedPassword, + &i.CreatedAt, + &i.UpdatedAt, + &i.Status, + pq.Array(&i.RBACRoles), + &i.LoginType, + &i.AvatarURL, + &i.Deleted, + &i.LastSeenAt, + ) + return i, err +} + const updateUserProfile = `-- name: UpdateUserProfile :one UPDATE users @@ -3406,7 +3447,7 @@ SET avatar_url = $4, updated_at = $5 WHERE - id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted + id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at ` type UpdateUserProfileParams struct { @@ -3438,6 +3479,7 @@ func (q *sqlQuerier) UpdateUserProfile(ctx context.Context, arg UpdateUserProfil &i.LoginType, &i.AvatarURL, &i.Deleted, + &i.LastSeenAt, ) return i, err } @@ -3450,7 +3492,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 +RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at ` type UpdateUserRolesParams struct { @@ -3473,6 +3515,7 @@ func (q *sqlQuerier) UpdateUserRoles(ctx context.Context, arg UpdateUserRolesPar &i.LoginType, &i.AvatarURL, &i.Deleted, + &i.LastSeenAt, ) return i, err } @@ -3484,7 +3527,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 + id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at ` type UpdateUserStatusParams struct { @@ -3508,6 +3551,7 @@ func (q *sqlQuerier) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusP &i.LoginType, &i.AvatarURL, &i.Deleted, + &i.LastSeenAt, ) return i, err } diff --git a/coderd/database/queries/users.sql b/coderd/database/queries/users.sql index 59332c5a57963..fd47afa1a1a63 100644 --- a/coderd/database/queries/users.sql +++ b/coderd/database/queries/users.sql @@ -157,6 +157,15 @@ SET WHERE id = $1 RETURNING *; +-- name: UpdateUserLastSeenAt :one +UPDATE + users +SET + last_seen_at = $2, + updated_at = $3 +WHERE + id = $1 RETURNING *; + -- name: GetAuthorizationUserRoles :one -- This function returns roles for authorization purposes. Implied member roles From a89dee94c9b0982c33a77b71030f4b137d6386c1 Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Sat, 24 Sep 2022 18:43:47 +0000 Subject: [PATCH 2/5] Write tests --- coderd/database/databasefake/databasefake.go | 16 +++++++++++ coderd/httpmw/apikey.go | 13 +++++++++ coderd/users.go | 1 + coderd/users_test.go | 29 ++++++++++++++++++++ codersdk/users.go | 10 ++++--- 5 files changed, 65 insertions(+), 4 deletions(-) diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index c502780145ccd..f0cdee99f254b 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -1956,6 +1956,22 @@ func (q *fakeQuerier) UpdateUserStatus(_ context.Context, arg database.UpdateUse return database.User{}, sql.ErrNoRows } +func (q *fakeQuerier) UpdateUserLastSeenAt(_ context.Context, arg database.UpdateUserLastSeenAtParams) (database.User, error) { + q.mutex.Lock() + defer q.mutex.Unlock() + + for index, user := range q.users { + if user.ID != arg.ID { + continue + } + user.LastSeenAt = arg.LastSeenAt + user.UpdatedAt = arg.UpdatedAt + q.users[index] = user + return user, nil + } + return database.User{}, sql.ErrNoRows +} + func (q *fakeQuerier) UpdateUserHashedPassword(_ context.Context, arg database.UpdateUserHashedPasswordParams) error { q.mutex.Lock() defer q.mutex.Unlock() diff --git a/coderd/httpmw/apikey.go b/coderd/httpmw/apikey.go index 395204f1efc33..8e7a0c9db504e 100644 --- a/coderd/httpmw/apikey.go +++ b/coderd/httpmw/apikey.go @@ -317,6 +317,19 @@ func ExtractAPIKey(cfg ExtractAPIKeyConfig) func(http.Handler) http.Handler { return } } + + _, err = cfg.DB.UpdateUserLastSeenAt(ctx, database.UpdateUserLastSeenAtParams{ + ID: key.UserID, + LastSeenAt: database.Now(), + UpdatedAt: database.Now(), + }) + if err != nil { + write(http.StatusInternalServerError, codersdk.Response{ + Message: internalErrorMessage, + Detail: fmt.Sprintf("update user last_seen_at: %s", err.Error()), + }) + return + } } // If the key is valid, we also fetch the user roles and status. diff --git a/coderd/users.go b/coderd/users.go index 5441bbafa23fc..9550c72e77446 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -1210,6 +1210,7 @@ func convertUser(user database.User, organizationIDs []uuid.UUID) codersdk.User ID: user.ID, Email: user.Email, CreatedAt: user.CreatedAt, + LastSeenAt: user.LastSeenAt, Username: user.Username, Status: codersdk.UserStatus(user.Status), OrganizationIDs: organizationIDs, diff --git a/coderd/users_test.go b/coderd/users_test.go index e23bbc78f381f..6477b6911361b 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -65,6 +65,35 @@ func TestFirstUser(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client) }) + t.Run("LastSeenAt", func(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + client := coderdtest.New(t, nil) + firstUserResp := coderdtest.CreateFirstUser(t, client) + + firstUser, err := client.User(ctx, firstUserResp.UserID.String()) + require.NoError(t, err) + + _ = coderdtest.CreateAnotherUser(t, client, firstUserResp.OrganizationID) + + allUsers, err := client.Users(ctx, codersdk.UsersRequest{}) + require.NoError(t, err) + + require.Len(t, allUsers, 2) + + // We sent the "GET Users" request with the first user, but the second user + // should be Never since they haven't performed a request. + for _, user := range allUsers { + if user.ID == firstUser.ID { + require.WithinDuration(t, firstUser.LastSeenAt, database.Now(), testutil.WaitShort) + } else { + require.Zero(t, user.LastSeenAt) + } + } + }) + t.Run("AutoImportsTemplates", func(t *testing.T) { t.Parallel() diff --git a/codersdk/users.go b/codersdk/users.go index 6dae49c7c1a6b..4fce8a3a47bfe 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -43,10 +43,12 @@ type UsersRequest struct { // User represents a user in Coder. type User struct { - ID uuid.UUID `json:"id" validate:"required" table:"id"` - Username string `json:"username" validate:"required" table:"username"` - Email string `json:"email" validate:"required" table:"email"` - CreatedAt time.Time `json:"created_at" validate:"required" table:"created at"` + ID uuid.UUID `json:"id" validate:"required" table:"id"` + Username string `json:"username" validate:"required" table:"username"` + Email string `json:"email" validate:"required" table:"email"` + CreatedAt time.Time `json:"created_at" validate:"required" table:"created at"` + LastSeenAt time.Time `json:"last_seen_at"` + Status UserStatus `json:"status" table:"status"` OrganizationIDs []uuid.UUID `json:"organization_ids"` Roles []Role `json:"roles"` From bf7f5ab8de8892dc2a20083eb8ed51e5377ac9e7 Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Sat, 24 Sep 2022 19:14:17 +0000 Subject: [PATCH 3/5] Complete frontend --- site/src/api/typesGenerated.ts | 1 + .../WorkspaceLastUsed.tsx => LastUsed/LastUsed.tsx} | 4 ++-- site/src/components/UsersTable/UsersTable.tsx | 2 ++ site/src/components/UsersTable/UsersTableBody.tsx | 4 ++++ site/src/components/WorkspacesTable/WorkspacesRow.tsx | 4 ++-- site/src/testHelpers/entities.ts | 4 ++++ 6 files changed, 15 insertions(+), 4 deletions(-) rename site/src/components/{WorkspacesTable/WorkspaceLastUsed.tsx => LastUsed/LastUsed.tsx} (93%) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index c81b84efac2a6..fdacbc6dacadf 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -510,6 +510,7 @@ export interface User { readonly username: string readonly email: string readonly created_at: string + readonly last_seen_at: string readonly status: UserStatus readonly organization_ids: string[] readonly roles: Role[] diff --git a/site/src/components/WorkspacesTable/WorkspaceLastUsed.tsx b/site/src/components/LastUsed/LastUsed.tsx similarity index 93% rename from site/src/components/WorkspacesTable/WorkspaceLastUsed.tsx rename to site/src/components/LastUsed/LastUsed.tsx index 90416aa5550dc..6466dc2291447 100644 --- a/site/src/components/WorkspacesTable/WorkspaceLastUsed.tsx +++ b/site/src/components/LastUsed/LastUsed.tsx @@ -8,11 +8,11 @@ import { colors } from "theme/colors" dayjs.extend(relativeTime) -interface WorkspaceLastUsedProps { +interface LastUsedProps { lastUsedAt: string } -export const WorkspaceLastUsed: FC = ({ lastUsedAt }) => { +export const LastUsed: FC = ({ lastUsedAt }) => { const theme: Theme = useTheme() const styles = useStyles() diff --git a/site/src/components/UsersTable/UsersTable.tsx b/site/src/components/UsersTable/UsersTable.tsx index f1406286f0ec5..869916b1ffa1f 100644 --- a/site/src/components/UsersTable/UsersTable.tsx +++ b/site/src/components/UsersTable/UsersTable.tsx @@ -14,6 +14,7 @@ export const Language = { usernameLabel: "User", rolesLabel: "Roles", statusLabel: "Status", + lastSeenLabel: "Last Seen", } export interface UsersTableProps { @@ -50,6 +51,7 @@ export const UsersTable: FC> = ({ {Language.usernameLabel} {Language.statusLabel} + {Language.lastSeenLabel} {Language.rolesLabel} diff --git a/site/src/components/UsersTable/UsersTableBody.tsx b/site/src/components/UsersTable/UsersTableBody.tsx index 157dfd4cee316..618795ee316fe 100644 --- a/site/src/components/UsersTable/UsersTableBody.tsx +++ b/site/src/components/UsersTable/UsersTableBody.tsx @@ -2,6 +2,7 @@ import Box from "@material-ui/core/Box" import { makeStyles } from "@material-ui/core/styles" import TableCell from "@material-ui/core/TableCell" import TableRow from "@material-ui/core/TableRow" +import { LastUsed } from "components/LastUsed/LastUsed" import { FC } from "react" import * as TypesGen from "../../api/typesGenerated" import { combineClasses } from "../../util/combineClasses" @@ -101,6 +102,9 @@ export const UsersTableBody: FC> = > {user.status} + + + {canEditUsers ? ( - + diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index c5392745ed5f8..177e79842e22d 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -71,6 +71,7 @@ export const MockUser: TypesGen.User = { organization_ids: ["fc0774ce-cc9e-48d4-80ae-88f7a4d4a8b0"], roles: [MockOwnerRole], avatar_url: "https://github.com/coder.png", + last_seen_at: "", } export const MockUserAdmin: TypesGen.User = { @@ -82,6 +83,7 @@ export const MockUserAdmin: TypesGen.User = { organization_ids: ["fc0774ce-cc9e-48d4-80ae-88f7a4d4a8b0"], roles: [MockUserAdminRole], avatar_url: "", + last_seen_at: "", } export const MockUser2: TypesGen.User = { @@ -93,6 +95,7 @@ export const MockUser2: TypesGen.User = { organization_ids: ["fc0774ce-cc9e-48d4-80ae-88f7a4d4a8b0"], roles: [], avatar_url: "", + last_seen_at: "2022-09-14T19:12:21Z", } export const SuspendedMockUser: TypesGen.User = { @@ -104,6 +107,7 @@ export const SuspendedMockUser: TypesGen.User = { organization_ids: ["fc0774ce-cc9e-48d4-80ae-88f7a4d4a8b0"], roles: [], avatar_url: "", + last_seen_at: "", } export const MockOrganization: TypesGen.Organization = { From 21fa66badc2c93805461fc8bdbe093c3d6de30be Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Sat, 24 Sep 2022 19:23:14 +0000 Subject: [PATCH 4/5] Complete self review --- coderd/database/migrations/000053_last_seen_at.down.sql | 1 - coderd/httpmw/apikey.go | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/coderd/database/migrations/000053_last_seen_at.down.sql b/coderd/database/migrations/000053_last_seen_at.down.sql index 6aa86f8684d02..e508c28373a03 100644 --- a/coderd/database/migrations/000053_last_seen_at.down.sql +++ b/coderd/database/migrations/000053_last_seen_at.down.sql @@ -1,3 +1,2 @@ ALTER TABLE ONLY users DROP COLUMN last_seen_at; - diff --git a/coderd/httpmw/apikey.go b/coderd/httpmw/apikey.go index 8e7a0c9db504e..1705df3262b83 100644 --- a/coderd/httpmw/apikey.go +++ b/coderd/httpmw/apikey.go @@ -318,6 +318,9 @@ func ExtractAPIKey(cfg ExtractAPIKeyConfig) func(http.Handler) http.Handler { } } + // We only want to update this occasionally to reduce DB write + // load. We update alongside the UserLink and APIKey since it's + // easier on the DB to colocate writes. _, err = cfg.DB.UpdateUserLastSeenAt(ctx, database.UpdateUserLastSeenAtParams{ ID: key.UserID, LastSeenAt: database.Now(), From 0a7f2c81f008ed6ff5527a7834da69bc67169438 Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Mon, 26 Sep 2022 15:20:59 +0000 Subject: [PATCH 5/5] Fix minor bugs --- enterprise/audit/table.go | 1 + site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index eaa2dd1bb9654..62bb463f43292 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -84,6 +84,7 @@ var AuditableResources = auditMap(map[any]map[string]Action{ "rbac_roles": ActionTrack, "login_type": ActionIgnore, "avatar_url": ActionIgnore, + "last_seen_at": ActionIgnore, "deleted": ActionTrack, }, &database.Workspace{}: { diff --git a/site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx b/site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx index 2936cc1ec2c77..0d8624373ba33 100644 --- a/site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx +++ b/site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx @@ -38,6 +38,7 @@ describe("AccountPage", () => { organization_ids: ["123"], roles: [], avatar_url: "", + last_seen_at: new Date().toString(), ...data, }), )