1
0
Fork 0
forked from 2sys/phonograph

fix email uniqueness logic for new signups

This commit is contained in:
Brent Schroeter 2026-01-20 07:25:45 +00:00
parent fc8e3d6b99
commit b701270d88
5 changed files with 60 additions and 20 deletions

View file

@ -0,0 +1,2 @@
drop index if exists users_email_idx;
create index on users (email);

View file

@ -0,0 +1,2 @@
drop index if exists users_email_idx;
create unique index on users (email);

View file

@ -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)?
}
},
)
}

View file

@ -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()

View file

@ -33,10 +33,5 @@
{% endfor %}
</menu>
</section>
<section class="workspace-nav__section">
<div class="workspace-nav__heading">
<h1>Shared With Me</h1>
</div>
</section>
</nav>
{% endblock %}