-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #19 from timhabermaas/cleanup-server-code
Cleanup server code
- Loading branch information
Showing
10 changed files
with
482 additions
and
507 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}); | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 })) | ||
} |
Oops, something went wrong.