Add queries
This commit is contained in:
parent
bc2d8a3963
commit
f802d0c4e5
@ -5,4 +5,3 @@ services:
|
||||
image: postgres:latest
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
|
@ -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;
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
33
jirs-server/src/db/issues.rs
Normal file
33
jirs-server/src/db/issues.rs
Normal 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)
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
32
jirs-server/src/db/projects.rs
Normal file
32
jirs-server/src/db/projects.rs
Normal 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)
|
||||
}
|
||||
}
|
41
jirs-server/src/db/users.rs
Normal file
41
jirs-server/src/db/users.rs
Normal 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
37
jirs-server/src/errors.rs
Normal 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)],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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))?
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)]
|
||||
|
@ -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("")
|
||||
}
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user