diff --git a/src/api/v1/errors.rs b/src/api/v1/errors.rs new file mode 100644 index 0000000..daf9413 --- /dev/null +++ b/src/api/v1/errors.rs @@ -0,0 +1,48 @@ +use axum::{http::StatusCode, response::IntoResponse, Json}; +use serde_json::json; + +#[derive(Debug)] +pub enum AuthError { + InternalError(E), + MissingCredentials, + InvalidCredentials, + MissingToken, + InvalidToken, + MissingUser, +} + +impl IntoResponse for AuthError { + fn into_response(self) -> axum::response::Response { + let (status, message) = match self { + Self::InternalError(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()), + Self::MissingCredentials => { + (StatusCode::BAD_REQUEST, "Missing credentials".to_string()) + } + Self::InvalidCredentials => { + (StatusCode::UNAUTHORIZED, "Invalid credentials".to_string()) + } + Self::MissingToken => (StatusCode::BAD_REQUEST, "Missing token".to_string()), + Self::InvalidToken => (StatusCode::UNAUTHORIZED, "Invalid token".to_string()), + Self::MissingUser => (StatusCode::UNAUTHORIZED, "User not exists".to_string()), + }; + + Json(json!({ + "status": status.to_string(), + "message": message + })) + .into_response() + } +} + +pub fn internal_error(err: E) -> (StatusCode, Json) +where + E: std::error::Error, +{ + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({ + "status": StatusCode::INTERNAL_SERVER_ERROR.to_string(), + "message": err.to_string() + })), + ) +} diff --git a/src/api/v1/middleware.rs b/src/api/v1/middleware.rs new file mode 100644 index 0000000..1cf1393 --- /dev/null +++ b/src/api/v1/middleware.rs @@ -0,0 +1,53 @@ +use std::sync::Arc; + +use axum::{ + body::Body, + extract::{Request, State}, + http::header, + middleware::Next, + response::IntoResponse, +}; +use axum_extra::extract::CookieJar; + +use crate::{db::user::User, state::AppState}; + +use super::errors::AuthError; +use super::token::TokenClaims; + +pub async fn jwt_auth( + cookie_jar: CookieJar, + State(state): State>, + mut req: Request, + next: Next, +) -> Result> { + let token = cookie_jar + .get("token") + .map(|cookie| cookie.value().to_string()) + .or_else(|| { + req.headers() + .get(header::AUTHORIZATION) + .and_then(|auth_header| auth_header.to_str().ok()) + .and_then(|auth_value| { + if auth_value.starts_with("Bearer ") { + Some(auth_value[7..].to_owned()) + } else { + None + } + }) + }); + + let token = token.ok_or_else(|| AuthError::MissingToken)?; + let claims = TokenClaims::validate(token, state.config.jwt.secret.to_owned()) + .map_err(|_| AuthError::InvalidToken)?; + + let user_id = uuid::Uuid::parse_str(&claims.sub).map_err(|_| AuthError::InvalidToken)?; + + let user = User::find(&state.database, User::by_id(user_id)) + .await + .map_err(AuthError::InternalError)?; + + let user = user.ok_or_else(|| AuthError::MissingUser)?; + + req.extensions_mut().insert(user); + Ok(next.run(req).await) +} diff --git a/src/api/v1/mod.rs b/src/api/v1/mod.rs index 82bcf27..135f28c 100644 --- a/src/api/v1/mod.rs +++ b/src/api/v1/mod.rs @@ -1,300 +1,58 @@ -use crate::db::models::{FilteredUser, LoginUser, NewUser, RegisterUser, TokenClaims, User}; -use crate::error_handle::internal_error; -use crate::state::AppState; -use argon2::{password_hash::SaltString, Argon2}; -use argon2::{PasswordHash, PasswordHasher, PasswordVerifier}; -use axum::extract::Request; -use axum::Extension; -use axum::{ - body::Body, - extract::State, - http::{header, StatusCode}, - middleware::Next, - response::{IntoResponse, Response}, - Json, -}; -use axum_extra::extract::cookie::{self, Cookie, SameSite}; -use axum_extra::extract::CookieJar; -use diesel::{connection, prelude::*}; -use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; -use rand_core::OsRng; +pub mod errors; +pub mod middleware; +pub mod token; +pub mod user; + use std::sync::Arc; +use axum::{ + http::{header::*, Method, StatusCode}, + response::IntoResponse, + routing::{get, post}, + Json, Router, +}; +use serde_json::json; +use tower_http::cors::CorsLayer; + +use crate::state::AppState; + +pub fn routes(state: Arc) -> Router { + let cors = CorsLayer::new() + .allow_methods([Method::GET, Method::POST, Method::OPTIONS]) + .allow_headers(vec![ORIGIN, AUTHORIZATION, ACCEPT, CONTENT_TYPE, COOKIE]) + .allow_origin([ + "http://localhost:54600".parse().unwrap(), + "http://localhost:5173".parse().unwrap(), + ]) + .allow_credentials(true); + + let jwt = axum::middleware::from_fn_with_state(state.to_owned(), middleware::jwt_auth); + + Router::new() + .route("/v1/healthcheck", get(healthcheck)) + .route("/v1/user/register", post(user::register)) + .route("/v1/user/login", post(user::login)) + .route("/v1/user/logout", get(user::logout)) + .route("/v1/user/profile", get(user::profile).route_layer(jwt)) + .layer(cors) + .fallback(fallback) + .with_state(state) +} + pub async fn healthcheck() -> impl IntoResponse { - Json(serde_json::json!({ - "status": "success", - "message": "healthy" - })) -} - -pub async fn register_user( - State(state): State>, - Json(body): Json, -) -> Result)> { - use crate::db::schema::{users, users::dsl}; - - let connection = state.database.get().await.unwrap(); - let (login, email) = (body.login.clone(), body.email.clone()); - let user_exists = connection - .interact(move |connection| { - dsl::users - .filter(dsl::login.eq(login).or(dsl::email.eq(email))) - .select(User::as_select()) - .first(connection) - .optional() - }) - .await - .map_err(internal_error)? - .map_err(|e| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ - "status": "fail", - "message": format!("Database error: {}", e) - })), - ) - })?; - - if user_exists.is_some() { - return Err(( - StatusCode::CONFLICT, - Json(serde_json::json!({ - "status": "fail", - "message": "Login or email already exists" - })), - )); - } - - let salt = SaltString::generate(&mut OsRng); - let hashed_password = Argon2::default() - .hash_password(body.password.as_bytes(), &salt) - .map_err(|e| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ - "status": "fail", - "message": format!("Error while hashing password: {}", e) - })), - ) - }) - .map(|hash| hash.to_string())?; - - let user = NewUser { - login: body.login.to_string(), - hashed_password: hashed_password, - name: body.name, - email: body.email, - is_admin: body.is_admin, - }; - - let new_user = connection - .interact(move |connection| { - diesel::insert_into(users::table) - .values(&user) - .returning(User::as_returning()) - .get_result(connection) - }) - .await - .map_err(internal_error)? - .map_err(|e| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ - "status": "fail", - "message": format!("Database error: {}", e) - })), - ) - })?; - - let response = serde_json::json!({"status": "success", "data": serde_json::json!({"user": FilteredUser::from(&new_user)})}); - - Ok(Json(response)) -} - -pub async fn login_user( - State(state): State>, - Json(body): Json, -) -> Result)> { - use crate::db::schema::{users, users::dsl}; - - let connection = state.database.get().await.unwrap(); - let user = connection - .interact(move |connection| { - dsl::users - .filter(dsl::email.eq(body.email)) - .select(User::as_select()) - .first(connection) - }) - .await - .map_err(internal_error)? - .map_err(|e| { - ( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ - "status": "fail", - "message": format!("Invalid login or email: {}", e) - })), - ) - })?; - - let is_valid = match PasswordHash::new(&user.hashed_password) { - Ok(parsed_hash) => Argon2::default() - .verify_password(body.password.as_bytes(), &parsed_hash) - .map_or(false, |_| true), - Err(_) => false, - }; - - if !is_valid { - return Err(( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ - "status": "fail", - "message": "Invalid login, email or password" - })), - )); - } - - let now = chrono::Utc::now(); - let iat = now.timestamp() as usize; - let exp = (now + chrono::Duration::try_minutes(60).unwrap()).timestamp() as usize; - let claims = TokenClaims { - sub: user.id.to_string(), - exp, - iat, - }; - - let token = encode( - &Header::default(), - &claims, - &EncodingKey::from_secret(state.config.jwt.secret.as_ref()), + ( + StatusCode::OK, + Json(json!({ + "status": StatusCode::OK.to_string(), + })), ) - .unwrap(); - - let cookie = Cookie::build(("token", token.to_owned())) - .path("/") - .max_age(time::Duration::hours(1)) - .same_site(SameSite::None) - .secure(true) - .http_only(true); - - let mut response = - Response::new(serde_json::json!({"status": "success", "token": token}).to_string()); - response - .headers_mut() - .insert(header::SET_COOKIE, cookie.to_string().parse().unwrap()); - - Ok(response) } -pub async fn logout_user() -> Result)> { - let cookie = Cookie::build(("token", "")) - .path("/") - .max_age(time::Duration::hours(-1)) - .same_site(SameSite::None) - .secure(true) - .http_only(true); - - let mut response = Response::new(serde_json::json!({"status": "success"}).to_string()); - response - .headers_mut() - .insert(header::SET_COOKIE, cookie.to_string().parse().unwrap()); - - Ok(response) -} - -pub async fn jwt_auth( - cookie_jar: CookieJar, - State(state): State>, - mut req: Request, - next: Next, -) -> Result)> { - let token = cookie_jar - .get("token") - .map(|cookie| cookie.value().to_string()) - .or_else(|| { - req.headers() - .get(header::AUTHORIZATION) - .and_then(|auth_header| auth_header.to_str().ok()) - .and_then(|auth_value| { - if auth_value.starts_with("Bearer ") { - Some(auth_value[7..].to_owned()) - } else { - None - } - }) - }); - - let token = token.ok_or_else(|| { - ( - StatusCode::UNAUTHORIZED, - Json(serde_json::json!({ - "status": "fail", - "message": "Cannot login without token" - })), - ) - })?; - - let claims = decode::( - &token, - &DecodingKey::from_secret(state.config.jwt.secret.as_ref()), - &Validation::default(), +pub async fn fallback() -> impl IntoResponse { + ( + StatusCode::NOT_FOUND, + Json(json!({ + "status": StatusCode::NOT_FOUND.to_string(), + })), ) - .map_err(|_| { - ( - StatusCode::UNAUTHORIZED, - Json(serde_json::json!({"status":"fail","message":"Invalid token"})), - ) - })? - .claims; - - let user_id = uuid::Uuid::parse_str(&claims.sub).map_err(|_| { - ( - StatusCode::UNAUTHORIZED, - Json(serde_json::json!({"status":"fail","message":"Invalid token"})), - ) - })?; - - use crate::db::schema::{users, users::dsl}; - - let connection = state.database.get().await.unwrap(); - let user = connection - .interact(move |connection| { - dsl::users - .filter(dsl::id.eq(user_id)) - .select(User::as_select()) - .first(connection) - .optional() - }) - .await - .map_err(internal_error)? - .map_err(|e| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ - "status": "fail", - "message": format!("Database error: {}", e) - })), - ) - })?; - - let user = user.ok_or_else(|| { - ( - StatusCode::UNAUTHORIZED, - Json(serde_json::json!({ - "status": "fail", - "message": "The user belonging to this token no longer exists" - })), - ) - })?; - - req.extensions_mut().insert(user); - Ok(next.run(req).await) -} - -pub async fn me( - Extension(user): Extension, -) -> Result)> { - Ok(Json( - serde_json::json!({"status":"success","data":serde_json::json!({"user":FilteredUser::from(&user)})}), - )) } diff --git a/src/api/v1/token.rs b/src/api/v1/token.rs new file mode 100644 index 0000000..e2607dc --- /dev/null +++ b/src/api/v1/token.rs @@ -0,0 +1,36 @@ +use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct TokenClaims { + pub sub: String, + pub exp: usize, + pub iat: usize, +} + +impl TokenClaims { + pub fn create( + sub: String, + secret: String, + duration: i64, + ) -> Result { + let now = chrono::Utc::now(); + let iat = now.timestamp() as usize; + let exp = (now + chrono::Duration::try_seconds(duration).unwrap()).timestamp() as usize; + let claims = Self { sub, exp, iat }; + + encode( + &Header::default(), + &claims, + &EncodingKey::from_secret(secret.as_ref()), + ) + } + + pub fn validate(token: String, secret: String) -> Result { + Ok(decode::( + &token, + &DecodingKey::from_secret(secret.as_ref()), + &Validation::default(), + )? + .claims) + } +} diff --git a/src/api/v1/user.rs b/src/api/v1/user.rs new file mode 100644 index 0000000..5729449 --- /dev/null +++ b/src/api/v1/user.rs @@ -0,0 +1,142 @@ +use argon2::Argon2; +use argon2::{PasswordHash, PasswordVerifier}; +use axum::Extension; +use axum::{ + extract::State, + http::{header, StatusCode}, + response::IntoResponse, + Json, +}; +use axum_extra::extract::cookie::{Cookie, SameSite}; +use serde_json::json; +use std::sync::Arc; + +use crate::db::user::User; +use crate::state::AppState; + +use super::errors::AuthError; +use super::token::TokenClaims; + +#[derive(serde::Deserialize)] +pub struct RegisterUser { + pub login: String, + pub password: String, + pub name: String, + pub email: String, + pub is_admin: bool, +} + +#[derive(serde::Serialize)] +pub struct FilteredUser { + pub id: String, + pub name: String, + pub email: String, + pub is_admin: bool, +} + +impl FilteredUser { + pub fn from(user: &User) -> Self { + FilteredUser { + id: user.id.to_string(), + name: user.name.to_owned(), + email: user.email.to_owned(), + is_admin: user.is_admin, + } + } +} + +#[derive(serde::Deserialize)] +pub struct LoginUser { + pub email: String, + pub password: String, +} + +pub async fn register( + State(state): State>, + Json(body): Json, +) -> Result> { + let user = User::register( + &state.database, + body.login, + body.password, + body.name, + body.email, + body.is_admin, + ) + .await + .map_err(AuthError::InternalError)?; + + Ok(Json(json!({ + "status": "success", + "user": FilteredUser::from(&user) + }))) +} + +pub async fn login( + State(state): State>, + Json(body): Json, +) -> Result> { + let user = User::find(&state.database, User::by_email(body.email)) + .await + .map_err(AuthError::InternalError)?; + + let user = match user { + Some(user) => user, + None => return Err(AuthError::InvalidCredentials), + }; + + if !match PasswordHash::new(&user.hashed_password) { + Ok(parsed_hash) => Argon2::default() + .verify_password(body.password.as_bytes(), &parsed_hash) + .map_or(false, |_| true), + Err(_) => false, + } { + return Err(AuthError::InvalidCredentials); + } + + let token = TokenClaims::create( + user.id.to_string(), + state.config.jwt.secret.to_owned(), + state.config.jwt.maxage, + ) + .unwrap(); + + let cookie = Cookie::build(("token", token.to_owned())) + .path("/") + .max_age(time::Duration::hours(1)) + .same_site(SameSite::None) + .secure(true) + .http_only(true); + + let mut response = + Json(json!({"status": StatusCode::OK.to_string(), "token": token})).into_response(); + response + .headers_mut() + .insert(header::SET_COOKIE, cookie.to_string().parse().unwrap()); + + Ok(response) +} + +pub async fn logout() -> Result)> { + let cookie = Cookie::build(("token", "")) + .path("/") + .max_age(time::Duration::hours(-1)) + .same_site(SameSite::None) + .secure(true) + .http_only(true); + + let mut response = Json(json!({"status": StatusCode::OK.to_string()})).into_response(); + response + .headers_mut() + .insert(header::SET_COOKIE, cookie.to_string().parse().unwrap()); + + Ok(response) +} + +pub async fn profile( + Extension(user): Extension, +) -> Result)> { + Ok(Json( + json!({"status":"success","user":json!(FilteredUser::from(&user))}), + )) +} diff --git a/src/config.rs b/src/config.rs index d247365..0072100 100644 --- a/src/config.rs +++ b/src/config.rs @@ -27,7 +27,7 @@ pub struct Server { pub struct Jwt { pub secret: String, pub expires_in: String, - pub maxage: i32, + pub maxage: i64, } impl Config { @@ -56,7 +56,7 @@ impl Config { secret: env::var("JWT_SECRET").unwrap_or("change_this_secret".to_string()), expires_in: env::var("JWT_EXPIRES_IN").unwrap_or("60m".to_string()), maxage: env::var("JWT_MAXAGE") - .unwrap_or("60".to_string()) + .unwrap_or("3600".to_string()) .parse() .unwrap(), }, diff --git a/src/db/errors.rs b/src/db/errors.rs new file mode 100644 index 0000000..ab7308e --- /dev/null +++ b/src/db/errors.rs @@ -0,0 +1,63 @@ +use deadpool_diesel::postgres::PoolError; + +#[derive(Debug)] +pub enum DatabaseError { + Connection(E), + Interaction, + Operation(E), + Migration, +} + +impl std::fmt::Display for DatabaseError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::Connection(e) => write!(f, "Failed to connect to database: {}", e), + Self::Interaction => write!(f, "Failed to interact with database"), + Self::Operation(e) => write!(f, "Failed operation: {}", e), + Self::Migration => write!(f, "Failed to run migrations"), + } + } +} + +impl std::error::Error for DatabaseError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Interaction | Self::Migration => None, + Self::Connection(e) => Some(e), + Self::Operation(e) => Some(e), + } + } +} + +impl From for DatabaseError { + fn from(e: PoolError) -> Self { + Self::Connection(e) + } +} + +#[derive(Debug)] +pub enum UserError { + Query, + Exists, + NotFound, + HashPassword, +} + +impl std::fmt::Display for UserError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::Query => write!(f, "User query failed"), + UserError::Exists => write!(f, "User already exists"), + UserError::NotFound => write!(f, "User not found"), + UserError::HashPassword => write!(f, "Failed to hash user password"), + } + } +} + +impl std::error::Error for UserError {} + +impl From for DatabaseError { + fn from(e: UserError) -> Self { + Self::Operation(e) + } +} diff --git a/src/db/mod.rs b/src/db/mod.rs index 545a402..091e46c 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,12 +1,12 @@ -pub mod models; +pub mod errors; pub mod schema; +pub mod user; use deadpool_diesel::postgres::Manager; pub use deadpool_diesel::postgres::Pool; -use diesel::prelude::*; use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; -use crate::db::models::{NewUser, User}; +use errors::DatabaseError; pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("src/db/migrations/"); @@ -16,45 +16,12 @@ pub fn create_pool(database_url: String) -> Pool { Pool::builder(manager).build().unwrap() } -pub async fn run_migrations(pool: &Pool) { - let conn = pool.get().await.unwrap(); - conn.interact(|conn| conn.run_pending_migrations(MIGRATIONS).map(|_| ())) +pub async fn run_migrations(pool: &Pool) -> Result<(), DatabaseError> { + let connection = pool.get().await.map_err(DatabaseError::Connection)?; + connection + .interact(move |connection| connection.run_pending_migrations(MIGRATIONS).map(|_| ())) .await - .unwrap() - .unwrap(); -} - -pub fn create_user( - connection: &mut PgConnection, - login: String, - hashed_password: String, - name: String, - email: String, - is_admin: bool, -) -> User { - use crate::db::schema::users; - - let new_user = NewUser { - login, - hashed_password, - name, - email, - is_admin, - }; - - diesel::insert_into(users::table) - .values(&new_user) - .returning(User::as_returning()) - .get_result(connection) - .expect("Error creating new user") -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - unimplemented!(); - } + .map_err(|_| DatabaseError::Interaction)? + .map_err(|_| DatabaseError::Migration)?; + Ok(()) } diff --git a/src/db/models.rs b/src/db/models.rs deleted file mode 100644 index 8cc3df4..0000000 --- a/src/db/models.rs +++ /dev/null @@ -1,65 +0,0 @@ -use crate::db::schema; -use diesel::prelude::*; - -#[derive(serde::Serialize, Queryable, Selectable, Clone)] -#[diesel(table_name = schema::users)] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct User { - pub id: uuid::Uuid, - pub login: String, - pub hashed_password: String, - pub name: String, - pub email: String, - pub is_admin: bool, -} - -#[derive(serde::Deserialize, Insertable)] -#[diesel(table_name = schema::users)] -pub struct NewUser { - pub login: String, - pub hashed_password: String, - pub name: String, - pub email: String, - pub is_admin: bool, -} - -#[derive(serde::Deserialize)] -pub struct RegisterUser { - pub login: String, - pub password: String, - pub name: String, - pub email: String, - pub is_admin: bool, -} - -#[derive(serde::Serialize)] -pub struct FilteredUser { - pub id: String, - pub name: String, - pub email: String, - pub is_admin: bool, -} - -impl FilteredUser { - pub fn from(user: &User) -> Self { - FilteredUser { - id: user.id.to_string(), - name: user.name.to_owned(), - email: user.email.to_owned(), - is_admin: user.is_admin, - } - } -} - -#[derive(serde::Deserialize)] -pub struct LoginUser { - pub email: String, - pub password: String, -} - -#[derive(serde::Serialize, serde::Deserialize)] -pub struct TokenClaims { - pub sub: String, - pub exp: usize, - pub iat: usize, -} diff --git a/src/db/user.rs b/src/db/user.rs new file mode 100644 index 0000000..8c04013 --- /dev/null +++ b/src/db/user.rs @@ -0,0 +1,121 @@ +use crate::db::{errors::*, schema::users, Pool}; +use argon2::{password_hash::SaltString, Argon2, PasswordHasher}; +use diesel::{ + dsl::{AsSelect, SqlTypeOf}, + pg::Pg, + prelude::*, +}; +use rand_core::OsRng; + +#[derive(serde::Serialize, Queryable, Selectable, Clone)] +#[diesel(table_name = users)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct User { + pub id: uuid::Uuid, + pub login: String, + pub hashed_password: String, + pub name: String, + pub email: String, + pub is_admin: bool, +} + +#[derive(serde::Deserialize, Insertable)] +#[diesel(table_name = users)] +pub struct NewUser { + pub login: String, + pub hashed_password: String, + pub name: String, + pub email: String, + pub is_admin: bool, +} + +type SqlType = SqlTypeOf>; +type BoxedQuery<'a> = users::BoxedQuery<'a, Pg, SqlType>; + +impl User { + pub async fn register( + pool: &Pool, + login: String, + password: String, + name: String, + email: String, + is_admin: bool, + ) -> Result> { + let user = User::find(pool, User::by_email(email.to_owned())) + .await + .map_err(|_| UserError::Query)?; + + if user.is_some() { + return Err(DatabaseError::Operation(UserError::Exists)); + } + + let hashed_password = Argon2::default() + .hash_password(password.as_bytes(), &SaltString::generate(&mut OsRng)) + .map_err(|_| UserError::HashPassword) + .map(|hash| hash.to_string())?; + + let new_user = NewUser { + login, + hashed_password, + name, + email, + is_admin, + }; + + let user = User::create(pool, new_user) + .await + .map_err(|_| UserError::Query)?; + + Ok(user) + } + + pub async fn create( + pool: &Pool, + new_user: NewUser, + ) -> Result> { + let connection = pool.get().await.map_err(DatabaseError::Connection)?; + let user = connection + .interact(move |connection| { + diesel::insert_into(users::table) + .values(new_user) + .returning(User::as_returning()) + .get_result(connection) + }) + .await + .map_err(|_| DatabaseError::Interaction)? + .map_err(|_| DatabaseError::Interaction)?; + + Ok(user) + } + + pub fn by_email(email: String) -> BoxedQuery<'static> { + users::table + .select(User::as_select()) + .into_boxed() + .filter(users::email.eq(email)) + } + + pub fn by_id(id: uuid::Uuid) -> BoxedQuery<'static> { + users::table + .select(User::as_select()) + .into_boxed() + .filter(users::id.eq(id)) + } + + pub async fn find( + pool: &Pool, + query: BoxedQuery<'static>, + ) -> Result, DatabaseError> { + let connection = pool.get().await.map_err(DatabaseError::Connection)?; + let user = connection + .interact(move |connection| query.first(connection).optional()) + .await + .map_err(|_| DatabaseError::Interaction)? + .map_err(|_| DatabaseError::Interaction)?; + + Ok(user) + } + + #[allow(unused_variables)] + pub fn remove(pool: Pool, email: String) {} +} diff --git a/src/main.rs b/src/main.rs index 9c775ee..1d227d4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,28 +5,18 @@ pub mod error_handle; pub mod state; use axum::{ - extract::{Path, State}, - http::{ - header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE, COOKIE, ORIGIN}, - HeaderValue, Method, StatusCode, Uri, - }, - middleware, - response::{IntoResponse, Json, Response}, - routing::{get, post}, + extract::Path, + http::{header::CONTENT_TYPE, StatusCode, Uri}, + response::{IntoResponse, Response}, + routing::get, Router, }; -use diesel::RunQueryDsl; use std::net::SocketAddr; use std::sync::Arc; -use std::{env, net::Ipv4Addr}; -use tower_http::{ - cors::{Any, CorsLayer}, - trace::{self, TraceLayer}, -}; +use tower_http::trace::{self, TraceLayer}; use tracing::Level; use crate::config::Config; -use crate::db::{create_user, models::User}; use crate::error_handle::*; use crate::state::AppState; @@ -63,38 +53,16 @@ async fn main() { let lister = tokio::net::TcpListener::bind(&address).await.unwrap(); - let cors = CorsLayer::new() - .allow_methods([Method::GET, Method::POST, Method::OPTIONS]) - .allow_headers(vec![ORIGIN, AUTHORIZATION, ACCEPT, CONTENT_TYPE, COOKIE]) - .allow_origin([ - "http://localhost:54600".parse().unwrap(), - "http://localhost:5173".parse().unwrap(), - ]) //Any) - .allow_credentials(true); //"http://localhost:5173".parse::().unwrap()); - let app = Router::new() .route("/", get(home)) .route("/user/login", get(user_login)) .route("/assets/*file", get(static_handler)) - .route("/api/v1/register_user", post(api::v1::register_user)) - .route("/api/v1/login_user", post(api::v1::login_user)) - .route( - "/api/v1/me", - get(api::v1::me).route_layer(middleware::from_fn_with_state( - state.clone(), - api::v1::jwt_auth, - )), - ) - .layer(cors) - .route("/api/v1/healthcheck", get(api::v1::healthcheck)) - .route("/api/v1/users", get(users)) - .route("/api/v1/logout_user", get(api::v1::logout_user)) + .nest("/api", api::v1::routes(state)) .layer( TraceLayer::new_for_http() .make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO)) .on_response(trace::DefaultOnResponse::new().level(Level::INFO)), - ) - .with_state(state); + ); println!("listening on http://{}", address); @@ -114,22 +82,6 @@ async fn user_login() -> impl IntoResponse { async fn user(Path(user): Path) -> impl IntoResponse {} -async fn users( - State(state): State>, -) -> Result>, (StatusCode, Json)> { - use db::schema::users::dsl::*; - - let conn = state.database.get().await.unwrap(); - - let result = conn - .interact(move |conn| users.load(conn)) - .await - .map_err(internal_error)? - .map_err(internal_error)?; - - Ok(Json(result)) -} - async fn static_handler(uri: Uri) -> impl IntoResponse { let mut path = uri.path().trim_start_matches('/').to_string(); @@ -158,21 +110,3 @@ where } } } - -/* - create_user( - connection, - "L-Nafaryus", - "asdasd", - "L-Nafaryus", - "l.nafaryus@elnafo.ru", - true, - ); - - let results = users - .select(User::as_select()) - .load(connection) - .expect("Error loading users"); - - println!("Found {} users", results.len()); -*/