Files
windmill/backend/windmill-common/src/usernames.rs
Ruben Fiszel c580572252 refactor: extract windmill-api-scripts and windmill-api-users subcrates (#7850)
* refactor: extract windmill-api-scripts and windmill-api-users subcrates

Split the monolithic windmill-api crate by extracting scripts.rs, flows.rs,
users.rs, and users_oss.rs into dedicated subcrates. This reduces incremental
rebuild times when editing these modules.

Changes:
- Create windmill-api-scripts crate (scripts.rs + flows.rs, ~4.3K lines)
- Create windmill-api-users crate (users.rs + users_oss.rs, ~2.4K lines)
- Move clear_schedule to windmill-queue (shared by scripts, flows, workspaces)
- Move username utilities (VALID_USERNAME, INVALID_USERNAME_CHARS,
  generate_instance_wide_unique_username) to windmill-common/src/usernames.rs
- Move COOKIE_DOMAIN, IS_SECURE, WithStarredInfoQuery, BulkDeleteRequest,
  WebhookShared to windmill-common for cross-crate access
- Original files in windmill-api become thin stubs with pub use re-exports
- EE-dependent route handlers remain in windmill-api (create_user, rename_user,
  set_password, reset_password, etc.)
- Feature forwarding for enterprise, private, parquet, no_auth

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: extract windmill-api-workspaces subcrate (Step 3)

Move workspaces.rs, workspaces_extra.rs, workspaces_oss.rs, and
workspaces_ee.rs into a new windmill-api-workspaces crate (~7K lines).

Routes that depend on windmill-api internals (AI copilot, teams,
tarball export, critical alerts, stripe) remain in the windmill-api
stub. The subcrate handles all other workspace management routes.

Also moved send_email_if_possible to windmill-common/email_oss.rs
to make it available across subcrates without circular deps.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* all

* refactor: extract windmill-api-groups subcrate (groups.rs + folders.rs)

Extract groups.rs (1,093 lines) and folders.rs (833 lines) into a new
windmill-api-groups subcrate. Both modules had clean dependencies on
already-extracted crates (windmill-api-auth, windmill-common,
windmill-api-workspaces). Also removes unused re-exports of
get_instance_username_or_create_pending and INVALID_USERNAME_CHARS
from windmill-api/src/utils.rs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: add granular_acls.rs and folder_history.rs to windmill-api-groups

Extract granular_acls.rs (395 lines) and folder_history.rs (68 lines) into
the windmill-api-groups subcrate. Both modules only depend on already-extracted
crates and belong to the same access-control domain as groups and folders.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove unused imports and dead code from subcrate extraction

- Remove unused BASE_URL import from lib.rs
- Remove workspaces_extra.rs and workspaces_oss.rs re-export stubs (no consumers in windmill-api)
- Remove dead send_email_if_possible OSS stub (callers moved to windmill-api-users)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* all

* chore: bust CI cargo cache for subcrate split

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: re-export BASE_URL for EE files that use crate::BASE_URL

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: forward no_auth feature to windmill-api-users, remove dead code

- Add "windmill-api-users/no_auth" to windmill-api's no_auth feature
  so the login bypass in users.rs:1600 activates correctly
- Remove dead send_email_if_possible from windmill-api-users/users_oss.rs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: re-enable cargo cache for backend tests

Cache was disabled to bust stale entries from before subcrate split.
Now that a clean build has run, re-enable for faster CI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: install mold+clang in CI workflows

The .cargo/config.toml uses mold linker for x86_64-linux.
Build scripts require linking even during cargo check.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: increase cargo test timeout to 30 min

Exit code 143 (SIGTERM) means the 20-min timeout was hit during
compilation without cache. Bump to 30 min as safety net.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: limit cargo build jobs to 4 to prevent OOM in CI

Exit code 143 (SIGTERM) after 8 min = OOM kill during compilation.
8 parallel LLVM codegen jobs exhaust memory on ubicloud-standard-8.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 12:12:03 +00:00

98 lines
3.1 KiB
Rust

/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2022
* This file and its contents are licensed under the AGPLv3 License.
* Please see the included NOTICE for copyright information and
* LICENSE-AGPL for a copy of the license.
*/
use regex::Regex;
use sqlx::{Postgres, Transaction};
use crate::error::{self, Error};
lazy_static::lazy_static! {
pub static ref INVALID_USERNAME_CHARS: Regex = Regex::new(r"[^A-Za-z0-9_]").unwrap();
pub static ref VALID_USERNAME: Regex = Regex::new(r#"^[a-zA-Z][a-zA-Z_0-9]*$"#).unwrap();
}
pub async fn generate_instance_wide_unique_username<'c>(
tx: &mut Transaction<'c, Postgres>,
email: &str,
) -> error::Result<String> {
let mut username = email.split('@').next().unwrap().to_string();
username = INVALID_USERNAME_CHARS
.replace_all(&mut username, "")
.to_string();
if username.is_empty() {
username = "user".to_string()
}
let base_username = username.clone();
let mut username_conflict = true;
let mut i = 1;
while username_conflict {
if i > 1000 {
return Err(Error::internal_err(format!(
"too many username conflicts for {}",
email
)));
}
if i > 1 {
username = format!("{}{}", base_username, i)
}
username_conflict = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM usr WHERE username = $1 and email != $2 UNION SELECT 1 FROM password WHERE username = $1 UNION SELECT 1 FROM pending_user WHERE username = $1)",
&username,
&email
)
.fetch_one(&mut **tx)
.await?
.unwrap_or(false);
i += 1;
}
Ok(username)
}
pub async fn get_instance_username_or_create_pending<'c>(
tx: &mut Transaction<'c, Postgres>,
email: &str,
) -> error::Result<String> {
let user = sqlx::query_scalar!("SELECT username FROM password WHERE email = $1", email)
.fetch_optional(&mut **tx)
.await?;
if let Some(opt_username) = user {
if let Some(username) = opt_username {
Ok(username)
} else {
Err(Error::BadRequest(format!("No instance-wide username found for {email}. The user has different usernames for different workspaces. Ask the instance administrator to solve the conflict in the instance settings.")))
}
} else {
let pending_username =
sqlx::query_scalar!("SELECT username FROM pending_user WHERE email = $1", email)
.fetch_optional(&mut **tx)
.await?;
if let Some(username) = pending_username {
Ok(username)
} else {
let username = generate_instance_wide_unique_username(&mut *tx, email).await?;
sqlx::query!(
"INSERT INTO pending_user (email, username) VALUES ($1, $2)",
email,
username
)
.execute(&mut **tx)
.await
.map_err(|e| Error::internal_err(format!("creating pending user: {e:#}")))?;
Ok(username)
}
}
}