Add queries

This commit is contained in:
Adrian Woźniak 2020-03-27 16:17:25 +01:00
parent bc2d8a3963
commit f802d0c4e5
14 changed files with 302 additions and 75 deletions

View File

@ -5,4 +5,3 @@ services:
image: postgres:latest
ports:
- 5432:5432

View File

@ -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;

View File

@ -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<crate::models::User, String>;
type Result = Result<crate::models::User, ServiceErrors>;
}
impl Handler<AuthorizeUser> for DbExecutor {
type Result = Result<crate::models::User, String>;
type Result = Result<crate::models::User, ServiceErrors>;
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::<Token>(conn)
.map_err(|e| format!("{}", e))?;
.map_err(|e| ServiceErrors::RecordNotFound("Token".to_string()))?;
let user = users
.filter(id.eq(token.user_id))
.first::<User>(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<crate::models::User, crate::errors::ServiceErrors>;
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::<Token>(&conn)
.map_err(|_| crate::errors::ServiceErrors::Unauthorized)?;
let user = users
.filter(id.eq(token.user_id))
.first::<User>(&conn)
.map_err(|_| crate::errors::ServiceErrors::Unauthorized)?;
Ok(user)
}
}

View File

@ -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<Vec<Issue>, ServiceErrors>;
}
impl Handler<LoadProjectIssues> for DbExecutor {
type Result = Result<Vec<Issue>, 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::<Issue>(conn)
.map_err(|_| ServiceErrors::RecordNotFound("project issues".to_string()))?;
Ok(vec)
}
}

View File

@ -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<ConnectionManager<PgConnection>>;
pub type DbPool = r2d2::Pool<ConnectionManager<PgConnection>>;
pub struct DbExecutor(pub DbPool);
@ -26,5 +29,11 @@ pub fn build_pool() -> DbPool {
let manager = ConnectionManager::<PgConnection>::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;
}

View File

@ -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<Project, ServiceErrors>;
}
impl Handler<LoadCurrentProject> for DbExecutor {
type Result = Result<Project, ServiceErrors>;
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::<Project>(conn)
.map_err(|_| ServiceErrors::RecordNotFound("Project".to_string()))?;
Ok(project)
}
}

View File

@ -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<Vec<User>, ServiceErrors>;
}
impl Handler<LoadProjectUsers> for DbExecutor {
type Result = Result<Vec<User>, 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<Issue>)> = 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<User> = vec![];
for row in rows {
vec.push(row.0);
}
Ok(vec)
}
}

37
jirs-server/src/errors.rs Normal file
View File

@ -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<HttpResponse> 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)],
})
}
}
}
}

View File

@ -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))?

View File

@ -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<actix::Addr<crate::db::DbExecutor>>;
#[derive(Serialize)]
pub struct ErrorResponse {
errors: Vec<String>,
}
type Db = actix_web::web::Data<crate::db::DbPool>;
#[derive(Default)]
pub struct Authorize;
@ -24,8 +20,8 @@ where
type Request = ServiceRequest;
type Response = ServiceResponse<B>;
type Error = Error;
type InitError = ();
type Transform = AuthorizeMiddleware<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
@ -46,70 +42,66 @@ where
type Request = ServiceRequest;
type Response = ServiceResponse<B>;
type Error = Error;
type Future = Either<
Ready<Result<Self::Response, Error>>,
LocalBoxFuture<'static, Result<Self::Response, Error>>,
>;
type Future = LocalBoxFuture<'static, Result<Self::Response, Error>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
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<crate::models::User> {
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<uuid::Uuid, crate::errors::ServiceErrors> {
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<uuid::Uuid> {
fn check_token(
headers: &HeaderMap,
pool: Db,
) -> std::result::Result<crate::models::User, crate::errors::ServiceErrors> {
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<uuid::Uuid, crate::errors::ServiceErrors> {
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)
}

View File

@ -3,6 +3,11 @@ use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Serialize)]
pub struct ErrorResponse {
pub errors: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize, Queryable)]
pub struct Comment {
pub id: i32,
@ -34,8 +39,8 @@ pub struct Issue {
pub estimate: Option<i32>,
pub time_spent: Option<i32>,
pub time_remaining: Option<i32>,
pub reporter_id: Option<i32>,
pub project_id: Option<i32>,
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<i32>,
pub time_spent: Option<i32>,
pub time_remaining: Option<i32>,
pub reporter_id: Option<i32>,
pub project_id: Option<i32>,
pub reporter_id: i32,
pub project_id: i32,
}
#[derive(Debug, Serialize, Deserialize, Queryable)]

View File

@ -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<Addr<DbExecutor>>,
) -> HttpResponse {
HttpResponse::NotImplemented().body("")
}
#[put("")]
pub async fn update(req: HttpRequest, db: Data<Addr<DbExecutor>>) -> HttpResponse {
HttpResponse::NotImplemented().body("")
}

View File

@ -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<Addr<DbExecutor>>) -> 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(),
}
}

View File

@ -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,
);