fix: mark users seen when activating on login (#21305)

fixes #21303

Update user last_seen_at when we mark them active on login. This prevents a narrow race where they can be re-marked dormant and fail to log in.
This commit is contained in:
Spike Curtis
2025-12-17 16:49:40 +04:00
committed by GitHub
parent f9087d6feb
commit bd753d9cb9
6 changed files with 43 additions and 26 deletions
+4 -3
View File
@@ -580,9 +580,10 @@ func User(t testing.TB, db database.Store, orig database.User) database.User {
require.NoError(t, err, "insert user")
user, err = db.UpdateUserStatus(genCtx, database.UpdateUserStatusParams{
ID: user.ID,
Status: takeFirst(orig.Status, database.UserStatusActive),
UpdatedAt: dbtime.Now(),
ID: user.ID,
Status: takeFirst(orig.Status, database.UserStatusActive),
UpdatedAt: dbtime.Now(),
UserIsSeen: false,
})
require.NoError(t, err, "insert user")
+13 -5
View File
@@ -17141,19 +17141,27 @@ UPDATE
users
SET
status = $2,
updated_at = $3
updated_at = $3,
-- If the user is logging in, set last_seen_at to updated_at.
last_seen_at = CASE WHEN $4 :: boolean THEN $3 :: timestamptz ELSE last_seen_at END
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, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system
`
type UpdateUserStatusParams struct {
ID uuid.UUID `db:"id" json:"id"`
Status UserStatus `db:"status" json:"status"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
ID uuid.UUID `db:"id" json:"id"`
Status UserStatus `db:"status" json:"status"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
UserIsSeen bool `db:"user_is_seen" json:"user_is_seen"`
}
func (q *sqlQuerier) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusParams) (User, error) {
row := q.db.QueryRowContext(ctx, updateUserStatus, arg.ID, arg.Status, arg.UpdatedAt)
row := q.db.QueryRowContext(ctx, updateUserStatus,
arg.ID,
arg.Status,
arg.UpdatedAt,
arg.UserIsSeen,
)
var i User
err := row.Scan(
&i.ID,
+3 -1
View File
@@ -325,7 +325,9 @@ UPDATE
users
SET
status = $2,
updated_at = $3
updated_at = $3,
-- If the user is logging in, set last_seen_at to updated_at.
last_seen_at = CASE WHEN @user_is_seen :: boolean THEN $3 :: timestamptz ELSE last_seen_at END
WHERE
id = $1 RETURNING *;
+8 -6
View File
@@ -648,9 +648,10 @@ func ActivateDormantUser(logger slog.Logger, auditor *atomic.Pointer[audit.Audit
//nolint:gocritic // System needs to update status of the user account (dormant -> active).
newUser, err := db.UpdateUserStatus(dbauthz.AsSystemRestricted(ctx), database.UpdateUserStatusParams{
ID: user.ID,
Status: database.UserStatusActive,
UpdatedAt: dbtime.Now(),
ID: user.ID,
Status: database.UserStatusActive,
UpdatedAt: dbtime.Now(),
UserIsSeen: true,
})
if err != nil {
logger.Error(ctx, "unable to update user status to active", slog.Error(err))
@@ -1799,9 +1800,10 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C
dormantConvertAudit.Old = user
//nolint:gocritic // System needs to update status of the user account (dormant -> active).
user, err = tx.UpdateUserStatus(dbauthz.AsSystemRestricted(ctx), database.UpdateUserStatusParams{
ID: user.ID,
Status: database.UserStatusActive,
UpdatedAt: dbtime.Now(),
ID: user.ID,
Status: database.UserStatusActive,
UpdatedAt: dbtime.Now(),
UserIsSeen: true,
})
if err != nil {
logger.Error(ctx, "unable to update user status to active", slog.Error(err))
+4 -3
View File
@@ -885,9 +885,10 @@ func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseW
}
targetUser, err := api.Database.UpdateUserStatus(ctx, database.UpdateUserStatusParams{
ID: user.ID,
Status: status,
UpdatedAt: dbtime.Now(),
ID: user.ID,
Status: status,
UpdatedAt: dbtime.Now(),
UserIsSeen: false,
})
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{