improve key renewal (#4519)

* improve key renewal

* sqlx and fix

* fix build

* improve everything

* nits

* nits

* update ee ref

* nit
This commit is contained in:
HugoCasa
2024-10-29 09:26:45 +01:00
committed by GitHub
parent ddab1f7c44
commit ec2d00c374
20 changed files with 313 additions and 161 deletions
@@ -1,20 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT created_at FROM metrics WHERE id = 'telemetry' ORDER BY created_at DESC LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "created_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": []
},
"nullable": [
false
]
},
"hash": "19f0abd79372698f378cb6deea3ee6d098a2758d16ede000809bd9a09660b604"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT email, login_type::text, verified, super_admin, name, company, username from password ORDER BY super_admin DESC, email LIMIT $1 OFFSET $2",
"query": "SELECT email, login_type::text, verified, super_admin, name, company, username, NULL::bool as operator_only FROM password ORDER BY super_admin DESC, email LIMIT $1 OFFSET $2",
"describe": {
"columns": [
{
@@ -37,6 +37,11 @@
"ordinal": 6,
"name": "username",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "operator_only",
"type_info": "Bool"
}
],
"parameters": {
@@ -52,8 +57,9 @@
false,
true,
true,
true
true,
null
]
},
"hash": "c2060e8cacef6c3b5ce51ed203a2dbafc18d66f2924d1fe518c6728997647db2"
"hash": "1a4d291c2f239f7b50c116594cebb031862e1a18ad9204e02a0194817db26d6a"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT email, login_type::TEXT, super_admin, verified, name, company, username FROM password WHERE email = $1",
"query": "SELECT email, login_type::TEXT, super_admin, verified, name, company, username, NULL::bool as operator_only FROM password WHERE email = $1",
"describe": {
"columns": [
{
@@ -37,6 +37,11 @@
"ordinal": 6,
"name": "username",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "operator_only",
"type_info": "Bool"
}
],
"parameters": {
@@ -51,8 +56,9 @@
false,
true,
true,
true
true,
null
]
},
"hash": "8813665f8adfcab0daefbac2cc6b50e427dfd1c12d12895451affd307dc59c37"
"hash": "2c14d3a88193f16ad3b8cd590749cb5537995f2499f6cb8f0f316fb62902d542"
}
@@ -0,0 +1,65 @@
{
"db_name": "PostgreSQL",
"query": "WITH active_users AS (SELECT distinct username as email FROM audit WHERE timestamp > NOW() - INTERVAL '1 month' AND (operation = 'users.login' OR operation = 'oauth.login')),\n authors as (SELECT distinct email FROM usr WHERE usr.operator IS false)\n SELECT email, email NOT IN (SELECT email FROM authors) as operator_only, login_type::text, verified, super_admin, name, company, username\n FROM password\n WHERE email IN (SELECT email FROM active_users)\n ORDER BY super_admin DESC\n LIMIT $1 OFFSET $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "email",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "operator_only",
"type_info": "Bool"
},
{
"ordinal": 2,
"name": "login_type",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "verified",
"type_info": "Bool"
},
{
"ordinal": 4,
"name": "super_admin",
"type_info": "Bool"
},
{
"ordinal": 5,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "company",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "username",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Int8",
"Int8"
]
},
"nullable": [
false,
null,
null,
false,
false,
true,
true,
true
]
},
"hash": "2eec077cc9e27d7ccd160cbaac118c321c422705f79e69550bb60f377083bcef"
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT pg_try_advisory_xact_lock($1)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "pg_try_advisory_xact_lock",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
null
]
},
"hash": "6776dc50f184188756ad7fe263b0304333536768527525a43bdd45aedffa3c4f"
}
@@ -1,20 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT created_at FROM metrics WHERE id = 'license_key_renewal' ORDER BY created_at DESC LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "created_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": []
},
"nullable": [
false
]
},
"hash": "9ed77e78e6295c62745ac3ac3b7e5f544f654d7cddc13acb5d3f4fdc12a8875f"
}
+1 -1
View File
@@ -1 +1 @@
ee638ab91623392cdbf6a139f2b54b9b527585ea
f667fcebdb5a2d1c22c1928d6a316ba57bb43d72
+2 -10
View File
@@ -1,16 +1,8 @@
use anyhow::anyhow;
#[cfg(feature = "enterprise")]
use windmill_common::error::{Error, Result};
pub async fn set_license_key(_license_key: String) -> anyhow::Result<()> {
pub async fn set_license_key(_license_key: String) -> () {
// Implementation is not open source
Err(anyhow!("License cannot be set in Windmill CE"))
}
#[cfg(feature = "enterprise")]
pub async fn verify_license_key() -> Result<()> {
pub async fn verify_license_key() -> () {
// Implementation is not open source
Err(Error::InternalErr(
"License always invalid in Windmill CE".to_string(),
))
}
+70 -11
View File
@@ -27,7 +27,7 @@ use uuid::Uuid;
use windmill_api::HTTP_CLIENT;
#[cfg(feature = "enterprise")]
use windmill_common::ee::schedule_key_renewal;
use windmill_common::ee::{maybe_renew_license_key_on_start, LICENSE_KEY_ID, LICENSE_KEY_VALID};
use windmill_common::{
global_settings::{
@@ -307,7 +307,8 @@ async fn windmill_main() -> anyhow::Result<()> {
Mode::Standalone
});
let num_workers = if mode == Mode::Server || mode == Mode::Indexer {
#[allow(unused_mut)]
let mut num_workers = if mode == Mode::Server || mode == Mode::Indexer {
0
} else {
std::env::var("NUM_WORKERS")
@@ -422,6 +423,50 @@ Windmill Community Edition {GIT_VERSION}
display_config(&ENV_SETTINGS);
if let Err(e) = reload_base_url_setting(&db).await {
tracing::error!("Error loading base url: {:?}", e)
}
if let Err(e) = reload_critical_error_channels_setting(&db).await {
tracing::error!("Could loading critical error emails setting: {:?}", e);
}
#[cfg(feature = "enterprise")]
{
// load the license key and check if it's valid
// if not valid and not server mode just quit
// if not expired and server mode then force renewal
// if key still invalid and num_workers > 0, set to 0
if let Err(err) = reload_license_key(&db).await {
tracing::error!("Failed to reload license key: {err:#}");
}
let valid_key = *LICENSE_KEY_VALID.read().await;
if !valid_key && !server_mode {
panic!("Invalid license key, workers require a valid license key");
}
if server_mode {
// only force renewal if invalid but not empty (= expired)
let renewed_now = maybe_renew_license_key_on_start(
&HTTP_CLIENT,
&db,
!valid_key && !LICENSE_KEY_ID.read().await.is_empty(),
)
.await;
if renewed_now {
if let Err(err) = reload_license_key(&db).await {
tracing::error!("Failed to reload license key: {err:#}");
}
}
if num_workers > 0 {
let valid_key = *LICENSE_KEY_VALID.read().await;
if !valid_key {
tracing::warn!("License key invalid, setting num_workers to 0");
num_workers = 0;
}
}
}
}
let worker_mode = num_workers > 0;
if server_mode || worker_mode || indexer_mode {
@@ -448,7 +493,16 @@ Windmill Community Edition {GIT_VERSION}
initial_load(&db, killpill_tx.clone(), worker_mode, server_mode, is_agent).await;
monitor_db(&db, &base_internal_url, rsmq.clone(), server_mode, true).await;
monitor_db(
&db,
&base_internal_url,
rsmq.clone(),
server_mode,
worker_mode,
true,
killpill_tx.clone(),
)
.await;
monitor_pool(&db).await;
@@ -578,7 +632,9 @@ Windmill Community Edition {GIT_VERSION}
&base_internal_url,
rsmq.clone(),
server_mode,
false
worker_mode,
false,
tx.clone(),
)
.await;
},
@@ -621,7 +677,15 @@ Windmill Community Edition {GIT_VERSION}
},
LICENSE_KEY_SETTING => {
if let Err(e) = reload_license_key(&db).await {
tracing::error!(error = %e, "Could not reload license key setting");
tracing::error!("Failed to reload license key: {e:#}");
}
#[cfg(feature = "enterprise")]
if worker_mode {
let valid_key = *LICENSE_KEY_VALID.read().await;
if !valid_key {
tracing::error!("Invalid license key, exiting...");
tx.send(()).expect("send");
}
}
},
DEFAULT_TAGS_PER_WORKSPACE_SETTING => {
@@ -767,15 +831,10 @@ Windmill Community Edition {GIT_VERSION}
Ok(()) as anyhow::Result<()>
};
if mode == Mode::Server || mode == Mode::Standalone {
if server_mode {
schedule_stats(&db, &HTTP_CLIENT).await;
}
#[cfg(feature = "enterprise")]
if mode == Mode::Server || mode == Mode::Standalone {
schedule_key_renewal(&HTTP_CLIENT, &db).await;
}
futures::try_join!(
shutdown_signal,
workers_f,
+16 -31
View File
@@ -27,7 +27,7 @@ use windmill_api::{
DEFAULT_BODY_LIMIT, IS_SECURE, OAUTH_CLIENTS, REQUEST_SIZE_LIMIT, SAML_METADATA, SCIM_TOKEN,
};
#[cfg(feature = "enterprise")]
use windmill_common::ee::{jobs_waiting_alerts, worker_groups_alerts};
use windmill_common::ee::{jobs_waiting_alerts, worker_groups_alerts, LICENSE_KEY_VALID};
use windmill_common::{
auth::JWT_SECRET,
ee::CriticalErrorChannel,
@@ -76,9 +76,6 @@ use windmill_common::global_settings::OBJECT_STORE_CACHE_CONFIG_SETTING;
#[cfg(feature = "enterprise")]
use crate::ee::verify_license_key;
#[cfg(feature = "enterprise")]
use windmill_common::ee::LICENSE_KEY_VALID;
use crate::ee::set_license_key;
lazy_static::lazy_static! {
@@ -152,18 +149,10 @@ pub async fn initial_load(
tracing::error!("Error reloading custom tags: {:?}", e)
}
if let Err(e) = reload_base_url_setting(db).await {
tracing::error!("Error reloading base url: {:?}", e)
}
if let Err(e) = reload_hub_base_url_setting(db, server_mode).await {
tracing::error!("Error reloading hub base url: {:?}", e)
}
if let Err(e) = reload_critical_error_channels_setting(&db).await {
tracing::error!("Could not reload critical error emails setting: {:?}", e);
}
if let Err(e) = reload_jwt_secret_setting(&db).await {
tracing::error!("Could not reload jwt secret setting: {:?}", e);
}
@@ -182,11 +171,6 @@ pub async fn initial_load(
reload_scim_token_setting(&db).await;
}
#[cfg(feature = "enterprise")]
if let Err(e) = reload_license_key(&db).await {
tracing::error!("Error reloading license key: {:?}", e)
}
if worker_mode {
reload_extra_pip_index_url_setting(&db).await;
reload_pip_index_url_setting(&db).await;
@@ -854,8 +838,10 @@ pub async fn reload_request_size(db: &DB) {
}
}
pub async fn reload_license_key(db: &DB) -> error::Result<()> {
let q = load_value_from_global_settings(db, LICENSE_KEY_SETTING).await?;
pub async fn reload_license_key(db: &DB) -> anyhow::Result<()> {
let q = load_value_from_global_settings(db, LICENSE_KEY_SETTING)
.await
.map_err(|err| anyhow::anyhow!("Error reloading license key: {}", err.to_string()))?;
let mut value = std::env::var("LICENSE_KEY")
.ok()
@@ -873,9 +859,7 @@ pub async fn reload_license_key(db: &DB) -> error::Result<()> {
tracing::error!("Could not parse LICENSE_KEY found: {:#?}", &q);
}
};
set_license_key(value).await?;
set_license_key(value).await;
Ok(())
}
@@ -1014,7 +998,9 @@ pub async fn monitor_db(
base_internal_url: &str,
rsmq: Option<MultiplexedRsmq>,
server_mode: bool,
_worker_mode: bool,
initial_load: bool,
_killpill_tx: tokio::sync::broadcast::Sender<()>,
) {
let zombie_jobs_f = async {
if server_mode && !initial_load {
@@ -1035,15 +1021,14 @@ pub async fn monitor_db(
let verify_license_key_f = async {
#[cfg(feature = "enterprise")]
if let Err(e) = verify_license_key().await {
tracing::error!("Error verifying license key: {:?}", e);
let mut l = LICENSE_KEY_VALID.write().await;
*l = false;
} else {
let is_valid = LICENSE_KEY_VALID.read().await.clone();
if !is_valid {
let mut l = LICENSE_KEY_VALID.write().await;
*l = true;
if !initial_load {
verify_license_key().await;
if _worker_mode {
let valid_key = *LICENSE_KEY_VALID.read().await;
if !valid_key {
tracing::error!("Invalid license key, exiting...");
_killpill_tx.send(()).expect("send");
}
}
}
};
+7
View File
@@ -1467,6 +1467,11 @@ paths:
parameters:
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/PerPage"
- name: active_only
in: query
description: filter only active users
schema:
type: boolean
responses:
"200":
description: user
@@ -11791,6 +11796,8 @@ components:
type: string
username:
type: string
operator_only:
type: boolean
required:
- email
+1 -1
View File
@@ -4,7 +4,7 @@ use std::sync::Arc;
#[cfg(feature = "enterprise")]
use tokio::sync::RwLock;
pub async fn validate_license_key(_license_key: String) -> anyhow::Result<String> {
pub async fn validate_license_key(_license_key: String) -> anyhow::Result<(String, bool)> {
// Implementation is not open source
Err(anyhow!("License can't be validated in Windmill CE"))
}
+6 -2
View File
@@ -456,9 +456,13 @@ async fn ee_license() -> &'static str {
#[cfg(feature = "enterprise")]
async fn ee_license() -> String {
use windmill_common::ee::LICENSE_KEY_ID;
use windmill_common::ee::{LICENSE_KEY_ID, LICENSE_KEY_VALID};
LICENSE_KEY_ID.read().await.clone()
if *LICENSE_KEY_VALID.read().await {
LICENSE_KEY_ID.read().await.clone()
} else {
"".to_string()
}
}
async fn openapi() -> &'static str {
+7 -3
View File
@@ -163,8 +163,13 @@ pub async fn test_license_key(
Json(TestKey { license_key }): Json<TestKey>,
) -> error::Result<String> {
require_super_admin(&db, &authed.email).await?;
validate_license_key(license_key).await?;
Ok("Sent test email".to_string())
let (_, expired) = validate_license_key(license_key).await?;
if expired {
Err(error::Error::BadRequest("Expired license key".to_string()))
} else {
Ok("Valid license key".to_string())
}
}
pub async fn get_local_settings(
@@ -301,7 +306,6 @@ pub async fn send_stats(Extension(db): Extension<DB>, authed: ApiAuthed) -> Resu
windmill_common::stats_ee::send_stats(
&HTTP_CLIENT,
&db,
true,
windmill_common::stats_ee::SendStatsReason::Manual,
)
.await?;
+37 -10
View File
@@ -789,6 +789,8 @@ pub struct GlobalUserInfo {
name: Option<String>,
company: Option<String>,
username: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
operator_only: Option<bool>,
}
#[derive(Serialize, Debug)]
@@ -1001,24 +1003,48 @@ async fn list_user_usage(
Ok(Json(rows))
}
#[derive(Deserialize)]
struct ActiveUsersOnly {
active_only: Option<bool>,
}
async fn list_users_as_super_admin(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Query(pagination): Query<Pagination>,
Query(ActiveUsersOnly { active_only }): Query<ActiveUsersOnly>,
) -> JsonResult<Vec<GlobalUserInfo>> {
require_super_admin(&db, &authed.email).await?;
let per_page = pagination.per_page.unwrap_or(10000).max(1);
let offset = (pagination.page.unwrap_or(1).max(1) - 1) * per_page;
let rows = sqlx::query_as!(
GlobalUserInfo,
"SELECT email, login_type::text, verified, super_admin, name, company, username from password ORDER BY super_admin DESC, email LIMIT \
$1 OFFSET $2",
per_page as i32,
offset as i32
)
.fetch_all(&db)
.await?;
let rows = if active_only.is_some_and(|x| x) {
sqlx::query_as!(
GlobalUserInfo,
"WITH active_users AS (SELECT distinct username as email FROM audit WHERE timestamp > NOW() - INTERVAL '1 month' AND (operation = 'users.login' OR operation = 'oauth.login')),
authors as (SELECT distinct email FROM usr WHERE usr.operator IS false)
SELECT email, email NOT IN (SELECT email FROM authors) as operator_only, login_type::text, verified, super_admin, name, company, username
FROM password
WHERE email IN (SELECT email FROM active_users)
ORDER BY super_admin DESC
LIMIT $1 OFFSET $2",
per_page as i32,
offset as i32
)
.fetch_all(&db)
.await?
} else {
sqlx::query_as!(
GlobalUserInfo,
"SELECT email, login_type::text, verified, super_admin, name, company, username, NULL::bool as operator_only FROM password ORDER BY super_admin DESC, email LIMIT \
$1 OFFSET $2",
per_page as i32,
offset as i32
)
.fetch_all(&db)
.await?
};
Ok(Json(rows))
}
@@ -1173,7 +1199,7 @@ async fn global_whoami(
) -> JsonResult<GlobalUserInfo> {
let user = sqlx::query_as!(
GlobalUserInfo,
"SELECT email, login_type::TEXT, super_admin, verified, name, company, username FROM password WHERE \
"SELECT email, login_type::TEXT, super_admin, verified, name, company, username, NULL::bool as operator_only FROM password WHERE \
email = $1",
email
)
@@ -1192,6 +1218,7 @@ async fn global_whoami(
name: None,
company: None,
username: None,
operator_only: None,
}))
} else {
Err(user.unwrap_err())
+6 -1
View File
@@ -48,8 +48,13 @@ pub async fn send_critical_alert(
}
#[cfg(feature = "enterprise")]
pub async fn schedule_key_renewal(_http_client: &reqwest::Client, _db: &crate::db::DB) -> () {
pub async fn maybe_renew_license_key_on_start(
_http_client: &reqwest::Client,
_db: &crate::db::DB,
force_renew_now: bool,
) -> bool {
// Implementation is not open source
force_renew_now
}
#[cfg(feature = "enterprise")]
-1
View File
@@ -28,7 +28,6 @@ pub enum SendStatsReason {
pub async fn send_stats(
_http_client: &reqwest::Client,
_db: &DB,
_skip_job_usage: bool,
_reason: SendStatsReason,
) -> Result<()> {
// stats details are closed source
+8
View File
@@ -206,6 +206,14 @@ pub async fn get_uid<'c, E: sqlx::Executor<'c, Database = Postgres>>(db: E) -> R
Ok(uid)
}
pub fn map_string_to_number(s: &str, max_number: u64) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
s.hash(&mut hasher);
hasher.finish() % (max_number + 1)
}
#[derive(Serialize, Deserialize, PartialEq, Clone)]
#[serde(rename_all = "lowercase")]
pub enum Mode {
@@ -277,9 +277,7 @@
<div class="text-secondary pb-4 text-xs"
>Setting SMTP unlocks sending emails upon adding new users to the workspace or the
instance or sending critical alerts.
<a
target="_blank"
href="https://www.windmill.dev/docs/advanced/instance_settings#smtp">Learn more</a
<a target="_blank" href="https://www.windmill.dev/docs/misc/setup_smtp">Learn more</a
></div
>
{:else if category == 'Registries'}
@@ -653,24 +651,33 @@
{@const attemptedAt = new Date(
latestKeyRenewalAttempt.attempted_at
).toLocaleString()}
{@const isTrial =
latestKeyRenewalAttempt.result.startsWith('error: trial:')}
<div class="relative">
<Popover notClickable>
<div class="flex flex-row items-center gap-1">
{#if latestKeyRenewalAttempt.result === 'success'}
<BadgeCheck class="text-green-600" size={12} />
{:else}
<BadgeX class="text-red-600" size={12} />
<BadgeX
class={isTrial ? 'text-yellow-600' : 'text-red-600'}
size={12}
/>
{/if}
<span
class={classNames(
'text-xs',
latestKeyRenewalAttempt.result === 'success'
? 'text-green-600'
: isTrial
? 'text-yellow-600'
: 'text-red-600'
)}
>
{latestKeyRenewalAttempt.result === 'success'
? 'Latest key renewal succeeded'
: isTrial
? 'Latest key renewal ignored because in trial'
: 'Latest key renewal failed'}
on {attemptedAt}
</span>
@@ -680,6 +687,10 @@
<span class="text-green-300">
Latest key renewal succeeded on {attemptedAt}
</span>
{:else if isTrial}
<span class="text-yellow-300">
License key cannot be renewed during trial ({attemptedAt})
</span>
{:else}
<span class="text-red-300">
Latest key renewal failed on {attemptedAt}: {latestKeyRenewalAttempt.result.replace(
@@ -22,11 +22,12 @@
import ChangeInstanceUsername from './ChangeInstanceUsername.svelte'
import { isCloudHosted } from '$lib/cloud'
import InstanceNameEditor from './InstanceNameEditor.svelte'
import Toggle from './Toggle.svelte'
let drawer: Drawer
let filter = ''
export function openDrawer() {
listUsers()
listUsers(activeOnly)
drawer?.openDrawer?.()
}
@@ -45,11 +46,14 @@
let users: GlobalUserInfo[] = []
let filteredUsers: GlobalUserInfo[] = []
let deleteConfirmedCallback: (() => void) | undefined = undefined
let activeOnly = false
async function listUsers(): Promise<void> {
users = await UserService.listUsersAsSuperAdmin({ perPage: 100000 })
async function listUsers(activeOnly: boolean): Promise<void> {
users = await UserService.listUsersAsSuperAdmin({ perPage: 100000, activeOnly: activeOnly })
}
$: listUsers(activeOnly)
let tab: 'users' | string = 'users'
let nbDisplayed = 50
@@ -70,7 +74,7 @@
})
getAutomateUsernameCreationSetting()
sendUserToast('Automatic username creation enabled')
listUsers()
listUsers(activeOnly)
}
async function updateName(name: string | undefined, email: string) {
@@ -82,7 +86,7 @@
}
})
sendUserToast('User updated')
listUsers()
listUsers(activeOnly)
} catch (e) {
sendUserToast('Error updating user', true)
}
@@ -96,7 +100,12 @@
f={(x) => x.email + ' ' + x.name + ' ' + x.company}
/>
<Drawer bind:this={drawer} on:open={listUsers} size="1200px" on:close={removeHash}>
<Drawer
bind:this={drawer}
on:open={() => listUsers(activeOnly)}
size="1200px"
on:close={removeHash}
>
<DrawerContent overflow_y={true} title="Instance settings" on:close={closeDrawer}>
<div class="flex flex-col h-full w-full">
<div>
@@ -130,11 +139,16 @@
<div class="h-full">
{#if !automateUsernameCreation && !isCloudHosted()}
<div class="mb-4">
<h3 class="mb-2">
Automatic username creation
</h3>
<h3 class="mb-2"> Automatic username creation </h3>
<div class="mb-2">
<span class="text-primary text-sm">Automatically create a username for new users based on their email, shared across workspaces. <a target="_blank" href="https://www.windmill.dev/docs/advanced/instance_settings#automatic-username-creation">Learn more</a></span>
<span class="text-primary text-sm"
>Automatically create a username for new users based on their email, shared
across workspaces. <a
target="_blank"
href="https://www.windmill.dev/docs/advanced/instance_settings#automatic-username-creation"
>Learn more</a
></span
>
</div>
<Button
btnClasses="w-auto"
@@ -164,10 +178,20 @@
{/if}
<div class="py-2 mb-4">
<InviteGlobalUser on:new={listUsers} />
<InviteGlobalUser on:new={() => listUsers(activeOnly)} />
</div>
<h3>All instance users</h3>
<div class="flex flex-row justify-between">
<h3>All instance users</h3>
<Toggle
bind:checked={activeOnly}
options={{
left: 'Show active users only',
leftTooltip:
'An active user is a user who has performed at least one action in the last 30 days'
}}
/>
</div>
<div class="pb-1" />
<div>
<input placeholder="Search users" bind:value={filter} class="input mt-1" />
@@ -181,12 +205,15 @@
{#if automateUsernameCreation}
<th>username</th>
{/if}
{#if activeOnly}
<th>kind</th>
{/if}
<th />
<th />
</tr>
<tbody slot="body" class="overflow-y-auto w-full h-full max-h-full">
{#if filteredUsers && users}
{#each filteredUsers.slice(0, nbDisplayed) as { email, super_admin, login_type, name, username } (email)}
{#each filteredUsers.slice(0, nbDisplayed) as { email, super_admin, login_type, name, username, operator_only } (email)}
<tr class="border">
<td>{email}</td>
<td>{login_type}</td>
@@ -203,13 +230,22 @@
{email}
isConflict
on:renamed={() => {
listUsers()
listUsers(activeOnly)
}}
/>
{/key}
{/if}
</td>
{/if}
{#if activeOnly}
<td>
{#if operator_only}
Operator only
{:else}
Developer
{/if}
</td>
{/if}
<td>
<ToggleButtonGroup
selected={super_admin}
@@ -217,7 +253,7 @@
console.log('BAR')
if (email == $userStore?.email) {
sendUserToast('You cannot demote yourself', true)
listUsers()
listUsers(activeOnly)
return
}
await UserService.globalUserUpdate({
@@ -227,7 +263,7 @@
}
})
sendUserToast('User updated')
listUsers()
listUsers(activeOnly)
}}
>
<ToggleButton value={false} size="xs" label="User" />
@@ -244,7 +280,7 @@
updateName(e.detail, email)
}}
on:renamed={() => {
listUsers()
listUsers(activeOnly)
}}
{automateUsernameCreation}
/>
@@ -258,7 +294,7 @@
deleteConfirmedCallback = async () => {
await UserService.globalUserDelete({ email })
sendUserToast(`User ${email} removed`)
listUsers()
listUsers(activeOnly)
}
}}
>