Profile edit (WIP)
This commit is contained in:
parent
405aa68526
commit
327b2d0a5b
15 changed files with 174 additions and 46 deletions
|
|
@ -1,2 +1,2 @@
|
||||||
# To launch RUP and watching source. See https://actix.rs/docs/autoreload/.
|
# To launch RUP and watching source. See https://actix.rs/docs/autoreload/.
|
||||||
cargo [watch -x run]
|
cargo watch -x run
|
||||||
|
|
@ -244,19 +244,46 @@ FROM [UserLoginToken] WHERE [token] = $1
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn load_user(&self, user_id: i64) -> Result<Option<model::User>> {
|
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)
|
.bind(user_id)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(DBError::from)
|
.map_err(DBError::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn set_user_name(&self, user_id: i64, name: &str) -> Result<()> {
|
pub async fn update_user(
|
||||||
sqlx::query("UPDATE [User] SET [name] = $2 WHERE [id] = $1")
|
&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)
|
.bind(user_id)
|
||||||
.bind(name)
|
.fetch_one(&mut *tx)
|
||||||
.execute(&self.pool)
|
|
||||||
.await?;
|
.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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1075,6 +1102,59 @@ VALUES (
|
||||||
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]
|
#[tokio::test]
|
||||||
async fn create_a_new_recipe_then_update_its_title() -> Result<()> {
|
async fn create_a_new_recipe_then_update_its_title() -> Result<()> {
|
||||||
let connection = Connection::new_in_memory().await?;
|
let connection = Connection::new_in_memory().await?;
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ impl FromRow<'_, SqliteRow> for model::User {
|
||||||
Ok(model::User {
|
Ok(model::User {
|
||||||
id: row.try_get("id")?,
|
id: row.try_get("id")?,
|
||||||
email: row.try_get("email")?,
|
email: row.try_get("email")?,
|
||||||
|
name: row.try_get("name")?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -95,9 +95,12 @@ async fn main() {
|
||||||
"/reset_password",
|
"/reset_password",
|
||||||
get(services::reset_password_get).post(services::reset_password_post),
|
get(services::reset_password_get).post(services::reset_password_post),
|
||||||
)
|
)
|
||||||
|
// Recipes.
|
||||||
.route("/recipe/view/:id", get(services::view_recipe))
|
.route("/recipe/view/:id", get(services::view_recipe))
|
||||||
|
// User.
|
||||||
|
.route("/user/edit", get(services::edit_user))
|
||||||
// RON API.
|
// 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())
|
.layer(TraceLayer::new_for_http())
|
||||||
.route_layer(middleware::from_fn_with_state(
|
.route_layer(middleware::from_fn_with_state(
|
||||||
state.clone(),
|
state.clone(),
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ use chrono::prelude::*;
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
|
pub name: String,
|
||||||
pub email: String,
|
pub email: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ use tracing::{event, Level};
|
||||||
|
|
||||||
use crate::{config::Config, consts, data::db, email, model, utils, AppState};
|
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 {
|
impl axum::response::IntoResponse for db::DBError {
|
||||||
fn into_response(self) -> Response {
|
fn into_response(self) -> Response {
|
||||||
|
|
@ -722,12 +722,26 @@ pub async fn reset_password_post(
|
||||||
|
|
||||||
///// EDIT PROFILE /////
|
///// EDIT PROFILE /////
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "profile.html")]
|
||||||
|
struct ProfileTemplate {
|
||||||
|
user: Option<model::User>,
|
||||||
|
}
|
||||||
|
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
pub async fn edit_user(
|
pub async fn edit_user(
|
||||||
State(connection): State<db::Connection>,
|
State(connection): State<db::Connection>,
|
||||||
Extension(user): Extension<Option<model::User>>,
|
Extension(user): Extension<Option<model::User>>,
|
||||||
) -> Result<impl IntoResponse> {
|
) -> Response {
|
||||||
Ok("todo")
|
if user.is_some() {
|
||||||
|
ProfileTemplate { user }.into_response()
|
||||||
|
} else {
|
||||||
|
MessageTemplate {
|
||||||
|
user: None,
|
||||||
|
message: "Not logged in",
|
||||||
|
}
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///// 404 /////
|
///// 404 /////
|
||||||
|
|
|
||||||
|
|
@ -63,13 +63,13 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
pub async fn set_user_name(
|
pub async fn update_user(
|
||||||
State(connection): State<db::Connection>,
|
State(connection): State<db::Connection>,
|
||||||
Extension(user): Extension<Option<model::User>>,
|
Extension(user): Extension<Option<model::User>>,
|
||||||
ExtractRon(ron): ExtractRon<common::ron_api::SetProfileName>,
|
ExtractRon(ron): ExtractRon<common::ron_api::UpdateProfile>,
|
||||||
) -> Result<StatusCode> {
|
) -> Result<StatusCode> {
|
||||||
if let Some(user) = user {
|
if let Some(user) = user {
|
||||||
connection.set_user_name(user.id, &ron.name).await?;
|
// connection.set_user_name(user.id, &ron.name).await?;
|
||||||
} else {
|
} else {
|
||||||
return Err(ErrorResponse::from(ron_error(
|
return Err(ErrorResponse::from(ron_error(
|
||||||
StatusCode::UNAUTHORIZED,
|
StatusCode::UNAUTHORIZED,
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<form action="/ask_reset_password" method="post">
|
<form action="/ask_reset_password" method="post">
|
||||||
<label for="email_field">Your email address</label>
|
<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 }}
|
{{ message_email }}
|
||||||
|
|
||||||
<input type="submit" name="commit" value="Ask reset" />
|
<input type="submit" name="commit" value="Ask reset" />
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,14 @@
|
||||||
{% extends "base_with_list.html" %}
|
{% extends "base_with_header.html" %}
|
||||||
|
|
||||||
|
{% block main_container %}
|
||||||
|
|
||||||
|
<h2>Profile</h2>
|
||||||
|
|
||||||
|
{% match user %}
|
||||||
|
{% when Some with (user) %}
|
||||||
|
|
||||||
|
<div id="user-edit">
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<label for="title_field">Name</label>
|
<label for="title_field">Name</label>
|
||||||
<input
|
<input
|
||||||
id="name_field"
|
id="name_field"
|
||||||
|
|
@ -10,4 +18,18 @@
|
||||||
autocapitalize="none"
|
autocapitalize="none"
|
||||||
autocomplete="title"
|
autocomplete="title"
|
||||||
autofocus="autofocus" />
|
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 %}
|
{% endblock %}
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<form action="/signin" method="post">
|
<form action="/signin" method="post">
|
||||||
<label for="email_field">Email address</label>
|
<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>
|
<label for="password_field">Password</label>
|
||||||
<input id="password_field" type="password" name="password" autocomplete="current-password" />
|
<input id="password_field" type="password" name="password" autocomplete="current-password" />
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<form action="/signup" method="post">
|
<form action="/signup" method="post">
|
||||||
<label for="email_field">Your email address</label>
|
<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 }}
|
{{ message_email }}
|
||||||
|
|
||||||
<label for="password_field_1">Choose a password (minimum 8 characters)</label>
|
<label for="password_field_1">Choose a password (minimum 8 characters)</label>
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,8 @@ pub struct RemoveRecipeStep {
|
||||||
///// PROFILE /////
|
///// PROFILE /////
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct SetProfileName {
|
pub struct UpdateProfile {
|
||||||
pub name: String,
|
pub name: Option<String>,
|
||||||
|
pub email: Option<String>,
|
||||||
|
pub password: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,15 @@ common = { path = "../common" }
|
||||||
|
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
web-sys = { version = "0.3", features = [
|
web-sys = { version = "0.3", features = [
|
||||||
'console',
|
"console",
|
||||||
'Document',
|
"Document",
|
||||||
'Element',
|
"Element",
|
||||||
'HtmlElement',
|
"HtmlElement",
|
||||||
'Node',
|
"Node",
|
||||||
'Window',
|
"Window",
|
||||||
'Location',
|
"Location",
|
||||||
|
"EventTarget",
|
||||||
|
"HtmlLabelElement",
|
||||||
] }
|
] }
|
||||||
|
|
||||||
# The `console_error_panic_hook` crate provides better debugging of panics by
|
# 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.
|
# code size when deploying.
|
||||||
console_error_panic_hook = { version = "0.1", optional = true }
|
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]
|
# [dev-dependencies]
|
||||||
# wasm-bindgen-test = "0.3"
|
# wasm-bindgen-test = "0.3"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
use wasm_bindgen::prelude::*;
|
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();
|
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::<>()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,11 @@ pub fn main() -> Result<(), JsValue> {
|
||||||
let id = id.parse::<i64>().unwrap(); // TODO: remove unwrap.
|
let id = id.parse::<i64>().unwrap(); // TODO: remove unwrap.
|
||||||
console_log!("recipe edit ID: {}", id);
|
console_log!("recipe edit ID: {}", id);
|
||||||
|
|
||||||
handles::edit_recipe(&document);
|
handles::recipe_edit(&document);
|
||||||
|
}
|
||||||
|
|
||||||
|
["user", "edit"] => {
|
||||||
|
handles::user_edit(&document);
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue