summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/auth.rs31
-rw-r--r--src/database.rs6
-rw-r--r--src/database/database.rs158
-rw-r--r--src/database/problem.rs3
-rw-r--r--src/database/sql/fetch_user_by_email.sql1
-rw-r--r--src/database/sql/fetch_user_by_username.sql1
-rw-r--r--src/database/sql/initialize.sql2
-rw-r--r--src/database/sql/insert_user.sql1
-rw-r--r--src/database/user.rs17
-rw-r--r--src/main.rs24
-rw-r--r--src/routes.rs4
-rw-r--r--src/routes/errors.rs26
-rw-r--r--src/routes/problem.rs (renamed from src/routes/problems.rs)0
-rw-r--r--src/routes/user.rs49
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(),
+ }))));
+}
+