Skip to content

Commit

Permalink
dim-web: whoami route migrated to axum router
Browse files Browse the repository at this point in the history
  • Loading branch information
niamu authored and mental32 committed Nov 7, 2023
1 parent 13b4083 commit 6689aad
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 73 deletions.
2 changes: 1 addition & 1 deletion dim-database/src/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ where
}
}

#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct User {
pub id: UserID,
pub username: String,
Expand Down
81 changes: 80 additions & 1 deletion dim-web/src/routes/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,14 @@
//! [`Unauthenticated`]: crate::errors::DimError::Unauthenticated
//! [`login`]: fn@login
use axum::extract;
use axum::{
extract,
Extension
};
use axum::response::IntoResponse;

use dim_database::asset::Asset;
use dim_database::progress::Progress;
use dim_database::user::verify;
use dim_database::user::InsertableUser;
use dim_database::user::Login;
Expand All @@ -33,6 +38,80 @@ use http::StatusCode;
use serde_json::json;
use thiserror::Error;


#[derive(Debug, Error)]
pub enum AuthError {
#[error("Not logged in.")]
InvalidCredentials,
#[error("database: {0}")]
Database(#[from] DatabaseError),
}

impl IntoResponse for AuthError {
fn into_response(self) -> axum::response::Response {
match self {
Self::InvalidCredentials => {
(StatusCode::UNAUTHORIZED, self.to_string()).into_response()
}
Self::Database(_) => {
(StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response()
}
}
}
}

/// # GET `/api/v1/user`
/// Method returns metadata about the currently logged in user.
///
/// # Request
/// This method takes in no additional parameters or data.
///
/// ## Authorization
/// This method requires a valid authentication token.
///
/// ## Example
/// ```text
/// curl -X GET http://127.0.0.1:8000/api/v1/user -H "Authorization: ..."
/// ```
///
/// # Response
/// This method will return a JSON payload with the following schema:
/// ```no_compile
/// {
/// "picture": Option<String>,
/// "spentWatching": i64,
/// "username": String,
/// "roles": [String]
/// }
/// ```
///
/// ## Example
/// ```no_compile
/// {
/// "picture": "/images/avatar.jpg",
/// "spentWatching": 12,
/// "username": "admin",
/// "roles": ["owner"],
/// }
/// ```
#[axum::debug_handler]
pub async fn whoami(
Extension(user): Extension<User>,
extract::State(conn): extract::State<DbConnection>,
) -> Result<axum::response::Response, AuthError> {
let mut tx = conn.read().begin().await.map_err(DatabaseError::from)?;

Ok(axum::response::Json(json!({
"picture": Asset::get_of_user(&mut tx, user.id).await.ok().map(|x| format!("/images/{}", x.local_path)),
"spentWatching": Progress::get_total_time_spent_watching(&mut tx, user.id)
.await
.unwrap_or(0) / 3600,
"username": user.username,
"roles": user.roles()
}))
.into_response())
}

#[derive(Debug, Error)]
pub enum LoginError {
#[error("The provided username or password is incorrect.")]
Expand Down
45 changes: 42 additions & 3 deletions dim/src/core.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
use crate::errors::DimError;
use crate::routes;
use crate::routes::*;
use crate::scanner;
use crate::stream_tracking::StreamTracking;

use dim_database::library::MediaType;
use dim_database::user::User;

use dim_extern_api::tmdb::TMDBMetadataProvider;

use dim_web::axum::extract::ConnectInfo;
Expand Down Expand Up @@ -141,17 +144,24 @@ pub async fn warp_core(
}

let router = dim_web::axum::Router::new()
// .route_service("/api/v1/auth/login", warp!(auth::filters::login))
.route(
"/api/v1/auth/whoami",
dim_web::axum::routing::get(dim_web::routes::auth::whoami)
.with_state(conn.clone()),
)
.route_layer(dim_web::axum::middleware::from_fn_with_state(
conn.clone(),
with_auth
))
// --- End of routes authenticated by Axum middleware ---
.route(
"/api/v1/auth/login",
dim_web::axum::routing::post(dim_web::routes::auth::login).with_state(conn.clone()),
)
// .route_service("/api/v1/auth/register", warp!(auth::filters::register))
.route(
"/api/v1/auth/register",
dim_web::axum::routing::post(dim_web::routes::auth::register).with_state(conn.clone()),
)
.route_service("/api/v1/auth/whoami", warp!(user::filters::whoami))
.route(
"/api/v1/auth/admin_exists",
dim_web::axum::routing::get(dim_web::routes::auth::admin_exists)
Expand Down Expand Up @@ -336,3 +346,32 @@ pub async fn warp_core(
}
}
}

pub async fn with_auth<B>(
State(conn): State<DbConnection>,
mut req: dim_web::axum::http::Request<B>,
next: dim_web::axum::middleware::Next<B>
) -> Result<dim_web::axum::response::Response, DimError> {
match req.headers().get(dim_web::axum::http::header::AUTHORIZATION) {
Some(token) => {
let mut tx = match conn.read().begin().await {
Ok(tx) => tx,
Err(_) => {
return Err(DimError::DatabaseError {
description: String::from("Failed to start transaction"),
})
}
};
let id = dim_database::user::Login::verify_cookie(token.to_str().unwrap().to_string())
.map_err(|e| DimError::CookieError(e))?;

let current_user = User::get_by_id(&mut tx, id)
.await
.map_err(|_| DimError::UserNotFound)?;

req.extensions_mut().insert(current_user);
Ok(next.run(req).await)
},
None => Err(DimError::NoToken),
}
}
36 changes: 36 additions & 0 deletions dim/src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use dim_database::DatabaseError;
use dim_web::axum::response::{IntoResponse, Response};
use displaydoc::Display;
use thiserror::Error;

Expand Down Expand Up @@ -137,6 +138,41 @@ impl warp::Reply for DimError {
}
}

impl IntoResponse for DimError {
fn into_response(self) -> Response {
let status = match self {
Self::LibraryNotFound
| Self::NoneError
| Self::NotFoundError
| Self::ExternalSearchError(_) => StatusCode::NOT_FOUND,
Self::StreamingError(_)
| Self::DatabaseError { .. }
| Self::UnknownError
| Self::IOError
| Self::InternalServerError
| Self::UploadFailed
| Self::ScannerError(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::Unauthenticated
| Self::Unauthorized
| Self::InvalidCredentials
| Self::CookieError(_)
| Self::NoToken
| Self::UserNotFound => StatusCode::UNAUTHORIZED,
Self::UsernameNotAvailable => StatusCode::BAD_REQUEST,
Self::UnsupportedFile | Self::InvalidMediaType | Self::MissingFieldInBody { .. } => {
StatusCode::NOT_ACCEPTABLE
}
Self::MediafileRouteError(ref e) => e.status_code(),
};

let resp = json!({
"error": json!(&self)["error"],
"messsage": self.to_string(),
});
(status, serde_json::to_string(&resp).unwrap()).into_response()
}
}

#[derive(Clone, Display, Debug, Error, Serialize)]
#[serde(tag = "error")]
pub enum StreamingErrors {
Expand Down
68 changes: 0 additions & 68 deletions dim/src/routes/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,64 +5,13 @@ use bytes::BufMut;

use dim_database::asset::Asset;
use dim_database::asset::InsertableAsset;
use dim_database::progress::Progress;
use dim_database::user::User;

use serde_json::json;

use warp::reply;

use http::StatusCode;

use futures::TryStreamExt;
use uuid::Uuid;

/// # GET `/api/v1/user`
/// Method returns metadata about the currently logged in user.
///
/// # Request
/// This method takes in no additional parameters or data.
///
/// ## Authorization
/// This method requires a valid authentication token.
///
/// ## Example
/// ```text
/// curl -X GET http://127.0.0.1:8000/api/v1/user -H "Authorization: ..."
/// ```
///
/// # Response
/// This method will return a JSON payload with the following schema:
/// ```no_compile
/// {
/// "picture": Option<String>,
/// "spentWatching": i64,
/// "username": String,
/// "roles": [String]
/// }
/// ```
///
/// ## Example
/// ```no_compile
/// {
/// "picture": "/images/avatar.jpg",
/// "spentWatching": 12,
/// "username": "admin",
/// "roles": ["owner"],
/// }
/// ```
pub async fn whoami(user: User, conn: DbConnection) -> Result<impl warp::Reply, errors::DimError> {
let mut tx = conn.read().begin().await?;

Ok(reply::json(&json!({
"picture": Asset::get_of_user(&mut tx, user.id).await.ok().map(|x| format!("/images/{}", x.local_path)),
"spentWatching": Progress::get_total_time_spent_watching(&mut tx, user.id)
.await
.unwrap_or(0) / 3600,
"username": user.username,
"roles": user.roles()
})))
}

/// # POST `/api/v1/user/password`
/// Method changes the password for a logged in account.
Expand Down Expand Up @@ -307,23 +256,6 @@ pub(crate) mod filters {
use super::super::global_filters::with_auth;
use super::super::global_filters::with_state;

pub fn whoami(
conn: DbConnection,
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
let path =
warp::path!("api" / "v1" / "user").or(warp::path!("api" / "v1" / "auth" / "whoami"));

path.unify()
.and(warp::get())
.and(with_auth(conn.clone()))
.and(with_state(conn))
.and_then(|auth: User, conn: DbConnection| async move {
super::whoami(auth, conn)
.await
.map_err(|e| reject::custom(e))
})
}

pub fn change_password(
conn: DbConnection,
) -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {
Expand Down

0 comments on commit 6689aad

Please sign in to comment.