collab: Add `GET /users/look_up` endpoint (#31059)

Marshall Bowers created

This PR adds a new `GET /users/look_up` endpoint for retrieving users by
various identifiers.

This endpoint can look up users by the following identifiers:

- Zed user ID
- Stripe Customer ID
- Stripe Subscription ID
- Email address
- GitHub login

Release Notes:

- N/A

Change summary

crates/collab/src/api.rs                          | 83 +++++++++++++++++
crates/collab/src/db/queries/billing_customers.rs | 13 ++
2 files changed, 96 insertions(+)

Detailed changes

crates/collab/src/api.rs 🔗

@@ -5,6 +5,7 @@ pub mod extensions;
 pub mod ips_file;
 pub mod slack;
 
+use crate::db::Database;
 use crate::{
     AppState, Error, Result, auth,
     db::{User, UserId},
@@ -97,6 +98,7 @@ impl std::fmt::Display for SystemIdHeader {
 pub fn routes(rpc_server: Arc<rpc::Server>) -> Router<(), Body> {
     Router::new()
         .route("/user", get(get_authenticated_user))
+        .route("/users/look_up", get(look_up_user))
         .route("/users/:id/access_tokens", post(create_access_token))
         .route("/rpc_server_snapshot", get(get_rpc_server_snapshot))
         .merge(billing::router())
@@ -181,6 +183,87 @@ async fn get_authenticated_user(
     }))
 }
 
+#[derive(Debug, Deserialize)]
+struct LookUpUserParams {
+    identifier: String,
+}
+
+#[derive(Debug, Serialize)]
+struct LookUpUserResponse {
+    user: Option<User>,
+}
+
+async fn look_up_user(
+    Query(params): Query<LookUpUserParams>,
+    Extension(app): Extension<Arc<AppState>>,
+) -> Result<Json<LookUpUserResponse>> {
+    let user = resolve_identifier_to_user(&app.db, &params.identifier).await?;
+    let user = if let Some(user) = user {
+        match user {
+            UserOrId::User(user) => Some(user),
+            UserOrId::Id(id) => app.db.get_user_by_id(id).await?,
+        }
+    } else {
+        None
+    };
+
+    Ok(Json(LookUpUserResponse { user }))
+}
+
+enum UserOrId {
+    User(User),
+    Id(UserId),
+}
+
+async fn resolve_identifier_to_user(
+    db: &Arc<Database>,
+    identifier: &str,
+) -> Result<Option<UserOrId>> {
+    if let Some(identifier) = identifier.parse::<i32>().ok() {
+        let user = db.get_user_by_id(UserId(identifier)).await?;
+
+        return Ok(user.map(UserOrId::User));
+    }
+
+    if identifier.starts_with("cus_") {
+        let billing_customer = db
+            .get_billing_customer_by_stripe_customer_id(&identifier)
+            .await?;
+
+        return Ok(billing_customer.map(|billing_customer| UserOrId::Id(billing_customer.user_id)));
+    }
+
+    if identifier.starts_with("sub_") {
+        let billing_subscription = db
+            .get_billing_subscription_by_stripe_subscription_id(&identifier)
+            .await?;
+
+        if let Some(billing_subscription) = billing_subscription {
+            let billing_customer = db
+                .get_billing_customer_by_id(billing_subscription.billing_customer_id)
+                .await?;
+
+            return Ok(
+                billing_customer.map(|billing_customer| UserOrId::Id(billing_customer.user_id))
+            );
+        } else {
+            return Ok(None);
+        }
+    }
+
+    if identifier.contains('@') {
+        let user = db.get_user_by_email(identifier).await?;
+
+        return Ok(user.map(UserOrId::User));
+    }
+
+    if let Some(user) = db.get_user_by_github_login(identifier).await? {
+        return Ok(Some(UserOrId::User(user)));
+    }
+
+    Ok(None)
+}
+
 #[derive(Deserialize, Debug)]
 struct CreateUserParams {
     github_user_id: i32,

crates/collab/src/db/queries/billing_customers.rs 🔗

@@ -57,6 +57,19 @@ impl Database {
         .await
     }
 
+    pub async fn get_billing_customer_by_id(
+        &self,
+        id: BillingCustomerId,
+    ) -> Result<Option<billing_customer::Model>> {
+        self.transaction(|tx| async move {
+            Ok(billing_customer::Entity::find()
+                .filter(billing_customer::Column::Id.eq(id))
+                .one(&*tx)
+                .await?)
+        })
+        .await
+    }
+
     /// Returns the billing customer for the user with the specified ID.
     pub async fn get_billing_customer_by_user_id(
         &self,