From: Greg Burri Date: Fri, 15 Nov 2024 13:47:10 +0000 (+0100) Subject: Profile edit (WIP) X-Git-Url: http://git.euphorik.ch/index.cgi?a=commitdiff_plain;ds=sidebyside;p=recipes.git Profile edit (WIP) --- diff --git a/backend/launch_debug.nu b/backend/launch_debug.nu index d095b12..5e20487 100644 --- a/backend/launch_debug.nu +++ b/backend/launch_debug.nu @@ -1,2 +1,2 @@ # To launch RUP and watching source. See https://actix.rs/docs/autoreload/. -cargo [watch -x run] \ No newline at end of file +cargo watch -x run \ No newline at end of file diff --git a/backend/src/data/db.rs b/backend/src/data/db.rs index 8fccd78..0820cb5 100644 --- a/backend/src/data/db.rs +++ b/backend/src/data/db.rs @@ -244,19 +244,46 @@ FROM [UserLoginToken] WHERE [token] = $1 } pub async fn load_user(&self, user_id: i64) -> Result> { - sqlx::query_as("SELECT [id], [email] FROM [User] WHERE [id] = $1") + sqlx::query_as("SELECT [id], [email], [name] FROM [User] WHERE [id] = $1") .bind(user_id) .fetch_optional(&self.pool) .await .map_err(DBError::from) } - pub async fn set_user_name(&self, user_id: i64, name: &str) -> Result<()> { - sqlx::query("UPDATE [User] SET [name] = $2 WHERE [id] = $1") - .bind(user_id) - .bind(name) - .execute(&self.pool) - .await?; + pub async fn update_user( + &self, + user_id: i64, + new_email: Option<&str>, + new_name: Option<&str>, + new_password: Option<&str>, + ) -> Result<()> { + let mut tx = self.tx().await?; + let hashed_new_password = new_password.map(|p| hash(p).unwrap()); + + let (email, name, password) = sqlx::query_as::<_, (String, String, String)>( + "SELECT [email], [name], [password] FROM [User] WHERE [id] = $1", + ) + .bind(user_id) + .fetch_one(&mut *tx) + .await?; + + sqlx::query( + r#" +UPDATE [User] +SET [email] = $2, [name] = $3, [password] = $4 +WHERE [id] = $1 + "#, + ) + .bind(user_id) + .bind(new_email.unwrap_or(&email)) + .bind(new_name.unwrap_or(&name)) + .bind(hashed_new_password.unwrap_or(password)) + .execute(&mut *tx) + .await?; + + tx.commit().await?; + Ok(()) } @@ -1075,6 +1102,59 @@ VALUES ( Ok(()) } + #[tokio::test] + async fn update_user() -> Result<()> { + let connection = Connection::new_in_memory().await?; + + connection.execute_sql( + sqlx::query( + r#" +INSERT INTO [User] + ([id], [email], [name], [password], [creation_datetime], [validation_token]) +VALUES + ($1, $2, $3, $4, $5, $6) + "# + ) + .bind(1) + .bind("paul@atreides.com") + .bind("paul") + .bind("$argon2id$v=19$m=4096,t=3,p=1$G4fjepS05MkRbTqEImUdYg$GGziE8uVQe1L1oFHk37lBno10g4VISnVqynSkLCH3Lc") + .bind("2022-11-29 22:05:04.121407300+00:00") + .bind(None::<&str>) // 'null'. + ).await?; + + let user = connection.load_user(1).await?.unwrap(); + + assert_eq!(user.name, "paul"); + assert_eq!(user.email, "paul@atreides.com"); + + connection + .update_user( + 1, + Some("muaddib@fremen.com"), + Some("muaddib"), + Some("Chani"), + ) + .await?; + + let user = connection.load_user(1).await?.unwrap(); + + assert_eq!(user.name, "muaddib"); + assert_eq!(user.email, "muaddib@fremen.com"); + + // Tets if password has been updated correctly. + if let SignInResult::Ok(_token, id) = connection + .sign_in("muaddib@fremen.com", "Chani", "127.0.0.1", "Mozilla/5.0") + .await? + { + assert_eq!(id, 1); + } else { + panic!("Can't sign in"); + } + + Ok(()) + } + #[tokio::test] async fn create_a_new_recipe_then_update_its_title() -> Result<()> { let connection = Connection::new_in_memory().await?; diff --git a/backend/src/data/utils.rs b/backend/src/data/utils.rs index c32cfa2..a692892 100644 --- a/backend/src/data/utils.rs +++ b/backend/src/data/utils.rs @@ -28,6 +28,7 @@ impl FromRow<'_, SqliteRow> for model::User { Ok(model::User { id: row.try_get("id")?, email: row.try_get("email")?, + name: row.try_get("name")?, }) } } diff --git a/backend/src/main.rs b/backend/src/main.rs index 2394670..74cc54c 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -95,9 +95,12 @@ async fn main() { "/reset_password", get(services::reset_password_get).post(services::reset_password_post), ) + // Recipes. .route("/recipe/view/:id", get(services::view_recipe)) + // User. + .route("/user/edit", get(services::edit_user)) // RON API. - .route("/user/set_name", put(services::ron_api::set_user_name)) + .route("/user/set_name", put(services::ron::update_user)) .layer(TraceLayer::new_for_http()) .route_layer(middleware::from_fn_with_state( state.clone(), diff --git a/backend/src/model.rs b/backend/src/model.rs index 011cab2..2cf53e8 100644 --- a/backend/src/model.rs +++ b/backend/src/model.rs @@ -3,6 +3,7 @@ use chrono::prelude::*; #[derive(Debug, Clone)] pub struct User { pub id: i64, + pub name: String, pub email: String, } diff --git a/backend/src/services.rs b/backend/src/services.rs index eee0117..7f9e75a 100644 --- a/backend/src/services.rs +++ b/backend/src/services.rs @@ -16,7 +16,7 @@ use tracing::{event, Level}; use crate::{config::Config, consts, data::db, email, model, utils, AppState}; -pub mod ron_api; +pub mod ron; impl axum::response::IntoResponse for db::DBError { fn into_response(self) -> Response { @@ -722,12 +722,26 @@ pub async fn reset_password_post( ///// EDIT PROFILE ///// +#[derive(Template)] +#[template(path = "profile.html")] +struct ProfileTemplate { + user: Option, +} + #[debug_handler] pub async fn edit_user( State(connection): State, Extension(user): Extension>, -) -> Result { - Ok("todo") +) -> Response { + if user.is_some() { + ProfileTemplate { user }.into_response() + } else { + MessageTemplate { + user: None, + message: "Not logged in", + } + .into_response() + } } ///// 404 ///// diff --git a/backend/src/services/ron.rs b/backend/src/services/ron.rs new file mode 100644 index 0000000..7592cb1 --- /dev/null +++ b/backend/src/services/ron.rs @@ -0,0 +1,95 @@ +// use actix_web::{ +// http::{header, header::ContentType, StatusCode}, +// post, put, web, HttpMessage, HttpRequest, HttpResponse, Responder, +// }; +// use log::{debug, error, info, log_enabled, Level}; +// use ron::de::from_bytes; + +// use super::Result; +// use crate::data::db; + +// #[put("/ron-api/recipe/set-title")] +// pub async fn set_recipe_title( +// req: HttpRequest, +// body: web::Bytes, +// connection: web::Data, +// ) -> Result { +// let ron_req: common::ron_api::SetRecipeTitle = from_bytes(&body)?; +// connection +// .set_recipe_title_async(ron_req.recipe_id, &ron_req.title) +// .await?; +// Ok(HttpResponse::Ok().finish()) +// } + +// #[put("/ron-api/recipe/set-description")] +// pub async fn set_recipe_description( +// req: HttpRequest, +// body: web::Bytes, +// connection: web::Data, +// ) -> Result { +// let ron_req: common::ron_api::SetRecipeDescription = from_bytes(&body)?; +// connection +// .set_recipe_description_async(ron_req.recipe_id, &ron_req.description) +// .await?; +// Ok(HttpResponse::Ok().finish()) +// } + +// #[put("/ron-api/recipe/add-image)] +// #[put("/ron-api/recipe/rm-photo")] +// #[put("/ron-api/recipe/add-ingredient")] +// #[put("/ron-api/recipe/rm-ingredient")] +// #[put("/ron-api/recipe/set-ingredients-order")] +// #[put("/ron-api/recipe/add-group")] +// #[put("/ron-api/recipe/rm-group")] +// #[put("/ron-api/recipe/set-groups-order")] +// #[put("/ron-api/recipe/add-step")] +// #[put("/ron-api/recipe/rm-step")] +// #[put("/ron-api/recipe/set-steps-order")] + +use askama_axum::IntoResponse; +use axum::{ + debug_handler, + extract::{Extension, State}, + http::StatusCode, + response::ErrorResponse, + response::Result, +}; +use tracing::{event, Level}; + +use crate::{ + data::db, + model, + ron_extractor::{ron_error, ron_response, ExtractRon}, +}; + +#[debug_handler] +pub async fn update_user( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon, +) -> Result { + if let Some(user) = user { + // connection.set_user_name(user.id, &ron.name).await?; + } else { + return Err(ErrorResponse::from(ron_error( + StatusCode::UNAUTHORIZED, + "Action not authorized", + ))); + } + Ok(StatusCode::OK) +} + +/* Example with RON return value. +#[debug_handler] +pub async fn set_user_name( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon, +) -> Result { + + + Ok(ron_response(common::ron_api::SetProfileName { + name: "abc".to_string(), + })) +} +*/ diff --git a/backend/src/services/ron_api.rs b/backend/src/services/ron_api.rs deleted file mode 100644 index b92b943..0000000 --- a/backend/src/services/ron_api.rs +++ /dev/null @@ -1,95 +0,0 @@ -// use actix_web::{ -// http::{header, header::ContentType, StatusCode}, -// post, put, web, HttpMessage, HttpRequest, HttpResponse, Responder, -// }; -// use log::{debug, error, info, log_enabled, Level}; -// use ron::de::from_bytes; - -// use super::Result; -// use crate::data::db; - -// #[put("/ron-api/recipe/set-title")] -// pub async fn set_recipe_title( -// req: HttpRequest, -// body: web::Bytes, -// connection: web::Data, -// ) -> Result { -// let ron_req: common::ron_api::SetRecipeTitle = from_bytes(&body)?; -// connection -// .set_recipe_title_async(ron_req.recipe_id, &ron_req.title) -// .await?; -// Ok(HttpResponse::Ok().finish()) -// } - -// #[put("/ron-api/recipe/set-description")] -// pub async fn set_recipe_description( -// req: HttpRequest, -// body: web::Bytes, -// connection: web::Data, -// ) -> Result { -// let ron_req: common::ron_api::SetRecipeDescription = from_bytes(&body)?; -// connection -// .set_recipe_description_async(ron_req.recipe_id, &ron_req.description) -// .await?; -// Ok(HttpResponse::Ok().finish()) -// } - -// #[put("/ron-api/recipe/add-image)] -// #[put("/ron-api/recipe/rm-photo")] -// #[put("/ron-api/recipe/add-ingredient")] -// #[put("/ron-api/recipe/rm-ingredient")] -// #[put("/ron-api/recipe/set-ingredients-order")] -// #[put("/ron-api/recipe/add-group")] -// #[put("/ron-api/recipe/rm-group")] -// #[put("/ron-api/recipe/set-groups-order")] -// #[put("/ron-api/recipe/add-step")] -// #[put("/ron-api/recipe/rm-step")] -// #[put("/ron-api/recipe/set-steps-order")] - -use askama_axum::IntoResponse; -use axum::{ - debug_handler, - extract::{Extension, State}, - http::StatusCode, - response::ErrorResponse, - response::Result, -}; -use tracing::{event, Level}; - -use crate::{ - data::db, - model, - ron_extractor::{ron_error, ron_response, ExtractRon}, -}; - -#[debug_handler] -pub async fn set_user_name( - State(connection): State, - Extension(user): Extension>, - ExtractRon(ron): ExtractRon, -) -> Result { - if let Some(user) = user { - connection.set_user_name(user.id, &ron.name).await?; - } else { - return Err(ErrorResponse::from(ron_error( - StatusCode::UNAUTHORIZED, - "Action not authorized", - ))); - } - Ok(StatusCode::OK) -} - -/* Example with RON return value. -#[debug_handler] -pub async fn set_user_name( - State(connection): State, - Extension(user): Extension>, - ExtractRon(ron): ExtractRon, -) -> Result { - - - Ok(ron_response(common::ron_api::SetProfileName { - name: "abc".to_string(), - })) -} -*/ diff --git a/backend/templates/ask_reset_password.html b/backend/templates/ask_reset_password.html index 4a5fc8d..246c844 100644 --- a/backend/templates/ask_reset_password.html +++ b/backend/templates/ask_reset_password.html @@ -4,7 +4,7 @@
- + {{ message_email }} diff --git a/backend/templates/profile.html b/backend/templates/profile.html index 9dc573c..42647d6 100644 --- a/backend/templates/profile.html +++ b/backend/templates/profile.html @@ -1,13 +1,35 @@ -{% extends "base_with_list.html" %} - -{% block content %} - - +{% extends "base_with_header.html" %} + +{% block main_container %} + +

