aboutsummaryrefslogtreecommitdiffstats
path: root/atuin-server/src/handlers
diff options
context:
space:
mode:
authorEllie Huxtable <e@elm.sh>2021-04-20 21:53:07 +0100
committerGitHub <noreply@github.com>2021-04-20 20:53:07 +0000
commita21737e2b7f8d1e426726bdd7536033f299d476a (patch)
treee940afdff9c145d25d9a2895fd44a77d70719a2e /atuin-server/src/handlers
parentSwitch to Warp + SQLx, use async, switch to Rust stable (#36) (diff)
downloadatuin-a21737e2b7f8d1e426726bdd7536033f299d476a.zip
Use cargo workspaces (#37)
* Switch to Cargo workspaces Breaking things into "client", "server" and "common" makes managing the codebase much easier! client - anything running on a user's machine for adding history server - handles storing/syncing history and running a HTTP server common - request/response API definitions, common utils, etc * Update dockerfile
Diffstat (limited to 'atuin-server/src/handlers')
-rw-r--r--atuin-server/src/handlers/history.rs89
-rw-r--r--atuin-server/src/handlers/mod.rs6
-rw-r--r--atuin-server/src/handlers/user.rs141
3 files changed, 236 insertions, 0 deletions
diff --git a/atuin-server/src/handlers/history.rs b/atuin-server/src/handlers/history.rs
new file mode 100644
index 00000000..1aebdde1
--- /dev/null
+++ b/atuin-server/src/handlers/history.rs
@@ -0,0 +1,89 @@
+use std::convert::Infallible;
+
+use warp::{http::StatusCode, reply::json};
+
+use crate::database::Database;
+use crate::models::{NewHistory, User};
+use atuin_common::api::{
+ AddHistoryRequest, CountResponse, ErrorResponse, SyncHistoryRequest, SyncHistoryResponse,
+};
+
+pub async fn count(
+ user: User,
+ db: impl Database + Clone + Send + Sync,
+) -> Result<Box<dyn warp::Reply>, Infallible> {
+ db.count_history(&user).await.map_or(
+ Ok(Box::new(ErrorResponse::reply(
+ "failed to query history count",
+ StatusCode::INTERNAL_SERVER_ERROR,
+ ))),
+ |count| Ok(Box::new(json(&CountResponse { count }))),
+ )
+}
+
+pub async fn list(
+ req: SyncHistoryRequest,
+ user: User,
+ db: impl Database + Clone + Send + Sync,
+) -> Result<Box<dyn warp::Reply>, Infallible> {
+ let history = db
+ .list_history(
+ &user,
+ req.sync_ts.naive_utc(),
+ req.history_ts.naive_utc(),
+ req.host,
+ )
+ .await;
+
+ if let Err(e) = history {
+ error!("failed to load history: {}", e);
+ let resp =
+ ErrorResponse::reply("failed to load history", StatusCode::INTERNAL_SERVER_ERROR);
+ let resp = Box::new(resp);
+ return Ok(resp);
+ }
+
+ let history: Vec<String> = history
+ .unwrap()
+ .iter()
+ .map(|i| i.data.to_string())
+ .collect();
+
+ debug!(
+ "loaded {} items of history for user {}",
+ history.len(),
+ user.id
+ );
+
+ Ok(Box::new(json(&SyncHistoryResponse { history })))
+}
+
+pub async fn add(
+ req: Vec<AddHistoryRequest>,
+ user: User,
+ db: impl Database + Clone + Send + Sync,
+) -> Result<Box<dyn warp::Reply>, Infallible> {
+ debug!("request to add {} history items", req.len());
+
+ let history: Vec<NewHistory> = req
+ .iter()
+ .map(|h| NewHistory {
+ client_id: h.id.as_str(),
+ user_id: user.id,
+ hostname: h.hostname.as_str(),
+ timestamp: h.timestamp.naive_utc(),
+ data: h.data.as_str(),
+ })
+ .collect();
+
+ if let Err(e) = db.add_history(&history).await {
+ error!("failed to add history: {}", e);
+
+ return Ok(Box::new(ErrorResponse::reply(
+ "failed to add history",
+ StatusCode::INTERNAL_SERVER_ERROR,
+ )));
+ };
+
+ Ok(Box::new(warp::reply()))
+}
diff --git a/atuin-server/src/handlers/mod.rs b/atuin-server/src/handlers/mod.rs
new file mode 100644
index 00000000..3c20538c
--- /dev/null
+++ b/atuin-server/src/handlers/mod.rs
@@ -0,0 +1,6 @@
+pub mod history;
+pub mod user;
+
+pub const fn index() -> &'static str {
+ "\"Through the fathomless deeps of space swims the star turtle Great A\u{2019}Tuin, bearing on its back the four giant elephants who carry on their shoulders the mass of the Discworld.\"\n\t-- Sir Terry Pratchett"
+}
diff --git a/atuin-server/src/handlers/user.rs b/atuin-server/src/handlers/user.rs
new file mode 100644
index 00000000..6b142cdc
--- /dev/null
+++ b/atuin-server/src/handlers/user.rs
@@ -0,0 +1,141 @@
+use std::convert::Infallible;
+
+use sodiumoxide::crypto::pwhash::argon2id13;
+use uuid::Uuid;
+use warp::http::StatusCode;
+use warp::reply::json;
+
+use atuin_common::api::{
+ ErrorResponse, LoginRequest, LoginResponse, RegisterRequest, RegisterResponse, UserResponse,
+};
+use atuin_common::utils::hash_secret;
+
+use crate::database::Database;
+use crate::models::{NewSession, NewUser};
+use crate::settings::Settings;
+
+pub fn verify_str(secret: &str, verify: &str) -> bool {
+ sodiumoxide::init().unwrap();
+
+ let mut padded = [0_u8; 128];
+ secret.as_bytes().iter().enumerate().for_each(|(i, val)| {
+ padded[i] = *val;
+ });
+
+ match argon2id13::HashedPassword::from_slice(&padded) {
+ Some(hp) => argon2id13::pwhash_verify(&hp, verify.as_bytes()),
+ None => false,
+ }
+}
+
+pub async fn get(
+ username: String,
+ db: impl Database + Clone + Send + Sync,
+) -> Result<Box<dyn warp::Reply>, Infallible> {
+ let user = match db.get_user(username).await {
+ Ok(user) => user,
+ Err(e) => {
+ debug!("user not found: {}", e);
+ return Ok(Box::new(ErrorResponse::reply(
+ "user not found",
+ StatusCode::NOT_FOUND,
+ )));
+ }
+ };
+
+ Ok(Box::new(warp::reply::json(&UserResponse {
+ username: user.username,
+ })))
+}
+
+pub async fn register(
+ register: RegisterRequest,
+ settings: Settings,
+ db: impl Database + Clone + Send + Sync,
+) -> Result<Box<dyn warp::Reply>, Infallible> {
+ if !settings.open_registration {
+ return Ok(Box::new(ErrorResponse::reply(
+ "this server is not open for registrations",
+ StatusCode::BAD_REQUEST,
+ )));
+ }
+
+ let hashed = hash_secret(register.password.as_str());
+
+ let new_user = NewUser {
+ email: register.email,
+ username: register.username,
+ password: hashed,
+ };
+
+ let user_id = match db.add_user(new_user).await {
+ Ok(id) => id,
+ Err(e) => {
+ error!("failed to add user: {}", e);
+ return Ok(Box::new(ErrorResponse::reply(
+ "failed to add user",
+ StatusCode::BAD_REQUEST,
+ )));
+ }
+ };
+
+ let token = Uuid::new_v4().to_simple().to_string();
+
+ let new_session = NewSession {
+ user_id,
+ token: token.as_str(),
+ };
+
+ match db.add_session(&new_session).await {
+ Ok(_) => Ok(Box::new(json(&RegisterResponse { session: token }))),
+ Err(e) => {
+ error!("failed to add session: {}", e);
+ Ok(Box::new(ErrorResponse::reply(
+ "failed to register user",
+ StatusCode::BAD_REQUEST,
+ )))
+ }
+ }
+}
+
+pub async fn login(
+ login: LoginRequest,
+ db: impl Database + Clone + Send + Sync,
+) -> Result<Box<dyn warp::Reply>, Infallible> {
+ let user = match db.get_user(login.username.clone()).await {
+ Ok(u) => u,
+ Err(e) => {
+ error!("failed to get user {}: {}", login.username.clone(), e);
+
+ return Ok(Box::new(ErrorResponse::reply(
+ "user not found",
+ StatusCode::NOT_FOUND,
+ )));
+ }
+ };
+
+ let session = match db.get_user_session(&user).await {
+ Ok(u) => u,
+ Err(e) => {
+ error!("failed to get session for {}: {}", login.username, e);
+
+ return Ok(Box::new(ErrorResponse::reply(
+ "user not found",
+ StatusCode::NOT_FOUND,
+ )));
+ }
+ };
+
+ let verified = verify_str(user.password.as_str(), login.password.as_str());
+
+ if !verified {
+ return Ok(Box::new(ErrorResponse::reply(
+ "user not found",
+ StatusCode::NOT_FOUND,
+ )));
+ }
+
+ Ok(Box::new(warp::reply::json(&LoginResponse {
+ session: session.token,
+ })))
+}