parent
ed02382138
commit
5d92bc6b2b
@ -1 +1,2 @@
|
|||||||
**/.direnv
|
**/.direnv
|
||||||
|
/rust_solid_cassandra/data/
|
||||||
|
@ -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 {
|
pub struct TodoRepository {
|
||||||
connection: String,
|
session: Arc<Session>,
|
||||||
table: String,
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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()
|
||||||
|
}
|
@ -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(),
|
||||||
|
}
|
||||||
|
}
|
@ -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:?}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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'
|
Loading…
Reference in new issue