Skip to content

Commit

Permalink
Merge pull request #19 from timhabermaas/cleanup-server-code
Browse files Browse the repository at this point in the history
Cleanup server code
  • Loading branch information
timhabermaas authored Dec 17, 2021
2 parents caecfbf + 6fa5689 commit 76773e9
Show file tree
Hide file tree
Showing 10 changed files with 482 additions and 507 deletions.
20 changes: 20 additions & 0 deletions records/src/app_state.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use sqlx::postgres::PgPoolOptions;
use sqlx::PgPool;

#[derive(Clone)]
pub struct AppState {
pub pool: PgPool,
pub jwt_secret: String,
}

impl AppState {
pub async fn new(db_url: String, jwt_secret: String) -> Self {
let pool: PgPool = PgPoolOptions::new()
.max_connections(5)
.connect(&db_url)
.await
.expect("failed to create pool");

AppState { pool, jwt_secret }
}
}
51 changes: 51 additions & 0 deletions records/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use actix_web::http;
use tracing::error;

#[derive(Debug)]
pub struct AppError {
pub cause: String,
pub message: String,
pub error_type: ErrorType,
}

#[derive(Debug)]
pub enum ErrorType {
NotFound,
Unauthorized,
DbError,
}

impl std::fmt::Display for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match &self.error_type {
ErrorType::NotFound => write!(f, "Resource not found"),
ErrorType::DbError => write!(f, "Db error"),
ErrorType::Unauthorized => write!(f, "Not authorized"),
}
}
}

impl actix_web::error::ResponseError for AppError {
fn status_code(&self) -> http::StatusCode {
match self.error_type {
ErrorType::NotFound => http::StatusCode::NOT_FOUND,
ErrorType::DbError => http::StatusCode::INTERNAL_SERVER_ERROR,
ErrorType::Unauthorized => http::StatusCode::UNAUTHORIZED,
}
}

fn error_response(&self) -> actix_web::HttpResponse {
error!("{:?}", self);
actix_web::HttpResponse::NotFound().json(serde_json::json!({"error": "TBD"}))
}
}

impl From<sqlx::Error> for AppError {
fn from(e: sqlx::Error) -> Self {
AppError {
cause: format!("{:?}", e),
message: "database access failed".to_string(),
error_type: ErrorType::DbError,
}
}
}
158 changes: 158 additions & 0 deletions records/src/extractors/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
use crate::app_state::AppState;
use crate::db;
use crate::error::{AppError, ErrorType};
use actix_web::{dev, web, FromRequest, HttpRequest};
use futures::Future;
use futures_util::future::{ok, Ready};
use jwt::{decode, Algorithm, DecodingKey, Validation};
use serde::Deserialize;
use std::collections::HashMap;
use std::pin::Pin;
use tracing::info;

#[derive(Deserialize, Debug)]
struct JwtClaim {
user_id: i32,
}

#[derive(Debug, Clone)]
pub struct UserSession {
pub user_id: Option<i32>,
}

impl FromRequest for UserSession {
type Error = AppError;
type Future = Ready<Result<Self, Self::Error>>;

fn from_request(req: &HttpRequest, _payload: &mut dev::Payload) -> Self::Future {
let app_state = req
.app_data::<web::Data<AppState>>()
.expect("app data not found");

let token = req.headers().get("Authorization").map(|content| {
content
.to_str()
.expect("must be string")
.trim_start_matches("Bearer ")
.to_string()
});
let query = parse_query_string(req.query_string()).remove("token");
// First try looking for authorization token, fallback to URL param otherwise.
if let Some(token) = token.or(query) {
let token_data = decode::<JwtClaim>(
&token,
&DecodingKey::from_secret(app_state.jwt_secret.as_ref()),
&Validation::new(Algorithm::HS256),
);

if let Err(e) = token_data {
info!("jwt token not valid, {:?}", e);
ok(UserSession { user_id: None })
} else {
ok(UserSession {
user_id: Some(token_data.unwrap().claims.user_id),
})
}
} else {
info!("jwt token not found");
ok(UserSession { user_id: None })
}
}
}

fn parse_query_string(query_string: &str) -> HashMap<String, String> {
let parts = query_string.split("&");
let mut result = HashMap::new();
for part in parts {
let mut key_value = part.splitn(2, "=");
if let Some(key) = key_value.next() {
if let Some(value) = key_value.next() {
result.insert(key.to_string(), value.to_string());
}
}
}
result
}

#[derive(Debug, Clone)]
pub struct LoggedInUser {
pub user: db::SimpleUser,
}

