mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add "dormant" user state (#8644)
This commit is contained in:
@@ -2099,6 +2099,13 @@ func (q *querier) UpdateGroupByID(ctx context.Context, arg database.UpdateGroupB
|
||||
return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateGroupByID)(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateInactiveUsersToDormant(ctx context.Context, lastSeenAfter database.UpdateInactiveUsersToDormantParams) ([]database.UpdateInactiveUsersToDormantRow, error) {
|
||||
if err := q.authorizeContext(ctx, rbac.ActionCreate, rbac.ResourceSystem); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.UpdateInactiveUsersToDormant(ctx, lastSeenAfter)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateMemberRoles(ctx context.Context, arg database.UpdateMemberRolesParams) (database.OrganizationMember, error) {
|
||||
// Authorized fetch will check that the actor has read access to the org member since the org member is returned.
|
||||
member, err := q.GetOrganizationMemberByUserID(ctx, database.GetOrganizationMemberByUserIDParams{
|
||||
|
||||
@@ -3862,7 +3862,7 @@ func (q *FakeQuerier) InsertUser(_ context.Context, arg database.InsertUserParam
|
||||
CreatedAt: arg.CreatedAt,
|
||||
UpdatedAt: arg.UpdatedAt,
|
||||
Username: arg.Username,
|
||||
Status: database.UserStatusActive,
|
||||
Status: database.UserStatusDormant,
|
||||
RBACRoles: arg.RBACRoles,
|
||||
LoginType: arg.LoginType,
|
||||
}
|
||||
@@ -4337,6 +4337,29 @@ func (q *FakeQuerier) UpdateGroupByID(_ context.Context, arg database.UpdateGrou
|
||||
return database.Group{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) UpdateInactiveUsersToDormant(_ context.Context, params database.UpdateInactiveUsersToDormantParams) ([]database.UpdateInactiveUsersToDormantRow, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
var updated []database.UpdateInactiveUsersToDormantRow
|
||||
for index, user := range q.users {
|
||||
if user.Status == database.UserStatusActive && user.LastSeenAt.Before(params.LastSeenAfter) {
|
||||
q.users[index].Status = database.UserStatusDormant
|
||||
q.users[index].UpdatedAt = params.UpdatedAt
|
||||
updated = append(updated, database.UpdateInactiveUsersToDormantRow{
|
||||
ID: user.ID,
|
||||
Email: user.Email,
|
||||
LastSeenAt: user.LastSeenAt,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(updated) == 0 {
|
||||
return nil, sql.ErrNoRows
|
||||
}
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) UpdateMemberRoles(_ context.Context, arg database.UpdateMemberRolesParams) (database.OrganizationMember, error) {
|
||||
if err := validateDatabaseType(arg); err != nil {
|
||||
return database.OrganizationMember{}, err
|
||||
|
||||
@@ -224,6 +224,13 @@ 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: database.UserStatusActive,
|
||||
UpdatedAt: database.Now(),
|
||||
})
|
||||
require.NoError(t, err, "insert user")
|
||||
|
||||
if !orig.LastSeenAt.IsZero() {
|
||||
user, err = db.UpdateUserLastSeenAt(genCtx, database.UpdateUserLastSeenAtParams{
|
||||
ID: user.ID,
|
||||
|
||||
@@ -1313,6 +1313,13 @@ func (m metricsStore) UpdateGroupByID(ctx context.Context, arg database.UpdateGr
|
||||
return group, err
|
||||
}
|
||||
|
||||
func (m metricsStore) UpdateInactiveUsersToDormant(ctx context.Context, lastSeenAfter database.UpdateInactiveUsersToDormantParams) ([]database.UpdateInactiveUsersToDormantRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateInactiveUsersToDormant(ctx, lastSeenAfter)
|
||||
m.queryLatencies.WithLabelValues("UpdateInactiveUsersToDormant").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m metricsStore) UpdateMemberRoles(ctx context.Context, arg database.UpdateMemberRolesParams) (database.OrganizationMember, error) {
|
||||
start := time.Now()
|
||||
member, err := m.s.UpdateMemberRoles(ctx, arg)
|
||||
|
||||
@@ -2775,6 +2775,21 @@ func (mr *MockStoreMockRecorder) UpdateGroupByID(arg0, arg1 interface{}) *gomock
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateGroupByID", reflect.TypeOf((*MockStore)(nil).UpdateGroupByID), arg0, arg1)
|
||||
}
|
||||
|
||||
// UpdateInactiveUsersToDormant mocks base method.
|
||||
func (m *MockStore) UpdateInactiveUsersToDormant(arg0 context.Context, arg1 database.UpdateInactiveUsersToDormantParams) ([]database.UpdateInactiveUsersToDormantRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateInactiveUsersToDormant", arg0, arg1)
|
||||
ret0, _ := ret[0].([]database.UpdateInactiveUsersToDormantRow)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdateInactiveUsersToDormant indicates an expected call of UpdateInactiveUsersToDormant.
|
||||
func (mr *MockStoreMockRecorder) UpdateInactiveUsersToDormant(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateInactiveUsersToDormant", reflect.TypeOf((*MockStore)(nil).UpdateInactiveUsersToDormant), arg0, arg1)
|
||||
}
|
||||
|
||||
// UpdateMemberRoles mocks base method.
|
||||
func (m *MockStore) UpdateMemberRoles(arg0 context.Context, arg1 database.UpdateMemberRolesParams) (database.OrganizationMember, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
Generated
+5
-2
@@ -113,9 +113,12 @@ CREATE TYPE startup_script_behavior AS ENUM (
|
||||
|
||||
CREATE TYPE user_status AS ENUM (
|
||||
'active',
|
||||
'suspended'
|
||||
'suspended',
|
||||
'dormant'
|
||||
);
|
||||
|
||||
COMMENT ON TYPE user_status IS 'Defines the user status: active, dormant, or suspended.';
|
||||
|
||||
CREATE TYPE workspace_agent_lifecycle_state AS ENUM (
|
||||
'created',
|
||||
'starting',
|
||||
@@ -561,7 +564,7 @@ CREATE TABLE users (
|
||||
hashed_password bytea NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
status user_status DEFAULT 'active'::user_status NOT NULL,
|
||||
status user_status DEFAULT 'dormant'::user_status NOT NULL,
|
||||
rbac_roles text[] DEFAULT '{}'::text[] NOT NULL,
|
||||
login_type login_type DEFAULT 'password'::login_type NOT NULL,
|
||||
avatar_url text,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
-- It's not possible to drop enum values from enum types, so the UP has "IF NOT EXISTS"
|
||||
|
||||
UPDATE users SET status = 'active'::user_status WHERE status = 'dormant'::user_status;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TYPE user_status ADD VALUE IF NOT EXISTS 'dormant';
|
||||
COMMENT ON TYPE user_status IS 'Defines the user status: active, dormant, or suspended.';
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE users ALTER COLUMN status SET DEFAULT 'active'::user_status;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE users ALTER COLUMN status SET DEFAULT 'dormant'::user_status;
|
||||
@@ -1032,11 +1032,13 @@ func AllStartupScriptBehaviorValues() []StartupScriptBehavior {
|
||||
}
|
||||
}
|
||||
|
||||
// Defines the user status: active, dormant, or suspended.
|
||||
type UserStatus string
|
||||
|
||||
const (
|
||||
UserStatusActive UserStatus = "active"
|
||||
UserStatusSuspended UserStatus = "suspended"
|
||||
UserStatusDormant UserStatus = "dormant"
|
||||
)
|
||||
|
||||
func (e *UserStatus) Scan(src interface{}) error {
|
||||
@@ -1077,7 +1079,8 @@ func (ns NullUserStatus) Value() (driver.Value, error) {
|
||||
func (e UserStatus) Valid() bool {
|
||||
switch e {
|
||||
case UserStatusActive,
|
||||
UserStatusSuspended:
|
||||
UserStatusSuspended,
|
||||
UserStatusDormant:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -1087,6 +1090,7 @@ func AllUserStatusValues() []UserStatus {
|
||||
return []UserStatus{
|
||||
UserStatusActive,
|
||||
UserStatusSuspended,
|
||||
UserStatusDormant,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -236,6 +236,7 @@ type sqlcQuerier interface {
|
||||
UpdateGitAuthLink(ctx context.Context, arg UpdateGitAuthLinkParams) (GitAuthLink, error)
|
||||
UpdateGitSSHKey(ctx context.Context, arg UpdateGitSSHKeyParams) (GitSSHKey, error)
|
||||
UpdateGroupByID(ctx context.Context, arg UpdateGroupByIDParams) (Group, error)
|
||||
UpdateInactiveUsersToDormant(ctx context.Context, arg UpdateInactiveUsersToDormantParams) ([]UpdateInactiveUsersToDormantRow, error)
|
||||
UpdateMemberRoles(ctx context.Context, arg UpdateMemberRolesParams) (OrganizationMember, error)
|
||||
UpdateProvisionerJobByID(ctx context.Context, arg UpdateProvisionerJobByIDParams) error
|
||||
UpdateProvisionerJobWithCancelByID(ctx context.Context, arg UpdateProvisionerJobWithCancelByIDParams) error
|
||||
|
||||
@@ -5708,6 +5708,52 @@ func (q *sqlQuerier) InsertUser(ctx context.Context, arg InsertUserParams) (User
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateInactiveUsersToDormant = `-- name: UpdateInactiveUsersToDormant :many
|
||||
UPDATE
|
||||
users
|
||||
SET
|
||||
status = 'dormant'::user_status,
|
||||
updated_at = $1
|
||||
WHERE
|
||||
last_seen_at < $2 :: timestamp
|
||||
AND status = 'active'::user_status
|
||||
RETURNING id, email, last_seen_at
|
||||
`
|
||||
|
||||
type UpdateInactiveUsersToDormantParams struct {
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
LastSeenAfter time.Time `db:"last_seen_after" json:"last_seen_after"`
|
||||
}
|
||||
|
||||
type UpdateInactiveUsersToDormantRow struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
Email string `db:"email" json:"email"`
|
||||
LastSeenAt time.Time `db:"last_seen_at" json:"last_seen_at"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateInactiveUsersToDormant(ctx context.Context, arg UpdateInactiveUsersToDormantParams) ([]UpdateInactiveUsersToDormantRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, updateInactiveUsersToDormant, arg.UpdatedAt, arg.LastSeenAfter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []UpdateInactiveUsersToDormantRow
|
||||
for rows.Next() {
|
||||
var i UpdateInactiveUsersToDormantRow
|
||||
if err := rows.Scan(&i.ID, &i.Email, &i.LastSeenAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const updateUserDeletedByID = `-- name: UpdateUserDeletedByID :exec
|
||||
UPDATE
|
||||
users
|
||||
|
||||
@@ -250,3 +250,15 @@ SET
|
||||
WHERE
|
||||
id = $1
|
||||
RETURNING *;
|
||||
|
||||
|
||||
-- name: UpdateInactiveUsersToDormant :many
|
||||
UPDATE
|
||||
users
|
||||
SET
|
||||
status = 'dormant'::user_status,
|
||||
updated_at = @updated_at
|
||||
WHERE
|
||||
last_seen_at < @last_seen_after :: timestamp
|
||||
AND status = 'active'::user_status
|
||||
RETURNING id, email, last_seen_at;
|
||||
|
||||
Reference in New Issue
Block a user