diff options
Diffstat (limited to 'server/src')
-rw-r--r-- | server/src/db.rs | 6 | ||||
-rw-r--r-- | server/src/errors.rs | 14 | ||||
-rw-r--r-- | server/src/logger.rs | 1 | ||||
-rw-r--r-- | server/src/main.rs | 5 | ||||
-rw-r--r-- | server/src/models/auth.rs | 10 | ||||
-rw-r--r-- | server/src/models/user.rs | 9 | ||||
-rw-r--r-- | server/src/routes/auth.rs | 3 | ||||
-rw-r--r-- | server/src/routes/user.rs | 4 |
8 files changed, 52 insertions, 0 deletions
diff --git a/server/src/db.rs b/server/src/db.rs index 3c1467e..43c3bd9 100644 --- a/server/src/db.rs +++ b/server/src/db.rs @@ -2,8 +2,12 @@ use crate::errors::AppError; use sqlx::postgres::PgPool; +/// Static variable used to manage the database connection. Called with value = None raises a panic +/// error. static mut CONNECTION: Option<PgPool> = None; +/// Setup database connection. Get variable `DATABASE_URL` from the environment. Sqlx crate already +/// defines an error for environments without DATABASE_URL. pub async fn setup() -> Result<(), AppError> { let database_url = std::env::var("DATABASE_URL").expect("Define `DATABASE_URL` environment variable."); @@ -15,6 +19,8 @@ pub async fn setup() -> Result<(), AppError> { Ok(()) } +/// Get connection. Raises an error if `setup()` has not been called yet. +/// Managing static `CONNECTION` is an unsafe operation. pub unsafe fn get_client() -> &'static PgPool { match &CONNECTION { Some(client) => client, diff --git a/server/src/errors.rs b/server/src/errors.rs index 304d744..e541eda 100644 --- a/server/src/errors.rs +++ b/server/src/errors.rs @@ -5,16 +5,29 @@ use axum::{ }; use serde_json::json; +/// All errors raised by the web app pub enum AppError { + /// Generic error, never called yet Generic, + /// Database error Database, + /// Generic bad request. It is handled with a message value BadRequest(String), + /// Not found error NotFound, + /// Raised when a token is not good created TokenCreation, + /// Raised when a passed token is not valid InvalidToken, } +/// Use `AppError` as response for an endpoint impl IntoResponse for AppError { + /// Matches `AppError` into a tuple of status and error message. + /// The response will be a JSON in the format of: + /// ```json + /// { "error": "<message>" } + /// ``` fn into_response(self) -> Response { let (status, error_message) = match self { AppError::Generic => ( @@ -42,6 +55,7 @@ impl IntoResponse for AppError { } } +/// Transforms a `sqlx::Error` into a `AppError::Databse` error impl From<sqlx::Error> for AppError { fn from(_error: sqlx::Error) -> AppError { AppError::Database diff --git a/server/src/logger.rs b/server/src/logger.rs index fa569ee..718384a 100644 --- a/server/src/logger.rs +++ b/server/src/logger.rs @@ -1,5 +1,6 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +/// Setup tracing subscriber logger pub fn setup() { tracing_subscriber::registry() .with(tracing_subscriber::EnvFilter::new( diff --git a/server/src/main.rs b/server/src/main.rs index 8e44f7a..508d6cd 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -13,10 +13,12 @@ use tower_http::sensitive_headers::SetSensitiveHeadersLayer; use tower_http::{classify::ServerErrorsFailureClass, trace::TraceLayer}; use tracing::Span; +/// Main application, called by the execution of the software #[tokio::main] async fn main() { let app = create_app().await; + /// By default the server is bind at "127.0.0.1:3000" let addr = std::env::var("ALLOWED_HOST").unwrap_or_else(|_| "127.0.0.1:3000".to_string()); tracing::info!("Listening on {}", addr); @@ -26,6 +28,7 @@ async fn main() { .unwrap(); } +/// Create the app: setup everything and returns a `Router` async fn create_app() -> Router { logger::setup(); let _ = db::setup().await; @@ -35,12 +38,14 @@ async fn create_app() -> Router { .nest("/auth", routes::auth::create_route()); Router::new() + // Map all routes to `/v1/*` namespace .nest("/v1", api_routes) // Mark the `Authorization` request header as sensitive so it doesn't // show in logs. .layer(SetSensitiveHeadersLayer::new(std::iter::once( header::AUTHORIZATION, ))) + // Use a layer for `TraceLayer` .layer( TraceLayer::new_for_http() .on_request(|request: &Request<_>, _span: &Span| { diff --git a/server/src/models/auth.rs b/server/src/models/auth.rs index 573f5d1..8b8f61c 100644 --- a/server/src/models/auth.rs +++ b/server/src/models/auth.rs @@ -14,15 +14,21 @@ struct Keys { decoding: DecodingKey, } +/// Claims struct #[derive(Serialize, Deserialize)] pub struct Claims { + /// ID from the user model user_id: i32, + /// Expiration timestamp exp: usize, } +/// Body used as response to login #[derive(Serialize)] pub struct AuthBody { + /// Access token string access_token: String, + /// "Bearer" string token_type: String, } @@ -41,6 +47,7 @@ impl Keys { } impl Claims { + /// Create a new Claim using the `user_id` and the current timestamp + 2 days pub fn new(user_id: i32) -> Self { let expiration = Local::now() + Duration::days(2); @@ -50,6 +57,8 @@ impl Claims { } } + /// Returns the token as a string. If a token is not encoded, raises an + /// `AppError::TokenCreation` pub fn get_token(&self) -> Result<String, AppError> { let token = encode(&Header::default(), &self, &KEYS.encoding) .map_err(|_| AppError::TokenCreation)?; @@ -67,6 +76,7 @@ impl AuthBody { } } +/// Parse a request to get the Authorization header and then decode it checking its validation #[async_trait] impl<B> FromRequest<B> for Claims where diff --git a/server/src/models/user.rs b/server/src/models/user.rs index dd96f90..06cde0a 100644 --- a/server/src/models/user.rs +++ b/server/src/models/user.rs @@ -4,6 +4,7 @@ use crate::errors::AppError; use serde::{Deserialize, Serialize}; use validator::Validate; +/// User model #[derive(Deserialize, Serialize, Validate)] pub struct User { id: i32, @@ -14,13 +15,16 @@ pub struct User { is_staff: Option<bool>, } +/// Response used to print a user (or a users list) #[derive(Deserialize, Serialize)] pub struct UserList { + // It is public because it used by `Claims` creation pub id: i32, email: String, is_staff: Option<bool>, } +/// Payload used for user creation #[derive(Deserialize)] pub struct UserCreate { pub email: String, @@ -28,6 +32,7 @@ pub struct UserCreate { } impl User { + /// By default an user has id = 0. It is not created yet pub fn new(email: String, password: String) -> Self { Self { id: 0, @@ -37,6 +42,7 @@ impl User { } } + /// Create a new user from the model using a SHA256 crypted password pub async fn create(user: User) -> Result<UserList, AppError> { let pool = unsafe { get_client() }; @@ -61,6 +67,7 @@ impl User { Ok(rec) } + /// Find a user using the model. It used for login pub async fn find(user: User) -> Result<UserList, AppError> { let pool = unsafe { get_client() }; @@ -81,6 +88,7 @@ impl User { Ok(rec) } + /// Returns the user with id = `user_id` pub async fn find_by_id(user_id: i32) -> Result<UserList, AppError> { let pool = unsafe { get_client() }; @@ -98,6 +106,7 @@ impl User { Ok(rec) } + /// List all users pub async fn list() -> Result<Vec<UserList>, AppError> { let pool = unsafe { get_client() }; let rows = sqlx::query_as!(UserList, r#"SELECT id, email, is_staff FROM users"#) diff --git a/server/src/routes/auth.rs b/server/src/routes/auth.rs index 629ed33..37c41b2 100644 --- a/server/src/routes/auth.rs +++ b/server/src/routes/auth.rs @@ -5,10 +5,13 @@ use crate::models::{ }; use axum::{routing::post, Json, Router}; +/// Create routes for `/v1/auth/` namespace pub fn create_route() -> Router { Router::new().route("/login", post(make_login)) } +/// Make login. Check if a user with the email and password passed in request body exists into the +/// database async fn make_login(Json(payload): Json<UserCreate>) -> Result<Json<AuthBody>, AppError> { let user = User::new(payload.email, payload.password); match User::find(user).await { diff --git a/server/src/routes/user.rs b/server/src/routes/user.rs index 3ca0e7b..d44df66 100644 --- a/server/src/routes/user.rs +++ b/server/src/routes/user.rs @@ -5,18 +5,21 @@ use crate::models::{ }; use axum::{extract::Path, routing::get, Json, Router}; +/// Create routes for `/v1/users/` namespace pub fn create_route() -> Router { Router::new() .route("/", get(list_users).post(create_user)) .route("/:id", get(get_user)) } +/// List users. Checks Authorization token async fn list_users(_: Claims) -> Result<Json<Vec<UserList>>, AppError> { let users = User::list().await?; Ok(Json(users)) } +/// Create an user. Checks Authorization token async fn create_user( Json(payload): Json<UserCreate>, _: Claims, @@ -27,6 +30,7 @@ async fn create_user( Ok(Json(user_new)) } +/// Get an user with id = `user_id`. Checks Authorization token async fn get_user(Path(user_id): Path<i32>, _: Claims) -> Result<Json<UserList>, AppError> { match User::find_by_id(user_id).await { Ok(user) => Ok(Json(user)), |