# 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
}
pub async fn load_user(&self, user_id: i64) -> Result<Option<model::User>> {
- 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(())
}
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?;
Ok(model::User {
id: row.try_get("id")?,
email: row.try_get("email")?,
+ name: row.try_get("name")?,
})
}
}
"/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(),
#[derive(Debug, Clone)]\r
pub struct User {\r
pub id: i64,\r
+ pub name: String,\r
pub email: String,\r
}\r
\r
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 {
///// EDIT PROFILE /////
+#[derive(Template)]
+#[template(path = "profile.html")]
+struct ProfileTemplate {
+ user: Option<model::User>,
+}
+
#[debug_handler]
pub async fn edit_user(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
-) -> Result<impl IntoResponse> {
- Ok("todo")
+) -> Response {
+ if user.is_some() {
+ ProfileTemplate { user }.into_response()
+ } else {
+ MessageTemplate {
+ user: None,
+ message: "Not logged in",
+ }
+ .into_response()
+ }
}
///// 404 /////
--- /dev/null
+// 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<db::Connection>,
+// ) -> Result<HttpResponse> {
+// 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<db::Connection>,
+// ) -> Result<HttpResponse> {
+// 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<db::Connection>,
+ Extension(user): Extension<Option<model::User>>,
+ ExtractRon(ron): ExtractRon<common::ron_api::UpdateProfile>,
+) -> Result<StatusCode> {
+ 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<db::Connection>,
+ Extension(user): Extension<Option<model::User>>,
+ ExtractRon(ron): ExtractRon<common::ron_api::SetProfileName>,
+) -> Result<impl IntoResponse> {
+
+
+ Ok(ron_response(common::ron_api::SetProfileName {
+ name: "abc".to_string(),
+ }))
+}
+*/
+++ /dev/null
-// 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<db::Connection>,
-// ) -> Result<HttpResponse> {
-// 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<db::Connection>,
-// ) -> Result<HttpResponse> {
-// 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<db::Connection>,
- Extension(user): Extension<Option<model::User>>,
- ExtractRon(ron): ExtractRon<common::ron_api::SetProfileName>,
-) -> Result<StatusCode> {
- 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<db::Connection>,
- Extension(user): Extension<Option<model::User>>,
- ExtractRon(ron): ExtractRon<common::ron_api::SetProfileName>,
-) -> Result<impl IntoResponse> {
-
-
- Ok(ron_response(common::ron_api::SetProfileName {
- name: "abc".to_string(),
- }))
-}
-*/
<div class="content">
<form action="/ask_reset_password" method="post">
<label for="email_field">Your email address</label>
- <input id="email_field" type="text" name="email" value="{{ email }}" autocapitalize="none" autocomplete="email" autofocus="autofocus" />
+ <input id="email_field" type="email" name="email" value="{{ email }}" autocapitalize="none" autocomplete="email" autofocus="autofocus" />
{{ message_email }}
<input type="submit" name="commit" value="Ask reset" />
-{% extends "base_with_list.html" %}
-
-{% block content %}
-<label for="title_field">Name</label>
-<input
- id="name_field"
- type="text"
- name="name"
- value="{{ user.name }}"
- autocapitalize="none"
- autocomplete="title"
- autofocus="autofocus" />
+{% extends "base_with_header.html" %}
+
+{% block main_container %}
+
+<h2>Profile</h2>
+
+{% match user %}
+{% when Some with (user) %}
+
+<div id="user-edit">
+
+ <label for="title_field">Name</label>
+ <input
+ id="name_field"
+ type="text"
+ name="name"
+ value="{{ user.name }}"
+ autocapitalize="none"
+ autocomplete="title"
+ autofocus="autofocus" />
+
+
+ <label for="password_field_1">New password (minimum 8 characters)</label>
+ <input id="password_field_1" type="password" name="password_1" />
+
+ <label for="password_field_1">Re-enter password</label>
+ <input id="password_field_2" type="password" name="password_2" />
+
+ <button class="button" typed="button">Save</button>
+</div>
+
+{% when None %}
+{% endmatch %}
+
{% endblock %}
\ No newline at end of file
<div class="content">
<form action="/signin" method="post">
<label for="email_field">Email address</label>
- <input id="email_field" type="text" name="email" value="{{ email }}" autocapitalize="none" autocomplete="email" autofocus="autofocus" />
+ <input id="email_field" type="email" name="email" value="{{ email }}" autocapitalize="none" autocomplete="email" autofocus="autofocus" />
<label for="password_field">Password</label>
<input id="password_field" type="password" name="password" autocomplete="current-password" />
<div class="content">
<form action="/signup" method="post">
<label for="email_field">Your email address</label>
- <input id="email_field" type="text" name="email" value="{{ email }}" autocapitalize="none" autocomplete="email" autofocus="autofocus" />
+ <input id="email_field" type="email" name="email" value="{{ email }}" autocapitalize="none" autocomplete="email" autofocus="autofocus" />
{{ message_email }}
<label for="password_field_1">Choose a password (minimum 8 characters)</label>
///// PROFILE /////
#[derive(Serialize, Deserialize, Clone)]
-pub struct SetProfileName {
- pub name: String,
+pub struct UpdateProfile {
+ pub name: Option<String>,
+ pub email: Option<String>,
+ pub password: Option<String>,
}
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
# 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"
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::<>()
+}
let id = id.parse::<i64>().unwrap(); // TODO: remove unwrap.
console_log!("recipe edit ID: {}", id);
- handles::edit_recipe(&document);
+ handles::recipe_edit(&document);
+ }
+
+ ["user", "edit"] => {
+ handles::user_edit(&document);
}
_ => (),
}