impl FromRequest for LoggedInUser {
type Error = AppError;
type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;

fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future {
let session = UserSession::from_request(req, payload).into_inner();
let app_state = req
.app_data::<web::Data<AppState>>()
.expect("app data not found")
.clone();

Box::pin(async move {
if let Ok(session) = session {
let user = current_user(&app_state, session.clone()).await?;
if let Some(user) = user {
Ok(Self { user })
} else {
Err(AppError {
cause: "expected existing user for session".to_string(),
message: "expected existing user for session".to_string(),
error_type: ErrorType::Unauthorized,
})
}
} else {
Err(AppError {
cause: "expected existing session".to_string(),
message: "expected existing session".to_string(),
error_type: ErrorType::Unauthorized,
})
}
})
}
}

async fn current_user(
app_state: &web::Data<AppState>,
user_session: UserSession,
) -> Result<Option<db::SimpleUser>, AppError> {
let current_user = if let Some(user_id) = user_session.user_id {
db::find_simple_user(&app_state.pool, user_id).await?
} else {
None
};

Ok(current_user)
}

#[derive(Debug, Clone)]
pub struct AdminUser {
pub user: db::SimpleUser,
}

impl FromRequest for AdminUser {
type Error = AppError;
type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;

fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future {
let req = req.clone();

let user_future = LoggedInUser::from_request(&req, payload);

Box::pin(async move {
let current_user = user_future.await?;
if current_user.user.is_admin() {
Ok(AdminUser {
user: current_user.user,
})
} else {
return Err(AppError {
cause: "user is not an admin".to_string(),
message: "user is not an admin".to_string(),
error_type: ErrorType::Unauthorized,
});
}
})
}
}
10 changes: 10 additions & 0 deletions records/src/handlers/announcement.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use crate::app_state::AppState;
use crate::db;
use crate::error::AppError;
use actix_web::{web, HttpResponse};

pub async fn announcement_api(app_state: web::Data<AppState>) -> Result<HttpResponse, AppError> {
let post = db::find_announcement(&app_state.pool).await?;

Ok(HttpResponse::Ok().json(post))
}
36 changes: 36 additions & 0 deletions records/src/handlers/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
pub mod announcement;
pub mod puzzle;
pub mod record;
pub mod single;
pub mod user;

use actix_web::web::{get, put, ServiceConfig};

pub fn add_routes(cfg: &mut ServiceConfig) {
cfg.route(
"/api/singles.csv",
get().to(crate::handlers::single::singles_csv),
)
.route("/api/me", get().to(crate::handlers::user::me_api))
.route(
"/api/max_singles_record",
get().to(crate::handlers::single::max_singles_count_api),
)
.route(
"/api/announcement",
get().to(crate::handlers::announcement::announcement_api),
)
.route("/api/users", get().to(crate::handlers::user::users_api))
.route(
"/api/users/{user_slug}/block",
put().to(crate::handlers::user::user_block_api),
)
.route(
"/api/puzzles",
get().to(crate::handlers::puzzle::puzzles_api),
)
.route(
"/api/records",
get().to(crate::handlers::record::records_api),
);
}
10 changes: 10 additions & 0 deletions records/src/handlers/puzzle.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use crate::app_state::AppState;
use crate::db;
use crate::error::AppError;
use actix_web::{web, HttpResponse};

pub async fn puzzles_api(app_state: web::Data<AppState>) -> Result<HttpResponse, AppError> {
let kinds = db::fetch_puzzles(&app_state.pool).await?;

Ok(HttpResponse::Ok().json(kinds))
}
38 changes: 38 additions & 0 deletions records/src/handlers/record.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use crate::app_state::AppState;
use crate::db;
use crate::error::AppError;
use actix_web::{web, HttpResponse};
use serde::{Deserialize, Serialize};
use tracing::info;

#[derive(Deserialize)]
pub struct RecordsQuery {
/// The user search string provided by the user.
#[serde(rename = "type")]
type_: String,
page: Option<u32>,
puzzle_slug: String,
}

#[derive(Serialize)]
struct RecordsResponse {
records: db::Paginated<db::RecordRow, u32>,
}

pub async fn records_api(
web::Query(q): web::Query<RecordsQuery>,
app_state: web::Data<AppState>,
) -> Result<HttpResponse, AppError> {
info!("{}", q.puzzle_slug);
let puzzle_id = db::fetch_puzzle_id_from_slug(&app_state.pool, &q.puzzle_slug).await?;

let puzzle_id = match puzzle_id {
Some(p) => p,
None => return Ok(HttpResponse::NotFound().body("puzzle slug doesn't exist")),
};
info!("{}", puzzle_id);

let records = db::fetch_records(&app_state.pool, &q.type_, puzzle_id, q.page, 50).await?;

Ok(HttpResponse::Ok().json(RecordsResponse { records }))
}
Loading

0 comments on commit 76773e9

Please sign in to comment.