Profile

+ +{% match user %} +{% when Some with (user) %} + +
+ + + + + + + + + + + + +
+ +{% when None %} +{% endmatch %} + {% endblock %} \ No newline at end of file diff --git a/backend/templates/sign_in_form.html b/backend/templates/sign_in_form.html index 1e54353..7edd530 100644 --- a/backend/templates/sign_in_form.html +++ b/backend/templates/sign_in_form.html @@ -4,7 +4,7 @@
- + diff --git a/backend/templates/sign_up_form.html b/backend/templates/sign_up_form.html index 54fe200..18d6f51 100644 --- a/backend/templates/sign_up_form.html +++ b/backend/templates/sign_up_form.html @@ -4,7 +4,7 @@
- + {{ message_email }} diff --git a/common/src/ron_api.rs b/common/src/ron_api.rs index ce7b2d1..1890c85 100644 --- a/common/src/ron_api.rs +++ b/common/src/ron_api.rs @@ -97,6 +97,8 @@ pub struct RemoveRecipeStep { ///// PROFILE ///// #[derive(Serialize, Deserialize, Clone)] -pub struct SetProfileName { - pub name: String, +pub struct UpdateProfile { + pub name: Option, + pub email: Option, + pub password: Option, } diff --git a/frontend/Cargo.toml b/frontend/Cargo.toml index 267ea63..e1b2e55 100644 --- a/frontend/Cargo.toml +++ b/frontend/Cargo.toml @@ -15,13 +15,15 @@ common = { path = "../common" } wasm-bindgen = "0.2" web-sys = { version = "0.3", features = [ - 'console', - 'Document', - 'Element', - 'HtmlElement', - 'Node', - 'Window', - 'Location', + "console", + "Document", + "Element", + "HtmlElement", + "Node", + "Window", + "Location", + "EventTarget", + "HtmlLabelElement", ] } # The `console_error_panic_hook` crate provides better debugging of panics by @@ -30,11 +32,6 @@ web-sys = { version = "0.3", features = [ # code size when deploying. console_error_panic_hook = { version = "0.1", optional = true } -# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size -# compared to the default allocator's ~10K. It is slower than the default -# allocator, however. -# wee_alloc = { version = "0.4", optional = true } - # [dev-dependencies] # wasm-bindgen-test = "0.3" diff --git a/frontend/src/handles.rs b/frontend/src/handles.rs index 55a927c..354f025 100644 --- a/frontend/src/handles.rs +++ b/frontend/src/handles.rs @@ -1,6 +1,10 @@ use wasm_bindgen::prelude::*; -use web_sys::Document; +use web_sys::{Document, HtmlLabelElement}; -pub fn edit_recipe(doc: &Document) { +pub fn recipe_edit(doc: &Document) { let title_input = doc.get_element_by_id("title_field").unwrap(); } + +pub fn user_edit(doc: &Document) { + // let name_input = doc.get_element_by_id("name_field").unwrap().dyn_ref::<>() +} diff --git a/frontend/src/lib.rs b/frontend/src/lib.rs index ddbe956..1f53a8d 100644 --- a/frontend/src/lib.rs +++ b/frontend/src/lib.rs @@ -39,7 +39,11 @@ pub fn main() -> Result<(), JsValue> { let id = id.parse::().unwrap(); // TODO: remove unwrap. console_log!("recipe edit ID: {}", id); - handles::edit_recipe(&document); + handles::recipe_edit(&document); + } + + ["user", "edit"] => { + handles::user_edit(&document); } _ => (), }