shoutdotdev/src/users.rs

186 lines
6.2 KiB
Rust
Raw Normal View History

2025-02-26 13:10:47 -08:00
use anyhow::Context;
use async_session::{Session, SessionStore as _};
use axum::{
extract::{FromRequestParts, OriginalUri},
http::{request::Parts, Method},
response::{IntoResponse, Redirect, Response},
RequestPartsExt,
};
use axum_extra::extract::{
cookie::{Cookie, SameSite},
CookieJar,
};
2025-02-26 13:10:50 -08:00
use diesel::{
2025-02-26 13:10:48 -08:00
associations::Identifiable,
deserialize::Queryable,
2025-02-26 13:10:47 -08:00
dsl::{auto_type, insert_into, AsSelect, Eq, Select},
2025-02-26 13:10:48 -08:00
pg::Pg,
prelude::*,
2025-02-26 13:10:50 -08:00
Selectable,
};
use uuid::Uuid;
2025-02-26 13:10:47 -08:00
use crate::{
app_error::AppError,
app_state::AppState,
auth::{AuthInfo, SESSION_KEY_AUTH_INFO, SESSION_KEY_AUTH_REDIRECT},
2025-02-26 13:10:47 -08:00
schema::{team_memberships, teams, users},
sessions::AppSession,
2025-02-26 13:10:47 -08:00
team_memberships::TeamMembership,
teams::Team,
};
2025-02-26 13:10:50 -08:00
#[derive(Clone, Debug, Identifiable, Insertable, Queryable, Selectable)]
2025-02-26 13:10:47 -08:00
#[diesel(table_name = users)]
2025-02-26 13:10:50 -08:00
#[diesel(check_for_backend(Pg))]
pub struct User {
pub id: Uuid,
pub uid: String,
pub email: String,
}
2025-02-26 13:10:48 -08:00
impl User {
2025-02-26 13:10:47 -08:00
pub fn all() -> Select<users::table, AsSelect<User, Pg>> {
users::table.select(User::as_select())
}
2025-03-14 13:04:57 -07:00
#[auto_type(no_type_alias)]
pub fn with_uid(uid_value: &str) -> _ {
2025-02-26 13:10:47 -08:00
users::uid.eq(uid_value)
2025-02-26 13:10:48 -08:00
}
2025-02-26 13:10:47 -08:00
#[auto_type(no_type_alias)]
2025-03-14 13:04:57 -07:00
pub fn team_memberships(&self) -> _ {
let user_id_filter: Eq<team_memberships::user_id, &Uuid> =
TeamMembership::with_user_id(&self.id);
2025-02-26 13:10:47 -08:00
let select: AsSelect<(TeamMembership, Team), Pg> = <(TeamMembership, Team)>::as_select();
team_memberships::table
.inner_join(teams::table)
.filter(user_id_filter)
.select(select)
2025-02-26 13:10:48 -08:00
}
}
2025-02-26 13:10:50 -08:00
#[derive(Clone, Debug)]
pub struct CurrentUser(pub User);
impl<S> FromRequestParts<S> for CurrentUser
where
S: Into<AppState> + Clone + Sync,
{
type Rejection = CurrentUserRejection;
2025-02-26 13:10:50 -08:00
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let app_state: AppState = state.clone().into();
let mut session =
if let AppSession(Some(value)) = parts.extract_with_state(&app_state).await? {
value
} else {
Session::new()
};
let auth_info = if let Some(value) = session.get::<AuthInfo>(SESSION_KEY_AUTH_INFO) {
value
} else {
let jar: CookieJar = parts.extract().await?;
let method: Method = parts.extract().await?;
let jar = if method == Method::GET {
let OriginalUri(uri) = parts.extract().await?;
session.insert(
SESSION_KEY_AUTH_REDIRECT,
uri.path_and_query()
.map(|value| value.to_string())
.unwrap_or(format!("{}/", app_state.settings.base_path)),
)?;
if let Some(cookie_value) = app_state.session_store.store_session(session).await? {
tracing::debug!("adding session cookie to jar");
jar.add(
Cookie::build((app_state.settings.auth.cookie_name.clone(), cookie_value))
.same_site(SameSite::Lax)
.http_only(true)
.path("/"),
)
} else {
tracing::debug!("inferred that session cookie already in jar");
jar
}
} else {
// If request method is not GET then do not attempt to infer the
// redirect target, as there may be no GET handler defined for
// it.
jar
};
return Err(Self::Rejection::SetCookiesAndRedirect(
jar,
format!("{}/auth/login", app_state.settings.base_path),
));
};
let db_conn = app_state.db_pool.get().await?;
let current_user = db_conn
2025-02-26 13:10:50 -08:00
.interact(move |conn| {
2025-02-26 13:10:48 -08:00
let maybe_current_user = User::all()
.filter(User::with_uid(&auth_info.sub))
2025-02-26 13:10:50 -08:00
.first(conn)
2025-02-26 13:10:47 -08:00
.optional()
.context("failed to load maybe_current_user")?;
2025-02-26 13:10:50 -08:00
if let Some(current_user) = maybe_current_user {
return Ok(current_user);
}
let new_user = User {
id: Uuid::now_v7(),
2025-02-26 13:10:47 -08:00
uid: auth_info.sub.clone(),
2025-02-26 13:10:50 -08:00
email: auth_info.email,
};
2025-02-26 13:10:47 -08:00
match insert_into(users::table)
2025-02-26 13:10:48 -08:00
.values(new_user)
2025-02-26 13:10:47 -08:00
.on_conflict(users::uid)
2025-02-26 13:10:50 -08:00
.do_nothing()
2025-02-26 13:10:48 -08:00
.returning(User::as_returning())
2025-02-26 13:10:50 -08:00
.get_result(conn)
2025-02-26 13:10:47 -08:00
{
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),
}
2025-02-26 13:10:50 -08:00
})
.await
2025-03-14 13:04:57 -07:00
.unwrap()?;
2025-02-26 13:10:50 -08:00
Ok(CurrentUser(current_user))
}
}
pub enum CurrentUserRejection {
AppError(AppError),
SetCookiesAndRedirect(CookieJar, String),
}
// Easily convert semi-arbitrary errors to InternalServerError
impl<E> From<E> for CurrentUserRejection
where
E: Into<AppError>,
{
fn from(err: E) -> Self {
Self::AppError(err.into())
}
}
impl IntoResponse for CurrentUserRejection {
fn into_response(self) -> Response {
match self {
Self::AppError(err) => err.into_response(),
Self::SetCookiesAndRedirect(jar, redirect_to) => {
(jar, Redirect::to(&redirect_to)).into_response()
}
}
}
}