diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql
index 01c2930f52..a8b1626938 100644
--- a/coderd/database/dump.sql
+++ b/coderd/database/dump.sql
@@ -749,11 +749,14 @@ CREATE TABLE users (
avatar_url text,
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,
- quiet_hours_schedule text DEFAULT ''::text NOT NULL
+ quiet_hours_schedule text DEFAULT ''::text NOT NULL,
+ theme_preference text DEFAULT ''::text NOT NULL
);
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.';
+COMMENT ON COLUMN users.theme_preference IS '"" can be interpreted as "the user does not care", falling back to the default theme';
+
CREATE VIEW visible_users AS
SELECT users.id,
users.username,
diff --git a/coderd/database/migrations/000175_add_user_theme_preference.down.sql b/coderd/database/migrations/000175_add_user_theme_preference.down.sql
new file mode 100644
index 0000000000..159c5d9b1c
--- /dev/null
+++ b/coderd/database/migrations/000175_add_user_theme_preference.down.sql
@@ -0,0 +1 @@
+ALTER TABLE users DROP COLUMN "theme_preference";
diff --git a/coderd/database/migrations/000175_add_user_theme_preference.up.sql b/coderd/database/migrations/000175_add_user_theme_preference.up.sql
new file mode 100644
index 0000000000..cb13138234
--- /dev/null
+++ b/coderd/database/migrations/000175_add_user_theme_preference.up.sql
@@ -0,0 +1,3 @@
+ALTER TABLE users ADD COLUMN "theme_preference" text NOT NULL DEFAULT '';
+
+COMMENT ON COLUMN "users"."theme_preference" IS '"" can be interpreted as "the user does not care", falling back to the default theme';
diff --git a/coderd/database/migrations/create_migration.sh b/coderd/database/migrations/create_migration.sh
index aac112c429..d6c80926a3 100755
--- a/coderd/database/migrations/create_migration.sh
+++ b/coderd/database/migrations/create_migration.sh
@@ -1,9 +1,9 @@
#!/usr/bin/env bash
# Usage:
-# ./create_migration name of migration
-# ./create_migration "name of migration"
-# ./create_migration name_of_migration
+# ./create_migration.sh name of migration
+# ./create_migration.sh "name of migration"
+# ./create_migration.sh name_of_migration
set -euo pipefail
@@ -27,7 +27,7 @@ CREATE TYPE new_logintype AS ENUM (
ALTER TABLE users
ALTER COLUMN login_type DROP DEFAULT, -- if the column has a default, it must be dropped first
- ALTER COLUMN login_type TYPE new_logintype USING (login_type::text::new_logintype), -- converts the old enum until the new enum using text as an intermediary
+ ALTER COLUMN login_type TYPE new_logintype USING (login_type::text::new_logintype), -- converts the old enum into the new enum using text as an intermediary
ALTER COLUMN login_type SET DEFAULT 'password'::new_logintype; -- re-add the default using the new enum
DROP TYPE login_type;
diff --git a/coderd/database/modelmethods.go b/coderd/database/modelmethods.go
index ed5bf76b78..8d15af65aa 100644
--- a/coderd/database/modelmethods.go
+++ b/coderd/database/modelmethods.go
@@ -332,18 +332,19 @@ func ConvertUserRows(rows []GetUsersRow) []User {
users := make([]User, len(rows))
for i, r := range rows {
users[i] = User{
- ID: r.ID,
- Email: r.Email,
- Username: r.Username,
- HashedPassword: r.HashedPassword,
- CreatedAt: r.CreatedAt,
- UpdatedAt: r.UpdatedAt,
- Status: r.Status,
- RBACRoles: r.RBACRoles,
- LoginType: r.LoginType,
- AvatarURL: r.AvatarURL,
- Deleted: r.Deleted,
- LastSeenAt: r.LastSeenAt,
+ ID: r.ID,
+ Email: r.Email,
+ Username: r.Username,
+ HashedPassword: r.HashedPassword,
+ CreatedAt: r.CreatedAt,
+ UpdatedAt: r.UpdatedAt,
+ Status: r.Status,
+ RBACRoles: r.RBACRoles,
+ LoginType: r.LoginType,
+ AvatarURL: r.AvatarURL,
+ Deleted: r.Deleted,
+ LastSeenAt: r.LastSeenAt,
+ ThemePreference: r.ThemePreference,
}
}
diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go
index 8cb7c32d3d..945b02c744 100644
--- a/coderd/database/modelqueries.go
+++ b/coderd/database/modelqueries.go
@@ -316,6 +316,7 @@ func (q *sqlQuerier) GetAuthorizedUsers(ctx context.Context, arg GetUsersParams,
&i.Deleted,
&i.LastSeenAt,
&i.QuietHoursSchedule,
+ &i.ThemePreference,
&i.Count,
); err != nil {
return nil, err
diff --git a/coderd/database/models.go b/coderd/database/models.go
index 4ab748e8d8..19d051aba0 100644
--- a/coderd/database/models.go
+++ b/coderd/database/models.go
@@ -2119,6 +2119,8 @@ type User struct {
LastSeenAt time.Time `db:"last_seen_at" json:"last_seen_at"`
// 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.
QuietHoursSchedule string `db:"quiet_hours_schedule" json:"quiet_hours_schedule"`
+ // "" can be interpreted as "the user does not care", falling back to the default theme
+ ThemePreference string `db:"theme_preference" json:"theme_preference"`
}
type UserLink struct {
diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go
index 951017b0ab..a8931e0d29 100644
--- a/coderd/database/queries.sql.go
+++ b/coderd/database/queries.sql.go
@@ -1300,7 +1300,7 @@ func (q *sqlQuerier) DeleteGroupMembersByOrgAndUser(ctx context.Context, arg Del
const getGroupMembers = `-- name: GetGroupMembers :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.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
FROM
users
LEFT JOIN
@@ -1347,6 +1347,7 @@ func (q *sqlQuerier) GetGroupMembers(ctx context.Context, groupID uuid.UUID) ([]
&i.Deleted,
&i.LastSeenAt,
&i.QuietHoursSchedule,
+ &i.ThemePreference,
); err != nil {
return nil, err
}
@@ -7046,7 +7047,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
+ 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
FROM
users
WHERE
@@ -7078,13 +7079,14 @@ func (q *sqlQuerier) GetUserByEmailOrUsername(ctx context.Context, arg GetUserBy
&i.Deleted,
&i.LastSeenAt,
&i.QuietHoursSchedule,
+ &i.ThemePreference,
)
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
+ 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
FROM
users
WHERE
@@ -7110,6 +7112,7 @@ func (q *sqlQuerier) GetUserByID(ctx context.Context, id uuid.UUID) (User, error
&i.Deleted,
&i.LastSeenAt,
&i.QuietHoursSchedule,
+ &i.ThemePreference,
)
return i, err
}
@@ -7132,7 +7135,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, 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, COUNT(*) OVER() AS count
FROM
users
WHERE
@@ -7229,6 +7232,7 @@ type GetUsersRow struct {
Deleted bool `db:"deleted" json:"deleted"`
LastSeenAt time.Time `db:"last_seen_at" json:"last_seen_at"`
QuietHoursSchedule string `db:"quiet_hours_schedule" json:"quiet_hours_schedule"`
+ ThemePreference string `db:"theme_preference" json:"theme_preference"`
Count int64 `db:"count" json:"count"`
}
@@ -7265,6 +7269,7 @@ func (q *sqlQuerier) GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUse
&i.Deleted,
&i.LastSeenAt,
&i.QuietHoursSchedule,
+ &i.ThemePreference,
&i.Count,
); err != nil {
return nil, err
@@ -7281,7 +7286,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 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 FROM users WHERE id = ANY($1 :: uuid [ ])
`
// This shouldn't check for deleted, because it's frequently used
@@ -7310,6 +7315,7 @@ func (q *sqlQuerier) GetUsersByIDs(ctx context.Context, ids []uuid.UUID) ([]User
&i.Deleted,
&i.LastSeenAt,
&i.QuietHoursSchedule,
+ &i.ThemePreference,
); err != nil {
return nil, err
}
@@ -7337,7 +7343,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, last_seen_at, quiet_hours_schedule
+ ($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, quiet_hours_schedule, theme_preference
`
type InsertUserParams struct {
@@ -7377,6 +7383,7 @@ func (q *sqlQuerier) InsertUser(ctx context.Context, arg InsertUserParams) (User
&i.Deleted,
&i.LastSeenAt,
&i.QuietHoursSchedule,
+ &i.ThemePreference,
)
return i, err
}
@@ -7472,7 +7479,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
+ 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
`
type UpdateUserLastSeenAtParams struct {
@@ -7498,6 +7505,7 @@ func (q *sqlQuerier) UpdateUserLastSeenAt(ctx context.Context, arg UpdateUserLas
&i.Deleted,
&i.LastSeenAt,
&i.QuietHoursSchedule,
+ &i.ThemePreference,
)
return i, err
}
@@ -7515,7 +7523,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
+ 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
`
type UpdateUserLoginTypeParams struct {
@@ -7540,6 +7548,7 @@ func (q *sqlQuerier) UpdateUserLoginType(ctx context.Context, arg UpdateUserLogi
&i.Deleted,
&i.LastSeenAt,
&i.QuietHoursSchedule,
+ &i.ThemePreference,
)
return i, err
}
@@ -7553,7 +7562,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, last_seen_at, quiet_hours_schedule
+ 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
`
type UpdateUserProfileParams struct {
@@ -7587,6 +7596,7 @@ func (q *sqlQuerier) UpdateUserProfile(ctx context.Context, arg UpdateUserProfil
&i.Deleted,
&i.LastSeenAt,
&i.QuietHoursSchedule,
+ &i.ThemePreference,
)
return i, err
}
@@ -7598,7 +7608,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
+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
`
type UpdateUserQuietHoursScheduleParams struct {
@@ -7623,6 +7633,7 @@ func (q *sqlQuerier) UpdateUserQuietHoursSchedule(ctx context.Context, arg Updat
&i.Deleted,
&i.LastSeenAt,
&i.QuietHoursSchedule,
+ &i.ThemePreference,
)
return i, err
}
@@ -7635,7 +7646,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
+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
`
type UpdateUserRolesParams struct {
@@ -7660,6 +7671,7 @@ func (q *sqlQuerier) UpdateUserRoles(ctx context.Context, arg UpdateUserRolesPar
&i.Deleted,
&i.LastSeenAt,
&i.QuietHoursSchedule,
+ &i.ThemePreference,
)
return i, err
}
@@ -7671,7 +7683,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
+ 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
`
type UpdateUserStatusParams struct {
@@ -7697,6 +7709,7 @@ func (q *sqlQuerier) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusP
&i.Deleted,
&i.LastSeenAt,
&i.QuietHoursSchedule,
+ &i.ThemePreference,
)
return i, err
}
diff --git a/docs/admin/audit-logs.md b/docs/admin/audit-logs.md
index 527134f88b..50848cc833 100644
--- a/docs/admin/audit-logs.md
+++ b/docs/admin/audit-logs.md
@@ -18,7 +18,7 @@ We track the following resources:
| License
create, delete |
| Field | Tracked |
|---|
| exp | true |
| id | false |
| jwt | false |
| uploaded_at | true |
| uuid | true |
|
| Template
write, delete | | Field | Tracked |
|---|
| active_version_id | 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_ttl | true |
| name | true |
| organization_id | 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 |
| quiet_hours_schedule | true |
| rbac_roles | true |
| status | true |
| updated_at | false |
| username | true |
|
+| 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 |
| 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 |
| 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/enterprise/audit/table.go b/enterprise/audit/table.go
index dcd775cf4c..fc5257110c 100644
--- a/enterprise/audit/table.go
+++ b/enterprise/audit/table.go
@@ -118,6 +118,7 @@ var auditableResourcesTypes = map[any]map[string]Action{
"last_seen_at": ActionIgnore,
"deleted": ActionTrack,
"quiet_hours_schedule": ActionTrack,
+ "theme_preference": ActionIgnore,
},
&database.Workspace{}: {
"id": ActionTrack,