diff --git a/phono-models/migrations/20260120062457_fix_email_unique_constraint.down.sql b/phono-models/migrations/20260120062457_fix_email_unique_constraint.down.sql new file mode 100644 index 0000000..f898462 --- /dev/null +++ b/phono-models/migrations/20260120062457_fix_email_unique_constraint.down.sql @@ -0,0 +1,2 @@ +drop index if exists users_email_idx; +create index on users (email); diff --git a/phono-models/migrations/20260120062457_fix_email_unique_constraint.up.sql b/phono-models/migrations/20260120062457_fix_email_unique_constraint.up.sql new file mode 100644 index 0000000..90a4112 --- /dev/null +++ b/phono-models/migrations/20260120062457_fix_email_unique_constraint.up.sql @@ -0,0 +1,2 @@ +drop index if exists users_email_idx; +create unique index on users (email); diff --git a/phono-models/src/user.rs b/phono-models/src/user.rs index eae9521..07c0196 100644 --- a/phono-models/src/user.rs +++ b/phono-models/src/user.rs @@ -110,29 +110,55 @@ returning id, uid, email .await? { user - } else if uid.is_some() { - // Conflict should have been on at least `uid`, meaning that the - // user already exists and its fields are fully populated. - query_as!(User, "select id, uid, email from users where uid = $1", uid) - .fetch_one(app_db.get_conn()) - .await? } else { - // Conflict must have been on `email`, meaning that the user - // already exists, though its `uid` may not be up to date. Use - // `COALESCE()` on the `uid` value to ensure that it is not - // inadvertently removed if present. - query_as!( + // Conflict may have been on either or both of `email` and + // `uid`. + // + // If the conflict was on `email`, we want to ensure that `uid` + // is updated if and only if it is currently null. This can be + // accomplished using `COALESCE()` with the current `uid` value + // as the first argument. The parameterized value will only be + // used if the current value is null. + // + // If the conflict was on `uid`, we merely want to return the + // record with the matching `uid`. + // + // If both fields conflicted then the `UPDATE` query will be + // sufficient, but if the operation is using a different email + // address than the one already stored then we will need one + // final `SELECT` query to guarantee that we obtain a result. + // Assuming that the caller queries by `uid` prior to starting + // the upsert, this last query should only be hit when two HTTP + // requests are running the auth flow concurrently. + if let Some(user) = query_as!( User, " update users -set uid = coalesce($1, uid) -where email = lower($1) +set uid = coalesce(uid, $1) +where email = lower($2) returning id, uid, email ", + uid, email, ) - .fetch_one(app_db.get_conn()) + .fetch_optional(app_db.get_conn()) .await? + { + user + } else { + User::with_uid( + // TODO: This should hold true *unless* the database is + // "tampered with" by an outside actor (such as a + // developer performing manual maintenance). While such + // an edge case is beyond the scope of the server's + // responsibilities, panicking may be considered an + // overreaction. + uid.expect("uid must be non-null to cause a database conflict"), + ) + .fetch_optional(app_db) + .await? + .ok_or(sqlx::Error::RowNotFound)? + } }, ) } diff --git a/phono-server/src/routes/workspaces_single/grant_workspace_privilege_handler.rs b/phono-server/src/routes/workspaces_single/grant_workspace_privilege_handler.rs index ac90aef..6134434 100644 --- a/phono-server/src/routes/workspaces_single/grant_workspace_privilege_handler.rs +++ b/phono-server/src/routes/workspaces_single/grant_workspace_privilege_handler.rs @@ -7,6 +7,7 @@ use phono_backends::{escape_identifier, pg_database::PgDatabase, rolnames::ROLE_ use phono_models::{ accessors::{Accessor as _, Actor, workspace::WorkspaceAccessor}, user::User, + workspace_user_perm::WorkspaceMembership, }; use serde::Deserialize; use sqlx::query; @@ -84,7 +85,7 @@ pub(super) async fn post( .await? .datname; query(&format!( - "grant connect on database {db_name_esc} to {rolname}", + "grant connect on database {db_name_esc} to {rolname} with grant option", db_name_esc = escape_identifier(&db_name), rolname = escape_identifier(&format!( "{ROLE_PREFIX_USER}{user_id}", @@ -93,6 +94,20 @@ pub(super) async fn post( )) .execute(root_client.get_conn()) .await?; + query(&format!( + "grant usage, create on schema phono to {rolname} with grant option", + rolname = escape_identifier(&format!( + "{ROLE_PREFIX_USER}{user_id}", + user_id = target_user.id.simple() + )) + )) + .execute(root_client.get_conn()) + .await?; + WorkspaceMembership::upsert() + .workspace_id(workspace_id) + .user_id(target_user.id) + .execute(&mut app_db) + .await?; Ok(navigator .workspace_page() diff --git a/phono-server/templates/workspaces_multi/list.html b/phono-server/templates/workspaces_multi/list.html index 879138a..bfb3a8d 100644 --- a/phono-server/templates/workspaces_multi/list.html +++ b/phono-server/templates/workspaces_multi/list.html @@ -33,10 +33,5 @@ {% endfor %} -
-
-

Shared With Me

-
-
{% endblock %}