Begining of a RON API to edit profile

This commit is contained in:
Greg Burri 2024-11-14 01:59:40 +01:00
parent 37f6de7a89
commit 405aa68526
11 changed files with 229 additions and 68 deletions

View file

@ -251,6 +251,15 @@ FROM [UserLoginToken] WHERE [token] = $1
.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?;
Ok(())
}
pub async fn sign_up(&self, email: &str, password: &str) -> Result<SignUpResult> {
self.sign_up_with_given_time(email, password, Utc::now())
.await

View file

@ -4,7 +4,7 @@ use axum::{
extract::{ConnectInfo, FromRef, Request, State},
middleware::{self, Next},
response::{Response, Result},
routing::get,
routing::{get, post, put},
Router,
};
use axum_extra::extract::cookie::CookieJar;
@ -22,6 +22,7 @@ mod data;
mod email;
mod hash;
mod model;
mod ron_extractor;
mod services;
mod utils;
@ -81,7 +82,6 @@ async fn main() {
get(services::sign_up_get).post(services::sign_up_post),
)
.route("/validation", get(services::sign_up_validation))
.route("/recipe/view/:id", get(services::view_recipe))
.route(
"/signin",
get(services::sign_in_get).post(services::sign_in_post),
@ -95,6 +95,9 @@ async fn main() {
"/reset_password",
get(services::reset_password_get).post(services::reset_password_post),
)
.route("/recipe/view/:id", get(services::view_recipe))
// RON API.
.route("/user/set_name", put(services::ron_api::set_user_name))
.layer(TraceLayer::new_for_http())
.route_layer(middleware::from_fn_with_state(
state.clone(),

View file

@ -0,0 +1,102 @@
use axum::{
async_trait,
body::Bytes,
extract::{FromRequest, Request},
http::{header, StatusCode},
response::{IntoResponse, Response},
};
use ron::{
de::from_bytes,
ser::{to_string_pretty, PrettyConfig},
};
use serde::{de::DeserializeOwned, Serialize};
const RON_CONTENT_TYPE: &'static str = "application/ron";
#[derive(Debug, Serialize, Clone)]
pub struct RonError {
pub error: String,
}
impl axum::response::IntoResponse for RonError {
fn into_response(self) -> Response {
let ron_as_str = to_string_pretty(&self, PrettyConfig::new()).unwrap();
ron_as_str.into_response()
}
}
impl From<RonError> for Response {
fn from(value: RonError) -> Self {
value.into_response()
}
}
pub fn ron_error(status: StatusCode, message: &str) -> impl IntoResponse {
(
status,
[(header::CONTENT_TYPE, RON_CONTENT_TYPE)],
RonError {
error: message.to_string(),
},
)
}
pub fn ron_response<T>(ron: T) -> impl IntoResponse
where
T: Serialize,
{
let ron_as_str = to_string_pretty(&ron, PrettyConfig::new()).unwrap();
([(header::CONTENT_TYPE, RON_CONTENT_TYPE)], ron_as_str)
}
fn parse_body<T>(body: Bytes) -> Result<T, RonError>
where
T: DeserializeOwned,
{
match from_bytes::<T>(&body) {
Ok(ron) => Ok(ron),
Err(error) => {
return Err(RonError {
error: format!("Ron parsing error: {}", error),
});
}
}
}
pub struct ExtractRon<T: DeserializeOwned>(pub T);
#[async_trait]
impl<S, T> FromRequest<S> for ExtractRon<T>
where
S: Send + Sync,
T: DeserializeOwned,
{
type Rejection = Response; // axum::Error::ErrorResponse;
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
match req.headers().get(header::CONTENT_TYPE) {
Some(content_type) => {
if content_type != RON_CONTENT_TYPE {
return Err(ron_error(
StatusCode::BAD_REQUEST,
&format!("Invalid content type, must be {}", RON_CONTENT_TYPE),
)
.into_response());
}
}
None => {
return Err(
ron_error(StatusCode::BAD_REQUEST, "No content type given").into_response()
)
}
}
let body = Bytes::from_request(req, state)
.await
.map_err(IntoResponse::into_response)?;
let ron = parse_body(body)?;
Ok(Self(ron))
}
}

View file

@ -4,7 +4,7 @@ use askama::Template;
use axum::{
body::Body,
debug_handler,
extract::{connect_info, ConnectInfo, Extension, Host, Path, Query, Request, State},
extract::{ConnectInfo, Extension, Host, Path, Query, Request, State},
http::{HeaderMap, StatusCode},
response::{IntoResponse, Redirect, Response, Result},
Form,
@ -14,14 +14,9 @@ use chrono::Duration;
use serde::Deserialize;
use tracing::{event, Level};
use crate::{
config::Config,
consts::{self, VALIDATION_PASSWORD_RESET_TOKEN_DURATION},
data::db,
email, model, utils, AppState,
};
use crate::{config::Config, consts, data::db, email, model, utils, AppState};
pub mod webapi;
pub mod ron_api;
impl axum::response::IntoResponse for db::DBError {
fn into_response(self) -> Response {
@ -623,8 +618,6 @@ pub async fn ask_reset_password_post(
struct ResetPasswordTemplate {
user: Option<model::User>,
reset_token: String,
password_1: String,
password_2: String,
message: String,
message_password: String,
}
@ -638,8 +631,6 @@ pub async fn reset_password_get(
Ok(ResetPasswordTemplate {
user,
reset_token: reset_token.to_string(),
password_1: String::new(),
password_2: String::new(),
message: String::new(),
message_password: String::new(),
}
@ -681,8 +672,6 @@ pub async fn reset_password_post(
Ok(ResetPasswordTemplate {
user,
reset_token: form_data.reset_token.clone(),
password_1: String::new(),
password_2: String::new(),
message_password: match error {
ResetPasswordError::PasswordsNotEqual => "Passwords don't match",
ResetPasswordError::InvalidPassword => {
@ -731,6 +720,16 @@ pub async fn reset_password_post(
}
}
///// EDIT PROFILE /////
#[debug_handler]
pub async fn edit_user(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
) -> Result<impl IntoResponse> {
Ok("todo")
}
///// 404 /////
#[debug_handler]
pub async fn not_found() -> Result<impl IntoResponse> {

View file

@ -45,3 +45,51 @@
// #[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(),
}))
}
*/

View file

@ -7,7 +7,7 @@
{% match user %}
{% when Some with (user) %}
<a class="create-recipe" href="/recipe/new" >Create a new recipe</a>
<span><a href="/edit_profile">{{ user.email }}</a> / <a href="/signout" />Sign out</a></span>
<span><a href="/user/edit">{{ user.email }}</a> / <a href="/signout" />Sign out</a></span>
{% when None %}
<span>
<a href="/signin" >Sign in</a>/<a href="/signup">Sign up</a>/<a href="/ask_reset_password">Lost password</a>

View file

@ -1,22 +1,12 @@
{% extends "base_with_list.html" %}
{% block content %}
<label for="title_field">Title</label>
<label for="title_field">Name</label>
<input
id="title_field"
id="name_field"
type="text"
name="title"
value="{{ current_recipe.title }}"
autocapitalize="none"
autocomplete="title"
autofocus="autofocus" />
<label for="description_field">Description</label>
<input
id="title_field"
type="text"
name="title"
value="{{ current_recipe.description }}"
name="name"
value="{{ user.name }}"
autocapitalize="none"
autocomplete="title"
autofocus="autofocus" />