diff --git a/.gitignore b/.gitignore index f266d33..e3b4724 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ **/.direnv +/rust_solid_cassandra/data/ diff --git a/rust_solid_cassandra/.dockerignore b/rust_solid_cassandra/.dockerignore index 918f82d..9c32c11 100644 --- a/rust_solid_cassandra/.dockerignore +++ b/rust_solid_cassandra/.dockerignore @@ -6,3 +6,4 @@ **/.envrc **/dist **/node_modules +/data diff --git a/rust_solid_cassandra/backend/src/main.rs b/rust_solid_cassandra/backend/src/main.rs index 2374da3..d6a51e1 100644 --- a/rust_solid_cassandra/backend/src/main.rs +++ b/rust_solid_cassandra/backend/src/main.rs @@ -1,161 +1,49 @@ use std::{io, sync::Arc}; -use actix_identity::{Identity, IdentityMiddleware}; -use actix_session::{ - config::PersistentSession, storage::CookieSessionStore, Session, SessionMiddleware, -}; +use actix_identity::IdentityMiddleware; +use actix_session::{config::PersistentSession, storage::CookieSessionStore, SessionMiddleware}; use actix_web::{ cookie::{time::Duration, Key}, - delete, get, - http::{header::ContentType, Method}, - middleware, post, put, + middleware, web::{self}, - App, HttpMessage, HttpRequest, HttpResponse, HttpServer, Responder, + App, HttpServer, }; // Define our model module mod model; -use model::todo::{Priority, Status, Todo}; use model::user::User; // Define our repo module mod repo; -// use repo::todo_repository::TodoRepository; +use repo::todo_repository::TodoRepository; use repo::user_repository::UserRepository; +// Define our routes module +mod routes; +use routes::{todo_routes, user_routes}; -async fn index(id: Option, method: Method) -> impl Responder { - match method { - Method::GET => match id { - Some(id) => HttpResponse::Ok() - .content_type(ContentType::plaintext()) - .body(format!( - "You are logged in. Welcome! ({:?})", - id.id().unwrap() - )), - None => HttpResponse::Unauthorized().finish(), - }, - _ => HttpResponse::Forbidden().finish(), - } -} - -#[get("/user")] -async fn get_user(id: Identity, repo: web::Data) -> impl Responder { - match repo.read_all() { - Ok(users) => { - log::info!("{users:?}"); - HttpResponse::Ok().body(format!("{users:?}")) - } - Err(err) => { - log::error!("Could not read from user repo: {err}"); - HttpResponse::InternalServerError() - .body(format!("Could not read from user repo: {err}")) - } - } -} - -// TODO: Guard for login -#[put("/user")] -async fn put_user( - id: Identity, - payload: web::Json, - session: Session, - repo: web::Data, -) -> impl Responder { - log::debug!("Received {payload:?}"); - match repo.update(&payload.0) { - Ok(_) => HttpResponse::Ok().finish(), - Err(_) => { - let msg = format!("No user with that id: {payload:?}"); - log::debug!("{}", msg); - HttpResponse::InternalServerError().body(msg) - } - } -} - -// TODO: For a real app, impl smth like a registration-secret or email verification -#[post("/user")] -async fn post_user(payload: web::Json, repo: web::Data) -> impl Responder { - log::debug!("Received {payload:?}"); - let user = User::new(payload.login(), payload.hash(), payload.salt()); - match repo.create(&user) { - Ok(_) => { - log::debug!("Successfully created {user:?}"); - HttpResponse::Created().finish() - } - Err(err) => { - log::debug!("{err}"); - HttpResponse::BadRequest().body(format!("{user:?}")) - } - } -} - -#[post("/login")] -async fn post_login( - req: HttpRequest, - payload: web::Json, - repo: web::Data, - session: Session, -) -> impl Responder { - log::debug!("Received {payload:?}"); - match repo.read(&payload.id()) { - Ok(Some(user)) => { - if payload.salt() == "" { - log::debug!("Initial login request with empty salt: {payload:?}"); - HttpResponse::Ok().json(format!("{{ 'salt': '{}' }}", user.salt())) - } else if payload.hash() == user.hash() { - log::debug!("User successfully logged in: {payload:?} == {user:?}"); - // TODO: Mayb handle more gracefully - Identity::login(&req.extensions(), format!("{}", payload.id())) - .expect("Log the user in"); - - // TODO: Mayb handle more gracefully - session - .insert("user", payload.0) - .expect("Insert user into session"); - HttpResponse::Ok().finish() - } else { - log::debug!("Wrong password hash for user: {payload:?} != {user:?}"); - HttpResponse::Unauthorized().finish() - } - } - Ok(None) => { - log::debug!("User not found: {payload:?}"); - HttpResponse::Unauthorized().finish() - } - Err(_) => { - let msg = format!("Could not create user: {payload:?}"); - log::debug!("{}", msg); - HttpResponse::InternalServerError().body(msg) - } - } -} - -#[delete("/logout")] -async fn delete_logout(id: Identity) -> impl Responder { - id.logout(); - HttpResponse::Ok() -} - +/// Starts the webserver and initializes required repos, etc. #[actix_web::main] async fn main() -> io::Result<()> { env_logger::init_from_env(env_logger::Env::new().default_filter_or("debug")); - let user = User::new("admin", "init_pw_hash", "init_salt"); - log::info!("{user:#?}"); - let todo = Todo::new( - user.id().clone(), - "Mein todo", - "Es hat viele Aufgaben", - Priority::Normal, - Status::Todo, - ); - log::info!("{todo:#?}"); - + // TODO: Currently I do not handle the case where the cassandra server is + // rebooted during the lifetime of the webserver and loses its configuration. + // Normally this is not a real scenario because the tables should be persistent + // througout reboots but idk, it would still be cool to handle this gracefully. + // What would have to happen is a reinit of the repo classes after the connection + // is regained to reinitialize the perhaps missing keyspaces/tables let cassandra_session = Arc::new(repo::init()); + // TODO: web::Data is already an Arc + //-> Investigate if we really need the session as an Arc as well + let user = User::new("admin", "init_pw_hash", "init_salt"); let user_repo = web::Data::new(UserRepository::new(Arc::clone(&cassandra_session))); if let Err(err) = user_repo.create(&user) { log::debug!("Default user already exists: {err}"); } + + let todo_repo = web::Data::new(TodoRepository::new(Arc::clone(&cassandra_session))); + + // Secret key used to init session storage let key = Key::generate(); log::info!("Starting HTTP server: http://127.0.0.1:6969"); @@ -173,13 +61,16 @@ async fn main() -> io::Result<()> { ) .wrap(middleware::Logger::default()) .app_data(user_repo.clone()) - // .service(get_counter) - .service(get_user) - .service(put_user) - .service(post_user) - .service(post_login) - .service(delete_logout) - .default_service(web::to(index)) + .app_data(todo_repo.clone()) + .service(user_routes::get_user) + .service(user_routes::put_user) + .service(user_routes::post_user) + .service(todo_routes::get_todo) + .service(todo_routes::put_todo) + .service(todo_routes::delete_todo) + .service(routes::post_login) + .service(routes::delete_logout) + .default_service(web::to(routes::index)) }) .bind(("127.0.0.1", 6969))? .workers(2) // number of workers per bind default ist #cpus diff --git a/rust_solid_cassandra/backend/src/model/todo.rs b/rust_solid_cassandra/backend/src/model/todo.rs index 6e320e3..0a8a707 100644 --- a/rust_solid_cassandra/backend/src/model/todo.rs +++ b/rust_solid_cassandra/backend/src/model/todo.rs @@ -1,20 +1,21 @@ +use serde::{Deserialize, Serialize}; use uuid::Uuid; -#[derive(Debug)] +#[derive(Debug, Copy, Clone, Deserialize, Serialize)] pub enum Priority { Low, Normal, High, } -#[derive(Debug)] +#[derive(Debug, Copy, Clone, Deserialize, Serialize)] pub enum Status { Todo, Doing, Done, } -#[derive(Debug)] +#[derive(Debug, Deserialize, Serialize)] pub struct Todo { id: Uuid, user_id: Uuid, @@ -25,20 +26,38 @@ pub struct Todo { } impl Todo { - pub fn new( - user_id: Uuid, - title: &str, - description: &str, - priority: Priority, - status: Status, - ) -> Todo { - Todo { - id: Uuid::new_v4(), - user_id, - title: String::from(title), - description: String::from(description), - priority, - status, - } + // TODO: Maybe return Result + pub fn from_json(json: &str) -> Todo { + log::debug!("Creating Todo from {json}"); + serde_json::from_str::(json).expect("Deserialized Todo object") + } + + pub fn to_json(&self) -> Result { + Ok(serde_json::to_string(self)?) + } + + pub fn set_user_id(&mut self, user_id: Uuid) { + self.user_id = user_id + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn todo_from_json() { + let json = "\ +{ + \"id\": \"00000000000000000000000000000000\", + \"user_id\": \"11111111111111111111111111111111\", + \"title\": \"Test Title\", + \"description\": \"This is a cool description for my very cool todo!\", + \"priority\": 1, + \"status\": 1 +}"; + println!("{json}"); + let todo = Todo::from_json(json); + println!("{todo:?}"); } } diff --git a/rust_solid_cassandra/backend/src/model/user.rs b/rust_solid_cassandra/backend/src/model/user.rs index 1b8df87..6ee6b13 100644 --- a/rust_solid_cassandra/backend/src/model/user.rs +++ b/rust_solid_cassandra/backend/src/model/user.rs @@ -19,8 +19,9 @@ impl User { } } + // TODO: Maybe return Result pub fn from_json(json: &str) -> User { - log::debug!("{json}"); + log::debug!("Creating User from {json}"); serde_json::from_str::(json).expect("Deserialized User object") } diff --git a/rust_solid_cassandra/backend/src/repo.rs b/rust_solid_cassandra/backend/src/repo.rs index b597066..8c881ee 100644 --- a/rust_solid_cassandra/backend/src/repo.rs +++ b/rust_solid_cassandra/backend/src/repo.rs @@ -1,12 +1,17 @@ -use std::{env, thread::sleep, time::Duration}; +use std::{env, thread::sleep, time::Duration, error::Error}; use cassandra_cpp::{stmt, Cluster, Session}; pub mod todo_repository; pub mod user_repository; +/// Repository Result type to use custom errors (abbreviation) +type Result = std::result::Result>; + +/// Keyspace name if none is given via ENV: KEYSPACE_NAME static DEFAULT_KEYSPACE_NAME: &str = "rust_solidjs_cassandra"; +/// Waits for the cassandra database to become available -> then returns a session. pub fn init() -> Session { let keyspace_name = env::var("KEYSPACE_NAME").unwrap_or(DEFAULT_KEYSPACE_NAME.to_string()); // Definitely set it so other modules can use it @@ -14,10 +19,15 @@ pub fn init() -> Session { let mut cluster = Cluster::default(); cluster.set_contact_points("127.0.0.1").unwrap(); // Panic if not successful + let mut count = 0; let session = loop { match cluster.connect() { Ok(session) => break session, - Err(_) => sleep(Duration::new(1, 0)), + Err(_) => { + count += 1; + log::warn!("Could not connect to database! Retrying... ({count})"); + sleep(Duration::new(5, 0)); + }, } }; let query = stmt!(&format!( diff --git a/rust_solid_cassandra/backend/src/repo/todo_repository.rs b/rust_solid_cassandra/backend/src/repo/todo_repository.rs index 62ce61b..aaae3bf 100644 --- a/rust_solid_cassandra/backend/src/repo/todo_repository.rs +++ b/rust_solid_cassandra/backend/src/repo/todo_repository.rs @@ -1,4 +1,125 @@ +use std::{env, sync::Arc}; + +use cassandra_cpp::{stmt, Session}; + +use crate::repo::Result; +use crate::model::todo::Todo; + pub struct TodoRepository { - connection: String, + session: Arc, table: String, } + +impl TodoRepository { + pub fn new(session: Arc) -> TodoRepository { + let keyspace_name = + env::var("KEYSPACE_NAME").expect("Value should be definitely set in init"); + let table = format!("{keyspace_name}.todo"); + let query = stmt!(&format!( + "CREATE TABLE IF NOT EXISTS {table} ( + id uuid, + user_id uuid, + title text, + priority varchar, + status varchar, + description text, + PRIMARY KEY (id, user_id) + );" + )); + + session + .execute(&query) + .wait() + .expect("Should create todo table if not exists"); + + // Create a secondary index so we can also query for todos of specific users + // without the use of ALLOW FILTERING (because this might be slow) + let query = stmt!(&format!("CREATE INDEX IF NOT EXISTS ON {table} (user_id);")); + + session + .execute(&query) + .wait() + .expect("Should create secondary index for user_id column"); + + TodoRepository { session, table } + } + + /// Creates OR updates the provided todo. + pub fn create(&self, todo: &Todo) -> Result<()> { + // We can just create todos, nothing to check beforehand + // Trying out the JSON syntax + // This is quite nice since cassandra just updates the record if + // the primary key (id, user_id) are already present + let json = todo.to_json()?; + log::debug!("Got this json for the query: {json:?}"); + let query = stmt!(&format!("INSERT INTO {} JSON '{}';", self.table, json)); + + self.session.execute(&query).wait()?; + Ok(()) + } + + // REMARK: Not required right now + // pub fn read(&self, id: &uuid::Uuid) -> Result> { + // let uuid = cassandra_cpp::Uuid::from(id.clone()); + + // let mut query = stmt!(&format!("SELECT JSON * FROM {} WHERE id = ?", self.table)); + // query.bind_uuid(0, uuid).expect("Binds id"); + + // Ok(match self.session.execute(&query).wait()?.first_row() { + // Some(row) => Some(Todo::from_json(&format!("{row}"))), + // None => None, + // }) + // } + + // REMARK: Not required right now + // pub fn read_all(&self) -> Result> { + // let query = stmt!(&format!("SELECT JSON * FROM {}", self.table)); + // Ok(self + // .session + // .execute(&query) + // .wait()? + // .iter() + // .map(|json| Todo::from_json(&format!("{json}"))) + // .collect()) + // } + + pub fn read_all_by_user_id(&self, user_id: &uuid::Uuid) -> Result> { + let mut query = stmt!(&format!( + "SELECT JSON * FROM {} WHERE user_id = ?", + self.table + )); + + let uuid = cassandra_cpp::Uuid::from(user_id.clone()); + query.bind_uuid(0, uuid).expect("Bind user_id"); + + Ok(self + .session + .execute(&query) + .wait()? + .iter() + .map(|json| Todo::from_json(&format!("{json}"))) + .collect()) + } + + // REMARK: Not required right now + // pub fn update(&self, todo: &Todo) -> Result<()> { + // // Read the comment in create + // self.create(todo) + // } + + pub fn delete(&self, id: &uuid::Uuid, user_id: &uuid::Uuid) -> Result<()> { + + let mut query = stmt!(&format!( + "DELETE FROM {} WHERE id = ? AND user_id = ?", + self.table + )); + + let id_uuid = cassandra_cpp::Uuid::from(id.clone()); + let user_id_uuid = cassandra_cpp::Uuid::from(user_id.clone()); + query.bind_uuid(0, id_uuid).expect("Bind id"); + query.bind_uuid(1, user_id_uuid).expect("Bind user_id"); + + self.session.execute(&query).wait()?; + Ok(()) + } +} diff --git a/rust_solid_cassandra/backend/src/repo/user_repository.rs b/rust_solid_cassandra/backend/src/repo/user_repository.rs index 9dd3b59..e3b6b7a 100644 --- a/rust_solid_cassandra/backend/src/repo/user_repository.rs +++ b/rust_solid_cassandra/backend/src/repo/user_repository.rs @@ -1,14 +1,45 @@ -use std::{env, sync::Arc}; +use std::{env, error::Error, fmt::Display, sync::Arc}; -use cassandra_cpp::{stmt, ErrorKind, Result, Session}; +use cassandra_cpp::{stmt, Session}; use crate::model::user::User; +use crate::repo::Result; + +// --------------------------------------------------------------------------------- +// Errors +// --------------------------------------------------------------------------------- + +#[derive(Debug)] +struct UserAlreadyExistsError; +#[derive(Debug)] +struct UserDoesNotExistError; + +impl Display for UserAlreadyExistsError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "User already exists!") + } +} + +impl Display for UserDoesNotExistError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "User does not exists!") + } +} + +impl Error for UserAlreadyExistsError {} +impl Error for UserDoesNotExistError {} + +// --------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------- pub struct UserRepository { session: Arc, table: String, } +/// This is an abstraction for the cassandra db used here. +/// It provides the typical CRUD actions. impl UserRepository { pub fn new(session: Arc) -> UserRepository { let keyspace_name = @@ -27,7 +58,7 @@ impl UserRepository { session .execute(&query) .wait() - .expect("Should create user keyspace if not exists"); + .expect("Should create user table if not exists"); let query = stmt!(&format!("CREATE INDEX IF NOT EXISTS ON {table} (login);")); @@ -40,10 +71,8 @@ impl UserRepository { } pub fn create(&self, user: &User) -> Result<()> { - if let Some(u) = self.read_by_login(user.login())? { - return Err(cassandra_cpp::Error::from(ErrorKind::Msg(format!( - "Creation of {u:?} failed. User already exists." - )))); + if let Some(_) = self.read_by_login(user.login())? { + return Err(Box::new(UserAlreadyExistsError)); } let mut query = stmt!(&format!( @@ -61,29 +90,16 @@ impl UserRepository { Ok(()) } - pub fn update(&self, user: &User) -> Result<()> { - match self.read(&user.id())? { - Some(u) => { - log::info!("Modifying {u:?} to represent {user:?}"); - - let mut query = stmt!(&format!( - "UPDATE {} SET login = ?, hash = ?, salt = ? - WHERE id = ?;", - self.table - )); + pub fn read(&self, id: &uuid::Uuid) -> Result> { + let uuid = cassandra_cpp::Uuid::from(id.clone()); + // There is no OR in cassandra statements + let mut query = stmt!(&format!("SELECT JSON * FROM {} WHERE id = ?", self.table)); + query.bind_uuid(0, uuid).expect("Binds id"); - let uuid = cassandra_cpp::Uuid::from(user.id().clone()); - query.bind_string(0, user.login()).expect("Binds the login"); - query.bind_string(1, user.hash()).expect("Binds the hash"); - query.bind_string(2, user.salt()).expect("Binds the salt"); - query.bind_uuid(3, uuid).expect("Binds the id"); - self.session.execute(&query).wait()?; - Ok(()) - } - None => Err(cassandra_cpp::Error::from(ErrorKind::Msg(format!( - "User {user:?} does not exist in the database" - )))), - } + Ok(match self.session.execute(&query).wait()?.first_row() { + Some(row) => Some(User::from_json(&format!("{row}"))), + None => None, + }) } fn read_by_login(&self, login: &str) -> Result> { @@ -98,18 +114,6 @@ impl UserRepository { }) } - pub fn read(&self, id: &uuid::Uuid) -> Result> { - let uuid = cassandra_cpp::Uuid::from(id.clone()); - // There is no OR in cassandra statements - let mut query = stmt!(&format!("SELECT JSON * FROM {} WHERE id = ?", self.table)); - query.bind_uuid(0, uuid).expect("Binds login"); - - Ok(match self.session.execute(&query).wait()?.first_row() { - Some(row) => Some(User::from_json(&format!("{row}"))), - None => None, - }) - } - pub fn read_all(&self) -> Result> { let query = stmt!(&format!("SELECT JSON * FROM {};", self.table)); Ok(self @@ -120,4 +124,28 @@ impl UserRepository { .map(|json| User::from_json(&format!("{json}"))) .collect()) } + + pub fn update(&self, user: &User) -> Result<()> { + match self.read(&user.id())? { + Some(u) => { + log::info!("Modifying {u:?} to represent {user:?}"); + + let mut query = stmt!(&format!( + "UPDATE {} SET login = ?, hash = ?, salt = ? + WHERE id = ?;", + self.table + )); + + let uuid = cassandra_cpp::Uuid::from(user.id().clone()); + query.bind_string(0, user.login()).expect("Binds the login"); + query.bind_string(1, user.hash()).expect("Binds the hash"); + query.bind_string(2, user.salt()).expect("Binds the salt"); + query.bind_uuid(3, uuid).expect("Binds the id"); + self.session.execute(&query).wait()?; + Ok(()) + } + None => Err(Box::new(UserDoesNotExistError)), + } + } + } diff --git a/rust_solid_cassandra/backend/src/routes.rs b/rust_solid_cassandra/backend/src/routes.rs new file mode 100644 index 0000000..66ced43 --- /dev/null +++ b/rust_solid_cassandra/backend/src/routes.rs @@ -0,0 +1,93 @@ +use actix_identity::Identity; +use actix_session::Session; +use actix_web::{ + delete, + http::{header::ContentType, Method}, + post, web, HttpRequest, HttpResponse, Responder, HttpMessage, +}; + +use crate::{model::user::User, repo::user_repository::UserRepository}; + +pub mod todo_routes; +pub mod user_routes; + +/// Helper function to get a valid User from session. +fn get_user_from_session(session: Session) -> Option { + match session.get::("user") { + Ok(Some(user)) => Some(user), + Ok(None) => { + log::warn!("Could no DESERIALIZE user from session despite someone is logged in"); + None + } + Err(err) => { + log::warn!("Could no GET user from session despite someone is logged in: {err}"); + None + } + } +} + + +/// Handles any route that is not defined explicitly. +pub async fn index(id: Option, method: Method) -> impl Responder { + match method { + Method::GET => match id { + Some(id) => HttpResponse::Ok() + .content_type(ContentType::plaintext()) + .body(format!( + "You are logged in. Welcome! ({:?})", + id.id().unwrap() + )), + None => HttpResponse::Unauthorized().body("Please log in to continue!"), + }, + _ => HttpResponse::Forbidden().finish(), + } +} + +/// Creates an Identity in the session if the provided credentials are valid. +#[post("/login")] +pub async fn post_login( + req: HttpRequest, + payload: web::Json, + repo: web::Data, + session: Session, +) -> impl Responder { + log::debug!("Received {payload:?}"); + match repo.read(&payload.id()) { + Ok(Some(user)) => { + if payload.salt() == "" { + log::debug!("Initial login request with empty salt: {payload:?}"); + HttpResponse::Ok().json(format!("{{ 'salt': '{}' }}", user.salt())) + } else if payload.hash() == user.hash() { + log::debug!("User successfully logged in: {payload:?} == {user:?}"); + // TODO: Mayb handle more gracefully + Identity::login(&req.extensions(), format!("{}", payload.id())) + .expect("Log the user in"); + + // TODO: Mayb handle more gracefully + session + .insert("user", payload.0) + .expect("Insert user into session"); + HttpResponse::Ok().finish() + } else { + log::debug!("Wrong password hash for user: {payload:?} != {user:?}"); + HttpResponse::Unauthorized().finish() + } + } + Ok(None) => { + log::debug!("User not found: {payload:?}"); + HttpResponse::Unauthorized().finish() + } + Err(_) => { + let msg = format!("Could not create user: {payload:?}"); + log::debug!("{}", msg); + HttpResponse::InternalServerError().body(msg) + } + } +} + +/// Removes the current Identity from the session -> logs the user out. +#[delete("/logout")] +pub async fn delete_logout(id: Identity) -> impl Responder { + id.logout(); + HttpResponse::Ok() +} diff --git a/rust_solid_cassandra/backend/src/routes/todo_routes.rs b/rust_solid_cassandra/backend/src/routes/todo_routes.rs new file mode 100644 index 0000000..8db27b9 --- /dev/null +++ b/rust_solid_cassandra/backend/src/routes/todo_routes.rs @@ -0,0 +1,79 @@ +use actix_identity::Identity; +use actix_session::Session; +use actix_web::{ + delete, get, put, + web::{self, Json}, + HttpResponse, Responder, +}; + +use crate::{ + model::todo::Todo, repo::todo_repository::TodoRepository, routes::get_user_from_session, +}; + +// IDEA: Depending on role see all todos not just your own (Group todos) +/// Fetches a list of all todos belonging to the currently logged in user. +#[get("/todo")] +pub async fn get_todo( + _id: Identity, + session: Session, + repo: web::Data, +) -> impl Responder { + match get_user_from_session(session) { + Some(user) => match repo.read_all_by_user_id(user.id()) { + Ok(todos) => HttpResponse::Ok().json(todos), + Err(err) => { + log::warn!("Could not fetch todos: {err}"); + HttpResponse::NoContent().finish() + } + }, + None => HttpResponse::BadRequest().finish(), + } +} + +// IDEA: Let users create todos for other users if role allows it +/// Creates or updates the todo provided in the body of the request. +#[put("/todo")] +pub async fn put_todo( + _id: Identity, + session: Session, + repo: web::Data, + todo: Json, +) -> impl Responder { + let mut todo = todo; // To set user_id from session + match get_user_from_session(session) { + Some(user) => { + todo.set_user_id(user.id().clone()); + match repo.create(&todo) { + Ok(_) => HttpResponse::Ok().finish(), + Err(err) => { + log::warn!("Error while creating or updating {todo:?}. ERROR: {err}"); + HttpResponse::BadRequest().finish() + } + } + } + None => HttpResponse::BadRequest().finish(), + } +} + +/// Deletes a todo via its id which is provided in the url. +/// Example `http://localhost/todo/uuid-of-the-todo-to-delete`. +#[delete("/todo/{todo_id}")] +pub async fn delete_todo( + _id: Identity, + session: Session, + repo: web::Data, + todo_id: web::Path, +) -> impl Responder { + match get_user_from_session(session) { + Some(user) => { + match repo.delete(&todo_id, user.id()) { + Ok(_) => HttpResponse::Ok().finish(), + Err(err) => { + log::warn!("Error while deleting todo with id {todo_id}: {err}"); + HttpResponse::BadRequest().finish() + } + } + } + None => HttpResponse::BadRequest().finish(), + } +} diff --git a/rust_solid_cassandra/backend/src/routes/user_routes.rs b/rust_solid_cassandra/backend/src/routes/user_routes.rs new file mode 100644 index 0000000..6cd8375 --- /dev/null +++ b/rust_solid_cassandra/backend/src/routes/user_routes.rs @@ -0,0 +1,62 @@ +use actix_identity::Identity; +// TODO: Guard for login +use actix_web::{get, web, Responder, HttpResponse, put, post}; + +use crate::{repo::user_repository::UserRepository, model::user::User}; + +// TODO: Only allow if role is admin (If I implement roles for this evaluation...) +/// Returns a json list of all users. +#[get("/user")] +pub async fn get_user(_id: Identity, repo: web::Data) -> impl Responder { + match repo.read_all() { + Ok(users) => { + HttpResponse::Ok().json(users) + } + Err(err) => { + log::error!("Could not read from user repo: {err}"); + HttpResponse::InternalServerError() + .body(format!("Could not read from user repo: {err}")) + } + } +} + +// REMARK: This uses the non-JSON way of inserting into cassandra +// this is why we need two separate REST endpoints to do the creates/updates +// whereas the todo api only requires PUT (it uses the JSON INSERT in cassandra) +// TODO: Only admin users should be able to modify other users but users should be +// able to modify themself -> Requires roles to be implemented +/// Updates an existing user. +#[put("/user")] +pub async fn put_user( + _id: Identity, + payload: web::Json, + repo: web::Data, +) -> impl Responder { + log::debug!("Received {payload:?}"); + match repo.update(&payload.0) { + Ok(_) => HttpResponse::Ok().finish(), + Err(err) => { + let msg = format!("{err}: {payload:?}"); + log::debug!("{}", msg); + HttpResponse::InternalServerError().body(msg) + } + } +} + +// TODO: For a real app, impl smth like a registration-secret or email verification +/// Creates a new user. +#[post("/user")] +pub async fn post_user(payload: web::Json, repo: web::Data) -> impl Responder { + log::debug!("Received {payload:?}"); + let user = User::new(payload.login(), payload.hash(), payload.salt()); + match repo.create(&user) { + Ok(_) => { + log::debug!("Successfully created {user:?}"); + HttpResponse::Created().finish() + } + Err(err) => { + log::debug!("{err}"); + HttpResponse::BadRequest().body(format!("{user:?}")) + } + } +} \ No newline at end of file diff --git a/rust_solid_cassandra/deploy/cqlsh.sh b/rust_solid_cassandra/deploy/cqlsh.sh new file mode 100755 index 0000000..d2c8adc --- /dev/null +++ b/rust_solid_cassandra/deploy/cqlsh.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +docker run --rm -it nuvo/docker-cqlsh cqlsh 10.10.10.65 9042 --cqlversion='3.4.5' diff --git a/rust_solid_cassandra/deploy/docker-compose.yml b/rust_solid_cassandra/deploy/docker-compose.yml index 5c072c1..94bbd39 100644 --- a/rust_solid_cassandra/deploy/docker-compose.yml +++ b/rust_solid_cassandra/deploy/docker-compose.yml @@ -13,6 +13,8 @@ services: image: cassandra:latest container_name: cassandra hostname: cassandra + volumes: + - ../data/cassandra:/var/lib/cassandra # DEVEL ports: - '9042:9042' \ No newline at end of file