shoutdotdev/src/users.rs
2025-02-19 23:50:38 -08:00

134 lines
4.3 KiB
Rust

use anyhow::Context;
use axum::{
extract::FromRequestParts,
http::request::Parts,
response::{IntoResponse, Redirect, Response},
RequestPartsExt,
};
use diesel::{
associations::Identifiable,
deserialize::Queryable,
dsl::{auto_type, insert_into, AsSelect, Eq, Select},
pg::Pg,
prelude::*,
Selectable,
};
use uuid::Uuid;
use crate::{
app_error::AppError,
app_state::AppState,
auth::AuthInfo,
schema::{team_memberships, teams, users},
team_memberships::TeamMembership,
teams::Team,
};
#[derive(Clone, Debug, Identifiable, Insertable, Queryable, Selectable)]
#[diesel(table_name = users)]
#[diesel(check_for_backend(Pg))]
pub struct User {
pub id: Uuid,
pub uid: String,
pub email: String,
}
impl User {
pub fn all() -> Select<users::table, AsSelect<User, Pg>> {
users::table.select(User::as_select())
}
pub fn with_uid(uid_value: &str) -> Eq<users::uid, &str> {
users::uid.eq(uid_value)
}
#[auto_type(no_type_alias)]
pub fn team_memberships(self) -> _ {
let user_id: Uuid = self.id.clone();
let user_id_filter: Eq<team_memberships::user_id, Uuid> =
TeamMembership::with_user_id(user_id);
let select: AsSelect<(TeamMembership, Team), Pg> = <(TeamMembership, Team)>::as_select();
team_memberships::table
.inner_join(teams::table)
.filter(user_id_filter)
.select(select)
}
}
#[derive(Clone, Debug)]
pub struct CurrentUser(pub User);
impl FromRequestParts<AppState> for CurrentUser {
type Rejection = CurrentUserRejection;
async fn from_request_parts(
parts: &mut Parts,
state: &AppState,
) -> Result<Self, <Self as FromRequestParts<AppState>>::Rejection> {
let auth_info = parts
.extract_with_state::<AuthInfo, AppState>(state)
.await
.map_err(|_| CurrentUserRejection::AuthRequired(state.settings.base_path.clone()))?;
let current_user = state
.db_pool
.get()
.await
.map_err(|err| CurrentUserRejection::InternalServerError(err.into()))?
.interact(move |conn| {
let maybe_current_user = User::all()
.filter(User::with_uid(&auth_info.sub))
.first(conn)
.optional()
.context("failed to load maybe_current_user")?;
if let Some(current_user) = maybe_current_user {
return Ok(current_user);
}
let new_user = User {
id: Uuid::now_v7(),
uid: auth_info.sub.clone(),
email: auth_info.email,
};
match insert_into(users::table)
.values(new_user)
.on_conflict(users::uid)
.do_nothing()
.returning(User::as_returning())
.get_result(conn)
{
QueryResult::Err(diesel::result::Error::NotFound) => {
tracing::debug!("detected race to insert current user record");
User::all()
.filter(User::with_uid(&auth_info.sub))
.first(conn)
.context(
"failed to load record after detecting race to insert current user",
)
}
QueryResult::Err(err) => {
Err(err).context("failed to insert current user record")
}
QueryResult::Ok(result) => Ok(result),
}
})
.await
.unwrap()
.map_err(|err| CurrentUserRejection::InternalServerError(err.into()))?;
Ok(CurrentUser(current_user))
}
}
pub enum CurrentUserRejection {
AuthRequired(String),
InternalServerError(AppError),
}
impl IntoResponse for CurrentUserRejection {
fn into_response(self) -> Response {
match self {
Self::AuthRequired(base_path) => {
Redirect::to(&format!("{}/auth/login", base_path)).into_response()
}
Self::InternalServerError(err) => err.into_response(),
}
}
}