2025-10-07 06:23:50 +00:00
|
|
|
use axum::{extract::State, response::IntoResponse};
|
2025-10-22 00:43:53 -07:00
|
|
|
use interim_models::{
|
|
|
|
|
client::AppDbClient, user::User, workspace::Workspace, workspace_user_perm::WorkspaceMembership,
|
|
|
|
|
};
|
|
|
|
|
use interim_pgtypes::{client::WorkspaceClient, escape_identifier};
|
2025-10-07 06:23:50 +00:00
|
|
|
use sqlx::{Connection as _, PgConnection, query};
|
|
|
|
|
|
|
|
|
|
use crate::{
|
|
|
|
|
app::AppDbConn,
|
|
|
|
|
errors::AppError,
|
|
|
|
|
navigator::Navigator,
|
|
|
|
|
settings::Settings,
|
|
|
|
|
user::CurrentUser,
|
|
|
|
|
workspace_pooler::{RoleAssignment, WorkspacePooler},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/// HTTP POST handler for creating a new workspace. This handler does not expect
|
|
|
|
|
/// any arguments, as the backing database name is generated pseudo-randomly and
|
|
|
|
|
/// the human-friendly name may be changed later rather than specified
|
|
|
|
|
/// permenantly upon creation.
|
|
|
|
|
pub(super) async fn post(
|
|
|
|
|
State(settings): State<Settings>,
|
|
|
|
|
State(mut pooler): State<WorkspacePooler>,
|
|
|
|
|
AppDbConn(mut app_db): AppDbConn,
|
|
|
|
|
navigator: Navigator,
|
|
|
|
|
CurrentUser(user): CurrentUser,
|
|
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
|
|
|
// FIXME: csrf
|
|
|
|
|
|
|
|
|
|
const NAME_LEN_WORDS: usize = 3;
|
|
|
|
|
// WARNING: `db_name` is injected directly into the `create database` SQL
|
|
|
|
|
// command. It **must not** contain spaces or any other unsafe characters.
|
|
|
|
|
// Additionally, it **must** be URL safe without percent encoding.
|
|
|
|
|
let db_name = interim_namegen::default_generator()
|
|
|
|
|
.with_separator('_')
|
|
|
|
|
.generate_name(NAME_LEN_WORDS);
|
2025-10-22 00:43:53 -07:00
|
|
|
{
|
|
|
|
|
// No need to pool these connections, since we don't expect to be using them
|
|
|
|
|
// often. One less thing to keep track of in application state.
|
|
|
|
|
let mut workspace_creator_conn =
|
|
|
|
|
PgConnection::connect(settings.new_workspace_db_url.as_str()).await?;
|
|
|
|
|
query(&format!(
|
|
|
|
|
// `db_name` is an underscore-separated sequence of alphabetical words,
|
|
|
|
|
// which should be safe to inject directly into the SQL statement.
|
|
|
|
|
"create database {db_name}"
|
|
|
|
|
))
|
|
|
|
|
.execute(&mut workspace_creator_conn)
|
|
|
|
|
.await?;
|
|
|
|
|
query(&format!("revoke connect on database {db_name} from public"))
|
|
|
|
|
.execute(&mut workspace_creator_conn)
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
// Close and drop `workspace_creator_conn` at end of block, then
|
|
|
|
|
// reconnect using a pooled connection.
|
|
|
|
|
workspace_creator_conn.close().await?;
|
|
|
|
|
}
|
2025-10-07 06:23:50 +00:00
|
|
|
|
|
|
|
|
let mut workspace_url = settings.new_workspace_db_url.clone();
|
|
|
|
|
// Alter database name but preserve auth and any query parameters.
|
|
|
|
|
workspace_url.set_path(&db_name);
|
|
|
|
|
|
|
|
|
|
let workspace = Workspace::insert()
|
|
|
|
|
.owner_id(user.id)
|
|
|
|
|
.url(workspace_url)
|
|
|
|
|
.build()?
|
|
|
|
|
.insert(&mut app_db)
|
|
|
|
|
.await?;
|
|
|
|
|
|
2025-10-22 00:43:53 -07:00
|
|
|
let mut workspace_root_conn = pooler
|
|
|
|
|
.acquire_for(workspace.id, RoleAssignment::Root)
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
// Initialize database user. Connection is not used and may be dropped
|
|
|
|
|
// immediately.
|
2025-10-07 06:23:50 +00:00
|
|
|
pooler
|
|
|
|
|
.acquire_for(workspace.id, RoleAssignment::User(user.id))
|
|
|
|
|
.await?;
|
2025-10-22 00:43:53 -07:00
|
|
|
query(&format!(
|
|
|
|
|
"create schema {nsp}",
|
|
|
|
|
nsp = escape_identifier(&settings.phono_table_namespace)
|
|
|
|
|
))
|
|
|
|
|
.execute(workspace_root_conn.get_conn())
|
|
|
|
|
.await?;
|
|
|
|
|
grant_workspace_membership(
|
|
|
|
|
&db_name,
|
|
|
|
|
settings.clone(),
|
|
|
|
|
&mut app_db,
|
|
|
|
|
&mut workspace_root_conn,
|
|
|
|
|
&user,
|
|
|
|
|
&workspace,
|
|
|
|
|
)
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
Ok(navigator.workspace_page(workspace.id).redirect_to())
|
|
|
|
|
}
|
2025-10-07 06:23:50 +00:00
|
|
|
|
2025-10-22 00:43:53 -07:00
|
|
|
async fn grant_workspace_membership(
|
|
|
|
|
db_name: &str,
|
|
|
|
|
settings: Settings,
|
|
|
|
|
app_db_client: &mut AppDbClient,
|
|
|
|
|
workspace_root_client: &mut WorkspaceClient,
|
|
|
|
|
user: &User,
|
|
|
|
|
workspace: &Workspace,
|
|
|
|
|
) -> Result<(), AppError> {
|
2025-10-07 06:23:50 +00:00
|
|
|
let rolname = format!(
|
|
|
|
|
"{prefix}{user_id}",
|
|
|
|
|
prefix = settings.db_role_prefix,
|
|
|
|
|
user_id = user.id.simple()
|
|
|
|
|
);
|
|
|
|
|
query(&format!(
|
|
|
|
|
"grant connect on database {db_name} to {db_user}",
|
|
|
|
|
db_user = escape_identifier(&rolname),
|
|
|
|
|
))
|
2025-10-22 00:43:53 -07:00
|
|
|
.execute(workspace_root_client.get_conn())
|
2025-10-07 06:23:50 +00:00
|
|
|
.await?;
|
|
|
|
|
|
2025-10-22 00:43:53 -07:00
|
|
|
// TODO: There may be cases in which we will want to grant granular
|
|
|
|
|
// workspace access which excludes privileges to create tables.
|
2025-10-07 06:23:50 +00:00
|
|
|
query(&format!(
|
|
|
|
|
"grant usage, create on schema {nsp} to {rolname}",
|
|
|
|
|
nsp = escape_identifier(&settings.phono_table_namespace),
|
|
|
|
|
rolname = escape_identifier(&rolname)
|
|
|
|
|
))
|
2025-10-22 00:43:53 -07:00
|
|
|
.execute(workspace_root_client.get_conn())
|
2025-10-07 06:23:50 +00:00
|
|
|
.await?;
|
|
|
|
|
|
2025-10-22 00:43:53 -07:00
|
|
|
WorkspaceMembership::insert()
|
|
|
|
|
.workspace_id(workspace.id)
|
|
|
|
|
.user_id(user.id)
|
|
|
|
|
.build()?
|
|
|
|
|
.execute(app_db_client)
|
|
|
|
|
.await?;
|
2025-10-07 06:23:50 +00:00
|
|
|
|
2025-10-22 00:43:53 -07:00
|
|
|
Ok(())
|
2025-10-07 06:23:50 +00:00
|
|
|
}
|