feat: Working backend with login/logout/todos/users/etc
This commit is contained in:
parent
ed02382138
commit
5d92bc6b2b
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
||||
**/.direnv
|
||||
/rust_solid_cassandra/data/
|
||||
|
@ -6,3 +6,4 @@
|
||||
**/.envrc
|
||||
**/dist
|
||||
**/node_modules
|
||||
/data
|
||||
|
@ -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<Identity>, 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<UserRepository>) -> 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<User>,
|
||||
session: Session,
|
||||
repo: web::Data<UserRepository>,
|
||||
) -> 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<User>, repo: web::Data<UserRepository>) -> 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<User>,
|
||||
repo: web::Data<UserRepository>,
|
||||
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
|
||||
|
@ -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::<Todo>(json).expect("Deserialized Todo object")
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> Result<String, serde_json::Error> {
|
||||
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:?}");
|
||||
}
|
||||
}
|
||||
|
@ -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::<User>(json).expect("Deserialized User object")
|
||||
}
|
||||
|
||||
|
@ -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<T> = std::result::Result<T, Box<dyn Error>>;
|
||||
|
||||
/// 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!(
|
||||
|
@ -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<Session>,
|
||||
table: String,
|
||||
}
|
||||
|
||||
impl TodoRepository {
|
||||
pub fn new(session: Arc<Session>) -> 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<Option<Todo>> {
|
||||
// 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<Vec<Todo>> {
|
||||
// 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<Vec<Todo>> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
@ -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<Session>,
|
||||
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<Session>) -> 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,6 +90,41 @@ impl UserRepository {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read(&self, id: &uuid::Uuid) -> Result<Option<User>> {
|
||||
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");
|
||||
|
||||
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<Option<User>> {
|
||||
let mut query = stmt!(&format!(
|
||||
"SELECT JSON * FROM {} WHERE login = ?",
|
||||
self.table
|
||||
));
|
||||
query.bind_string(0, login).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<Vec<User>> {
|
||||
let query = stmt!(&format!("SELECT JSON * FROM {};", self.table));
|
||||
Ok(self
|
||||
.session
|
||||
.execute(&query)
|
||||
.wait()?
|
||||
.iter()
|
||||
.map(|json| User::from_json(&format!("{json}")))
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn update(&self, user: &User) -> Result<()> {
|
||||
match self.read(&user.id())? {
|
||||
Some(u) => {
|
||||
@ -80,44 +144,8 @@ impl UserRepository {
|
||||
self.session.execute(&query).wait()?;
|
||||
Ok(())
|
||||
}
|
||||
None => Err(cassandra_cpp::Error::from(ErrorKind::Msg(format!(
|
||||
"User {user:?} does not exist in the database"
|
||||
)))),
|
||||
None => Err(Box::new(UserDoesNotExistError)),
|
||||
}
|
||||
}
|
||||
|
||||
fn read_by_login(&self, login: &str) -> Result<Option<User>> {
|
||||
let mut query = stmt!(&format!(
|
||||
"SELECT JSON * FROM {} WHERE login = ?",
|
||||
self.table
|
||||
));
|
||||
query.bind_string(0, login).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(&self, id: &uuid::Uuid) -> Result<Option<User>> {
|
||||
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<Vec<User>> {
|
||||
let query = stmt!(&format!("SELECT JSON * FROM {};", self.table));
|
||||
Ok(self
|
||||
.session
|
||||
.execute(&query)
|
||||
.wait()?
|
||||
.iter()
|
||||
.map(|json| User::from_json(&format!("{json}")))
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
93
rust_solid_cassandra/backend/src/routes.rs
Normal file
93
rust_solid_cassandra/backend/src/routes.rs
Normal file
@ -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<User> {
|
||||
match session.get::<User>("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<Identity>, 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<User>,
|
||||
repo: web::Data<UserRepository>,
|
||||
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()
|
||||
}
|
79
rust_solid_cassandra/backend/src/routes/todo_routes.rs
Normal file
79
rust_solid_cassandra/backend/src/routes/todo_routes.rs
Normal file
@ -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<TodoRepository>,
|
||||
) -> 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<TodoRepository>,
|
||||
todo: Json<Todo>,
|
||||
) -> 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<TodoRepository>,
|
||||
todo_id: web::Path<uuid::Uuid>,
|
||||
) -> 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(),
|
||||
}
|
||||
}
|
62
rust_solid_cassandra/backend/src/routes/user_routes.rs
Normal file
62
rust_solid_cassandra/backend/src/routes/user_routes.rs
Normal file
@ -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<UserRepository>) -> 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<User>,
|
||||
repo: web::Data<UserRepository>,
|
||||
) -> 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<User>, repo: web::Data<UserRepository>) -> 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:?}"))
|
||||
}
|
||||
}
|
||||
}
|
2
rust_solid_cassandra/deploy/cqlsh.sh
Executable file
2
rust_solid_cassandra/deploy/cqlsh.sh
Executable file
@ -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'
|
@ -13,6 +13,8 @@ services:
|
||||
image: cassandra:latest
|
||||
container_name: cassandra
|
||||
hostname: cassandra
|
||||
volumes:
|
||||
- ../data/cassandra:/var/lib/cassandra
|
||||
# DEVEL
|
||||
ports:
|
||||
- '9042:9042'
|
Loading…
x
Reference in New Issue
Block a user