diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/auth.rs | 31 | ||||
| -rw-r--r-- | src/database.rs | 6 | ||||
| -rw-r--r-- | src/database/database.rs | 158 | ||||
| -rw-r--r-- | src/database/problem.rs | 3 | ||||
| -rw-r--r-- | src/database/sql/fetch_user_by_email.sql | 1 | ||||
| -rw-r--r-- | src/database/sql/fetch_user_by_username.sql | 1 | ||||
| -rw-r--r-- | src/database/sql/initialize.sql | 2 | ||||
| -rw-r--r-- | src/database/sql/insert_user.sql | 1 | ||||
| -rw-r--r-- | src/database/user.rs | 17 | ||||
| -rw-r--r-- | src/main.rs | 24 | ||||
| -rw-r--r-- | src/routes.rs | 4 | ||||
| -rw-r--r-- | src/routes/errors.rs | 26 | ||||
| -rw-r--r-- | src/routes/problem.rs (renamed from src/routes/problems.rs) | 0 | ||||
| -rw-r--r-- | src/routes/user.rs | 49 |
14 files changed, 281 insertions, 42 deletions
diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..90ce4a5 --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,31 @@ +use argon2::{Argon2, PasswordHash, PasswordVerifier, password_hash::{ + Error, PasswordHasher, SaltString, rand_core::OsRng +}}; + +pub fn hash_password(password: &str) -> Result<String, Error> { + let argon2 = Argon2::default(); + let salt = SaltString::generate(&mut OsRng); + Ok(argon2.hash_password(password.as_bytes(), &salt)?.to_string()) +} + +pub fn check_password(password: &str, password_hash: &str) -> Result<bool, Error> { + let argon2 = Argon2::default(); + let hash = PasswordHash::new(password_hash)?; + Ok(argon2.verify_password(password.as_bytes(), &hash).is_ok()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_password_hashing() { + + let passwords = vec!["password", "test1", "random"]; + + for password in &passwords { + let hash = hash_password(password).unwrap(); + assert!(check_password(password, &hash).unwrap()); + } + } +} diff --git a/src/database.rs b/src/database.rs index 8853557..3c8afe6 100644 --- a/src/database.rs +++ b/src/database.rs @@ -1,2 +1,6 @@ -pub mod database; +mod database; + +pub use database::Database; pub mod problem; +pub mod user; + diff --git a/src/database/database.rs b/src/database/database.rs index 7da66f2..ca0e018 100644 --- a/src/database/database.rs +++ b/src/database/database.rs @@ -1,67 +1,154 @@ use std::path::Path; -use rusqlite::{Connection, Error}; -use crate::database::problem::Problem; +use r2d2::Pool; +use r2d2_sqlite::SqliteConnectionManager; +use r2d2_sqlite::rusqlite::OptionalExtension; +use super::problem::Problem; +use super::user::User; + +#[derive(Clone)] pub struct Database { - connection: Connection, + pool: Pool<SqliteConnectionManager> +} + +#[derive(Debug)] +pub enum DatabaseError { + Connection(String), + Query(String), } impl Database { - pub fn new(database_path: impl AsRef<Path>) -> Result<Self, Error> { - let connection = Connection::open(database_path)?; - Ok(Database { - connection, - }) + pub fn new(database_path: impl AsRef<Path>) -> Result<Self, DatabaseError> { + let manager = SqliteConnectionManager::file(database_path); + let pool = Pool::new(manager) + .map_err(|e| DatabaseError::Connection(e.to_string()))?; + Ok(Database { pool }) + } + + pub fn new_in_memory() -> Result<Self, DatabaseError> { + let manager = SqliteConnectionManager::memory(); + let pool = Pool::new(manager) + .map_err(|e| DatabaseError::Connection(e.to_string()))?; + Ok(Database { pool }) + } + + pub fn insert_user(&self, email: &str, username: &str, password_hash: &str) -> Result<User, DatabaseError> { + static QUERY: &str = include_str!("sql/insert_user.sql"); + let conn = self.pool + .get() + .map_err(|e| DatabaseError::Connection(e.to_string()))?; + let mut statement = conn.prepare(QUERY) + .map_err(|e| DatabaseError::Query(e.to_string()))?; + + Ok(statement + .query_one((email, username, password_hash), |row| { + Ok(User::new( + row.get("id")?, + email.to_owned(), + username.to_owned(), + password_hash.to_owned(), + )) + }) + .map_err(|e| DatabaseError::Query(e.to_string()))? + ) + } + + pub fn fetch_user_by_email(&self, email: &str) -> Result<Option<User>, DatabaseError> { + static QUERY: &str = include_str!("sql/fetch_user_by_email.sql"); + let conn = self.pool + .get() + .map_err(|e| DatabaseError::Connection(e.to_string()))?; + let mut statement = conn.prepare(QUERY) + .map_err(|e| DatabaseError::Query(e.to_string()))?; + + Ok(statement + .query_one([email], |row| { + Ok(User::new( + row.get("id")?, + row.get("email")?, + row.get("username")?, + row.get("password_hash")?, + )) + }) + .optional() + .map_err(|e| DatabaseError::Query(e.to_string()))? + ) } - pub fn new_in_memory() -> Result<Self, Error> { - let connection = Connection::open_in_memory()?; - Ok(Database { - connection, - }) + pub fn fetch_user_by_username(&self, username: &str) -> Result<Option<User>, DatabaseError> { + static QUERY: &str = include_str!("sql/fetch_user_by_username.sql"); + let conn = self.pool + .get() + .map_err(|e| DatabaseError::Connection(e.to_string()))?; + let mut statement = conn.prepare(QUERY) + .map_err(|e| DatabaseError::Query(e.to_string()))?; + + Ok(statement + .query_one([username], |row| { + Ok(User::new( + row.get("id")?, + row.get("email")?, + row.get("username")?, + row.get("password_hash")?, + )) + }) + .optional() + .map_err(|e| DatabaseError::Query(e.to_string()))? + ) } - pub fn insert_problem(&self, title: &str, description: &str) -> Result<Problem, Error> { + pub fn insert_problem(&self, title: &str, description: &str) -> Result<Problem, DatabaseError> { static QUERY: &str = include_str!("sql/insert_problem.sql"); - let mut statement = self.connection.prepare(QUERY)?; + let conn = self.pool + .get() + .map_err(|e| DatabaseError::Connection(e.to_string()))?; + let mut statement = conn.prepare(QUERY) + .map_err(|e| DatabaseError::Query(e.to_string()))?; - let result = statement + Ok(statement .query_one((title, description), |row| { Ok(Problem::new( row.get("id")?, title.to_owned(), description.to_owned(), )) - })?; - - Ok(result) - } - - pub fn delete_problem(&self, id: i64) -> Result<(), Error> { - todo!(); + }) + .map_err(|e| DatabaseError::Query(e.to_string()))? + ) } - pub fn fetch_problems(&self) -> Result<Vec<Problem>, Error> { + pub fn fetch_problems(&self) -> Result<Vec<Problem>, DatabaseError> { static QUERY: &str = include_str!("sql/fetch_problems.sql"); - let mut statement = self.connection.prepare(QUERY)?; + let conn = self.pool + .get() + .map_err(|e| DatabaseError::Connection(e.to_string()))?; + let mut statement = conn.prepare(QUERY) + .map_err(|e| DatabaseError::Query(e.to_string()))?; - let problems = statement + Ok(statement .query_map([], |row| { Ok(Problem::new( row.get("id")?, row.get("title")?, row.get("description")?, )) - })? - .collect::<Result<Vec<Problem>, _>>()?; - - Ok(problems) + }) + .map_err(|e| DatabaseError::Query(e.to_string()))? + .collect::<Result<Vec<Problem>, _>>() + .map_err(|e| DatabaseError::Query(e.to_string()))? + ) } - - pub fn initialize(&self) -> Result<(), Error> { - static QUERY: &str = include_str!("sql/initialize.sql"); - self.connection.execute_batch(QUERY)?; + + pub fn initialize(&self) -> Result<(), DatabaseError> { + static QUERY: &str = include_str!("sql/initialize.sql"); + let conn = self.pool + .get() + .map_err(|e| DatabaseError::Connection(e.to_string()))?; + + conn.execute_batch(QUERY) + .map_err(|e| DatabaseError::Query(e.to_string()))?; + Ok(()) } } @@ -82,10 +169,11 @@ mod tests { let problem = db.insert_problem(title, description).unwrap(); assert_eq!(problem.title(), title); assert_eq!(problem.description(), description); - + let problems = db.fetch_problems().unwrap(); assert_eq!(problems.len(), 1); assert_eq!(problems[0].title(), title); assert_eq!(problems[0].description(), description); + assert_eq!(problems[0].id(), problem.id()); } } diff --git a/src/database/problem.rs b/src/database/problem.rs index fdbb2b5..c3e04c2 100644 --- a/src/database/problem.rs +++ b/src/database/problem.rs @@ -5,10 +5,11 @@ pub struct Problem { } impl Problem { - pub fn new(id: i64, title: String, description: String) -> Self { + pub(super) fn new(id: i64, title: String, description: String) -> Self { Self { id, title, description } } + pub fn id(&self) -> i64 { self.id } pub fn title(&self) -> &str { &self.title } pub fn description(&self) -> &str { &self.description } } diff --git a/src/database/sql/fetch_user_by_email.sql b/src/database/sql/fetch_user_by_email.sql new file mode 100644 index 0000000..154b5ef --- /dev/null +++ b/src/database/sql/fetch_user_by_email.sql @@ -0,0 +1 @@ +SELECT * FROM user WHERE user.email = ?1; diff --git a/src/database/sql/fetch_user_by_username.sql b/src/database/sql/fetch_user_by_username.sql new file mode 100644 index 0000000..f90d9d1 --- /dev/null +++ b/src/database/sql/fetch_user_by_username.sql @@ -0,0 +1 @@ +SELECT * FROM user WHERE user.username = ?1; diff --git a/src/database/sql/initialize.sql b/src/database/sql/initialize.sql index 3baf4ff..065bac9 100644 --- a/src/database/sql/initialize.sql +++ b/src/database/sql/initialize.sql @@ -7,7 +7,7 @@ CREATE TABLE IF NOT EXISTS problem ( CREATE TABLE IF NOT EXISTS user ( id INTEGER PRIMARY KEY, email TEXT UNIQUE NOT NULL, - username TEXT NOT NULL, + username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL ); diff --git a/src/database/sql/insert_user.sql b/src/database/sql/insert_user.sql new file mode 100644 index 0000000..6b67678 --- /dev/null +++ b/src/database/sql/insert_user.sql @@ -0,0 +1 @@ +INSERT INTO user (email, username, password_hash) VALUES (?1, ?2, ?3) RETURNING id; diff --git a/src/database/user.rs b/src/database/user.rs new file mode 100644 index 0000000..c9aaf51 --- /dev/null +++ b/src/database/user.rs @@ -0,0 +1,17 @@ +pub struct User { + id: i64, + email: String, + username: String, + password_hash: String, +} + +impl User { + pub(super) fn new(id: i64, email: String, username: String, password_hash: String) -> Self { + Self { id, email, username, password_hash } + } + + pub fn id(&self) -> i64 { self.id } + pub fn email(&self) -> &str { &self.email } + pub fn username(&self) -> &str { &self.username } + pub fn password_hash(&self) -> &str { &self.password_hash } +} diff --git a/src/main.rs b/src/main.rs index a143df5..ef762f7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,18 +1,36 @@ mod database; mod routes; +mod auth; use axum::{ - routing::get, + routing::{get, post}, Router, }; -use routes::problems::get_problems; +use routes::problem::get_problems; +use routes::user::create_user; + +use crate::database::Database; + +#[derive(Clone)] +struct AppState { + database: Database, +} #[tokio::main] async fn main() { + + let database = Database::new_in_memory().unwrap(); + database.initialize().unwrap(); + + let state = AppState { + database: database, + }; + let app = Router::new() .route("/", get(|| async {"Hello World!"})) - .route("/problems", get(get_problems)); + .route("/problems", get(get_problems)) + .route("/user", post(create_user).with_state(state)); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); axum::serve(listener, app).await.unwrap(); diff --git a/src/routes.rs b/src/routes.rs index c054f87..6d7ac77 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -1 +1,3 @@ -pub mod problems; +pub mod problem; +pub mod user; +mod errors; diff --git a/src/routes/errors.rs b/src/routes/errors.rs new file mode 100644 index 0000000..6261d75 --- /dev/null +++ b/src/routes/errors.rs @@ -0,0 +1,26 @@ +use axum::{Json, body::Body, http::{Response, StatusCode}, response::IntoResponse}; +use serde_json::{json}; + +pub enum RouteError { + Internal(String), + UserCreateEmailExists(String), + UserCreateUsernameExists(String), +} + +impl IntoResponse for RouteError { + fn into_response(self) -> Response<Body> { + let (status, message) = match self { + Self::Internal(message) => { + (StatusCode::INTERNAL_SERVER_ERROR, message) + }, + RouteError::UserCreateEmailExists(email) => { + (StatusCode::BAD_REQUEST, format!("user with email \"{}\" already exists", email)) + }, + RouteError::UserCreateUsernameExists(username) => { + (StatusCode::BAD_REQUEST, format!("user with username \"{}\" already exists", username)) + } + }; + + (status, Json(json!({"error": message}))).into_response() + } +} diff --git a/src/routes/problems.rs b/src/routes/problem.rs index caeb808..caeb808 100644 --- a/src/routes/problems.rs +++ b/src/routes/problem.rs diff --git a/src/routes/user.rs b/src/routes/user.rs new file mode 100644 index 0000000..42db46f --- /dev/null +++ b/src/routes/user.rs @@ -0,0 +1,49 @@ +use axum::extract::{Json, State}; +use axum::http::StatusCode; +use axum::response::IntoResponse; +use serde::Deserialize; +use serde_json::json; + +use crate::AppState; +use crate::auth::hash_password; +use super::errors::RouteError; + +#[derive(Deserialize)] +pub struct CreateUserRequest { + email: String, + username: String, + password: String, +} + +pub async fn create_user( + State(state): State<AppState>, + Json(request): Json<CreateUserRequest> +) -> Result<impl IntoResponse, RouteError> { + + match state.database.fetch_user_by_email(&request.email) { + Err(_) => return Err(RouteError::Internal("database action failed".into())), + Ok(Some(_)) => return Err(RouteError::UserCreateEmailExists(request.email)), + Ok(None) => {}, + }; + + match state.database.fetch_user_by_username(&request.username) { + Err(_) => return Err(RouteError::Internal("database action failed B".into())), + Ok(Some(_)) => return Err(RouteError::UserCreateUsernameExists(request.username)), + Ok(None) => {}, + }; + + let Ok(password_hash) = hash_password(&request.password) else { + return Err(RouteError::Internal("failed to hash password".into())) + }; + + let Ok(user) = state.database.insert_user(&request.email, &request.username, &password_hash) else { + return Err(RouteError::Internal("failed to create user".into())); + }; + + return Ok((StatusCode::CREATED, Json(json!({ + "id": user.id(), + "email": user.email(), + "username": user.username(), + })))); +} + |
