From f802d0c4e531c1f8a46bfd9c536dd9e3d1feb713 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Wo=C5=BAniak?= Date: Fri, 27 Mar 2020 16:17:25 +0100 Subject: [PATCH] Add queries --- docker-compose.yml | 1 - jirs-server/{migrations => }/seed.sql | 2 +- jirs-server/src/db/authorize_user.rs | 38 +++++++-- jirs-server/src/db/issues.rs | 33 ++++++++ jirs-server/src/db/mod.rs | 13 ++- jirs-server/src/db/projects.rs | 32 ++++++++ jirs-server/src/db/users.rs | 41 ++++++++++ jirs-server/src/errors.rs | 37 +++++++++ jirs-server/src/main.rs | 14 ++++ jirs-server/src/middleware/authorize.rs | 104 +++++++++++------------- jirs-server/src/models.rs | 13 ++- jirs-server/src/routes/projects.rs | 16 ++++ jirs-server/src/routes/users.rs | 25 +++++- jirs-server/src/schema.rs | 8 +- 14 files changed, 302 insertions(+), 75 deletions(-) rename jirs-server/{migrations => }/seed.sql (91%) create mode 100644 jirs-server/src/db/issues.rs create mode 100644 jirs-server/src/db/projects.rs create mode 100644 jirs-server/src/db/users.rs create mode 100644 jirs-server/src/errors.rs diff --git a/docker-compose.yml b/docker-compose.yml index e6a6fde9..95e75d62 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,4 +5,3 @@ services: image: postgres:latest ports: - 5432:5432 - diff --git a/jirs-server/migrations/seed.sql b/jirs-server/seed.sql similarity index 91% rename from jirs-server/migrations/seed.sql rename to jirs-server/seed.sql index 39dee70d..4051ce5d 100644 --- a/jirs-server/migrations/seed.sql +++ b/jirs-server/seed.sql @@ -1,4 +1,4 @@ INSERT INTO projects (name) values ('initial'); INSERT INTO users (project_id, email, name) values (1, 'foo', 'bar'); INSERT INTO tokens (user_id, access_token, refresh_token) values (1, uuid_generate_v4(), uuid_generate_v4() ); - +SELECT * FROM tokens; diff --git a/jirs-server/src/db/authorize_user.rs b/jirs-server/src/db/authorize_user.rs index ce2756d1..b5ad3aea 100644 --- a/jirs-server/src/db/authorize_user.rs +++ b/jirs-server/src/db/authorize_user.rs @@ -1,4 +1,5 @@ -use crate::db::DbExecutor; +use crate::db::{DbExecutor, DbPool, SyncQuery}; +use crate::errors::ServiceErrors; use crate::models::{Token, User}; use actix::{Handler, Message}; use diesel::prelude::*; @@ -10,25 +11,50 @@ pub struct AuthorizeUser { } impl Message for AuthorizeUser { - type Result = Result; + type Result = Result; } impl Handler for DbExecutor { - type Result = Result; + type Result = Result; fn handle(&mut self, msg: AuthorizeUser, _: &mut Self::Context) -> Self::Result { use crate::schema::tokens::dsl::{access_token, tokens}; use crate::schema::users::dsl::{id, users}; - let conn: &PgConnection = &self.0.get().unwrap(); + let conn = &self + .0 + .get() + .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; let token = tokens .filter(access_token.eq(msg.access_token)) .first::(conn) - .map_err(|e| format!("{}", e))?; + .map_err(|e| ServiceErrors::RecordNotFound("Token".to_string()))?; let user = users .filter(id.eq(token.user_id)) .first::(conn) - .map_err(|e| format!("{}", e))?; + .map_err(|e| ServiceErrors::RecordNotFound("User".to_string()))?; + Ok(user) + } +} + +impl SyncQuery for AuthorizeUser { + type Result = std::result::Result; + + fn handle(&self, pool: &DbPool) -> Self::Result { + use crate::schema::tokens::dsl::{access_token, tokens}; + use crate::schema::users::dsl::{id, users}; + + let conn = pool + .get() + .map_err(|_| crate::errors::ServiceErrors::DatabaseConnectionLost)?; + let token = tokens + .filter(access_token.eq(self.access_token)) + .first::(&conn) + .map_err(|_| crate::errors::ServiceErrors::Unauthorized)?; + let user = users + .filter(id.eq(token.user_id)) + .first::(&conn) + .map_err(|_| crate::errors::ServiceErrors::Unauthorized)?; Ok(user) } } diff --git a/jirs-server/src/db/issues.rs b/jirs-server/src/db/issues.rs new file mode 100644 index 00000000..9fa753e6 --- /dev/null +++ b/jirs-server/src/db/issues.rs @@ -0,0 +1,33 @@ +use crate::db::DbExecutor; +use crate::errors::ServiceErrors; +use crate::models::Issue; +use actix::{Handler, Message}; +use diesel::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct LoadProjectIssues { + pub project_id: i32, +} + +impl Message for LoadProjectIssues { + type Result = Result, ServiceErrors>; +} + +impl Handler for DbExecutor { + type Result = Result, ServiceErrors>; + + fn handle(&mut self, msg: LoadProjectIssues, _ctx: &mut Self::Context) -> Self::Result { + use crate::schema::issues::dsl::{issues, project_id}; + let conn = &self + .0 + .get() + .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; + let vec = issues + .filter(project_id.eq(msg.project_id)) + .distinct() + .load::(conn) + .map_err(|_| ServiceErrors::RecordNotFound("project issues".to_string()))?; + Ok(vec) + } +} diff --git a/jirs-server/src/db/mod.rs b/jirs-server/src/db/mod.rs index 707c5d19..a8eb5eac 100644 --- a/jirs-server/src/db/mod.rs +++ b/jirs-server/src/db/mod.rs @@ -3,8 +3,11 @@ use diesel::pg::PgConnection; use diesel::r2d2::{self, ConnectionManager}; pub mod authorize_user; +pub mod issues; +pub mod projects; +pub mod users; -type DbPool = r2d2::Pool>; +pub type DbPool = r2d2::Pool>; pub struct DbExecutor(pub DbPool); @@ -26,5 +29,11 @@ pub fn build_pool() -> DbPool { let manager = ConnectionManager::::new(database_url); r2d2::Pool::builder() .build(manager) - .expect("Failed to create pool.") + .unwrap_or_else(|e| panic!("Failed to create pool. {}", e)) +} + +pub trait SyncQuery { + type Result; + + fn handle(&self, pool: &DbPool) -> Self::Result; } diff --git a/jirs-server/src/db/projects.rs b/jirs-server/src/db/projects.rs new file mode 100644 index 00000000..f65e6fb4 --- /dev/null +++ b/jirs-server/src/db/projects.rs @@ -0,0 +1,32 @@ +use crate::db::DbExecutor; +use crate::errors::ServiceErrors; +use crate::models::Project; +use actix::{Handler, Message}; +use diesel::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct LoadCurrentProject { + pub project_id: i32, +} + +impl Message for LoadCurrentProject { + type Result = Result; +} + +impl Handler for DbExecutor { + type Result = Result; + + fn handle(&mut self, msg: LoadCurrentProject, _ctx: &mut Self::Context) -> Self::Result { + use crate::schema::projects::dsl::{id, projects}; + let conn = &self + .0 + .get() + .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; + let project = projects + .filter(id.eq(msg.project_id)) + .first::(conn) + .map_err(|_| ServiceErrors::RecordNotFound("Project".to_string()))?; + Ok(project) + } +} diff --git a/jirs-server/src/db/users.rs b/jirs-server/src/db/users.rs new file mode 100644 index 00000000..9f0d028e --- /dev/null +++ b/jirs-server/src/db/users.rs @@ -0,0 +1,41 @@ +use crate::db::DbExecutor; +use crate::errors::ServiceErrors; +use crate::models::{Issue, User}; +use actix::{Handler, Message}; +use chrono::NaiveDateTime; +use diesel::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct LoadProjectUsers { + pub project_id: i32, +} + +impl Message for LoadProjectUsers { + type Result = Result, ServiceErrors>; +} + +impl Handler for DbExecutor { + type Result = Result, ServiceErrors>; + + fn handle(&mut self, msg: LoadProjectUsers, _ctx: &mut Self::Context) -> Self::Result { + use crate::schema::issues::dsl::{issues, project_id, reporter_id}; + use crate::schema::users::dsl::*; + + let conn = &self + .0 + .get() + .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; + let rows: Vec<(User, Option)> = users + .distinct() + .left_outer_join(issues.on(reporter_id.eq(id))) + .filter(project_id.eq(msg.project_id)) + .load(conn) + .map_err(|_| ServiceErrors::RecordNotFound("project issues".to_string()))?; + let mut vec: Vec = vec![]; + for row in rows { + vec.push(row.0); + } + Ok(vec) + } +} diff --git a/jirs-server/src/errors.rs b/jirs-server/src/errors.rs new file mode 100644 index 00000000..f896cc8c --- /dev/null +++ b/jirs-server/src/errors.rs @@ -0,0 +1,37 @@ +use crate::models::ErrorResponse; +use actix_web::HttpResponse; + +const TOKEN_NOT_FOUND: &str = "Token not found"; +const DATABASE_CONNECTION_FAILED: &str = "Database connection failed"; + +pub enum ServiceErrors { + Unauthorized, + DatabaseConnectionLost, + RecordNotFound(String), +} + +impl ServiceErrors { + pub fn into_http_response(self) -> HttpResponse { + self.into() + } +} + +impl Into for ServiceErrors { + fn into(self) -> HttpResponse { + match self { + ServiceErrors::Unauthorized => HttpResponse::Unauthorized().json(ErrorResponse { + errors: vec![TOKEN_NOT_FOUND.to_owned()], + }), + ServiceErrors::DatabaseConnectionLost => { + HttpResponse::InternalServerError().json(ErrorResponse { + errors: vec![DATABASE_CONNECTION_FAILED.to_owned()], + }) + } + ServiceErrors::RecordNotFound(resource_name) => { + HttpResponse::BadRequest().json(ErrorResponse { + errors: vec![format!("Resource not found {}", resource_name)], + }) + } + } + } +} diff --git a/jirs-server/src/main.rs b/jirs-server/src/main.rs index 3c2ee632..7e280e7f 100644 --- a/jirs-server/src/main.rs +++ b/jirs-server/src/main.rs @@ -5,6 +5,7 @@ use actix_cors::Cors; use actix_web::{web, App, HttpServer}; pub mod db; +pub mod errors; pub mod middleware; pub mod models; pub mod routes; @@ -23,6 +24,7 @@ async fn main() -> Result<(), String> { .wrap(actix_web::middleware::Logger::default()) .wrap(Cors::default()) .data(db_addr.clone()) + .data(crate::db::build_pool()) .service( web::scope("/issues") .wrap(crate::middleware::authorize::Authorize::default()) @@ -34,10 +36,22 @@ async fn main() -> Result<(), String> { ) .service( web::scope("/comments") + .wrap(crate::middleware::authorize::Authorize::default()) .service(crate::routes::comments::create) .service(crate::routes::comments::update) .service(crate::routes::comments::delete), ) + .service( + web::scope("/currentUser") + .wrap(crate::middleware::authorize::Authorize::default()) + .service(crate::routes::users::current_user), + ) + .service( + web::scope("/project") + .wrap(crate::middleware::authorize::Authorize::default()) + .service(crate::routes::projects::project_with_users_and_issues) + .service(crate::routes::projects::update), + ) }) .bind("127.0.0.1:8080") .map_err(|e| format!("{}", e))? diff --git a/jirs-server/src/middleware/authorize.rs b/jirs-server/src/middleware/authorize.rs index 9eeb6c5d..6a93da98 100644 --- a/jirs-server/src/middleware/authorize.rs +++ b/jirs-server/src/middleware/authorize.rs @@ -1,16 +1,12 @@ +use crate::db::SyncQuery; use actix_service::{Service, Transform}; use actix_web::http::header::{self}; -use actix_web::{dev::ServiceRequest, dev::ServiceResponse, Error, HttpResponse}; -use futures::future::{ok, Either, FutureExt, LocalBoxFuture, Ready}; -use serde::Serialize; +use actix_web::http::HeaderMap; +use actix_web::{dev::ServiceRequest, dev::ServiceResponse, Error}; +use futures::future::{ok, FutureExt, LocalBoxFuture, Ready}; use std::task::{Context, Poll}; -type Db = actix_web::web::Data>; - -#[derive(Serialize)] -pub struct ErrorResponse { - errors: Vec, -} +type Db = actix_web::web::Data; #[derive(Default)] pub struct Authorize; @@ -24,8 +20,8 @@ where type Request = ServiceRequest; type Response = ServiceResponse; type Error = Error; - type InitError = (); type Transform = AuthorizeMiddleware; + type InitError = (); type Future = Ready>; fn new_transform(&self, service: S) -> Self::Future { @@ -46,70 +42,66 @@ where type Request = ServiceRequest; type Response = ServiceResponse; type Error = Error; - type Future = Either< - Ready>, - LocalBoxFuture<'static, Result>, - >; + type Future = LocalBoxFuture<'static, Result>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { self.service.poll_ready(cx) } fn call(&mut self, req: ServiceRequest) -> Self::Future { - let header_value = req.headers().get(&header::AUTHORIZATION); - let access_token = - match header_value.and_then(|s| parse_bearer(s.to_str().unwrap_or_default())) { - Some(token) => token, - _ => { - println!("No access token found"); - let res = HttpResponse::Unauthorized().body("").into_body(); - return Either::Left(ok(req.into_response(res))); - } - }; - - let db_executor: Db = match req.app_data() { + let pool: Db = match req.app_data() { Some(d) => d, _ => { - let response = ErrorResponse { - errors: vec!["Database connection failed".to_string()], - }; - let res = HttpResponse::InternalServerError() - .json(response) - .into_body(); - return Either::Left(ok(req.into_response(res))); + return async move { + let res = crate::errors::ServiceErrors::DatabaseConnectionLost + .into_http_response() + .into_body(); + Ok(req.into_response(res)) + } + .boxed_local(); } }; - let fut = self.service.call(req); - - Either::Right( - async move { - match check_token(access_token, db_executor).await { - Some(user) => (), - _ => (), - }; - fut.await + match check_token(req.headers(), pool) { + std::result::Result::Err(e) => { + return async move { + let res = e.into_http_response().into_body(); + Ok(req.into_response(res)) + } + .boxed_local(); } - .boxed_local(), - ) + Ok(_user) => {} + }; + + let fut = self.service.call(req); + async move { fut.await }.boxed_local() } } -async fn check_token(access_token: uuid::Uuid, db_executor: Db) -> Option { - use crate::db::authorize_user::AuthorizeUser; - match db_executor.send(AuthorizeUser { access_token }).await { - Ok(Ok(user)) => Some(user), - _ => None, - } +pub fn token_from_headers( + headers: &HeaderMap, +) -> std::result::Result { + headers + .get(&header::AUTHORIZATION) + .ok_or_else(|| crate::errors::ServiceErrors::Unauthorized) + .map(|h| h.to_str().unwrap_or_default()) + .and_then(|s| parse_bearer(s)) } -fn parse_bearer(header: &str) -> Option { +fn check_token( + headers: &HeaderMap, + pool: Db, +) -> std::result::Result { + token_from_headers(headers).and_then(|access_token| { + use crate::db::authorize_user::AuthorizeUser; + AuthorizeUser { access_token }.handle(&pool) + }) +} + +fn parse_bearer(header: &str) -> Result { if !header.starts_with("Bearer ") { - return None; + return Err(crate::errors::ServiceErrors::Unauthorized); } let (_bearer, token) = header.split_at(7); - match uuid::Uuid::parse_str(token) { - Ok(u) => Some(u), - _ => None, - } + uuid::Uuid::parse_str(token).map_err(|_e| crate::errors::ServiceErrors::Unauthorized) } diff --git a/jirs-server/src/models.rs b/jirs-server/src/models.rs index 9dc0f689..1c63e90e 100644 --- a/jirs-server/src/models.rs +++ b/jirs-server/src/models.rs @@ -3,6 +3,11 @@ use chrono::NaiveDateTime; use serde::{Deserialize, Serialize}; use uuid::Uuid; +#[derive(Serialize)] +pub struct ErrorResponse { + pub errors: Vec, +} + #[derive(Debug, Serialize, Deserialize, Queryable)] pub struct Comment { pub id: i32, @@ -34,8 +39,8 @@ pub struct Issue { pub estimate: Option, pub time_spent: Option, pub time_remaining: Option, - pub reporter_id: Option, - pub project_id: Option, + pub reporter_id: i32, + pub project_id: i32, pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, } @@ -53,8 +58,8 @@ pub struct IssueForm { pub estimate: Option, pub time_spent: Option, pub time_remaining: Option, - pub reporter_id: Option, - pub project_id: Option, + pub reporter_id: i32, + pub project_id: i32, } #[derive(Debug, Serialize, Deserialize, Queryable)] diff --git a/jirs-server/src/routes/projects.rs b/jirs-server/src/routes/projects.rs index 8b137891..280df1c9 100644 --- a/jirs-server/src/routes/projects.rs +++ b/jirs-server/src/routes/projects.rs @@ -1 +1,17 @@ +use crate::db::DbExecutor; +use actix::Addr; +use actix_web::web::Data; +use actix_web::{get, put, HttpRequest, HttpResponse}; +#[get("")] +pub async fn project_with_users_and_issues( + req: HttpRequest, + db: Data>, +) -> HttpResponse { + HttpResponse::NotImplemented().body("") +} + +#[put("")] +pub async fn update(req: HttpRequest, db: Data>) -> HttpResponse { + HttpResponse::NotImplemented().body("") +} diff --git a/jirs-server/src/routes/users.rs b/jirs-server/src/routes/users.rs index 23704347..6a1791d1 100644 --- a/jirs-server/src/routes/users.rs +++ b/jirs-server/src/routes/users.rs @@ -1,6 +1,23 @@ -use actix_web::{get, HttpResponse}; +use crate::db::authorize_user::AuthorizeUser; +use crate::db::DbExecutor; +use crate::middleware::authorize::token_from_headers; +use actix::Addr; +use actix_web::web::Data; +use actix_web::{get, HttpRequest, HttpResponse}; -#[get("/currentUser")] -pub async fn current_user() -> HttpResponse { - HttpResponse::Ok().content_type("text/html").body("") +#[get("")] +pub async fn current_user(req: HttpRequest, db: Data>) -> HttpResponse { + let token = match token_from_headers(req.headers()) { + Ok(uuid) => uuid, + _ => return crate::errors::ServiceErrors::Unauthorized.into_http_response(), + }; + match db + .send(AuthorizeUser { + access_token: token, + }) + .await + { + Ok(Ok(user)) => HttpResponse::Ok().json(user), + _ => crate::errors::ServiceErrors::Unauthorized.into_http_response(), + } } diff --git a/jirs-server/src/schema.rs b/jirs-server/src/schema.rs index 8bcb4c0e..4a6f9279 100644 --- a/jirs-server/src/schema.rs +++ b/jirs-server/src/schema.rs @@ -71,4 +71,10 @@ joinable!(issues -> users (reporter_id)); joinable!(tokens -> users (user_id)); joinable!(users -> projects (project_id)); -allow_tables_to_appear_in_same_query!(comments, issues, projects, tokens, users,); +allow_tables_to_appear_in_same_query!( + comments, + issues, + projects, + tokens, + users, +);