Add basic user auth
This commit is contained in:
@@ -27,7 +27,9 @@ pub async fn get(
|
||||
let content = fs::read_to_string(content_path).unwrap_or_else(|_| "No content".to_string());
|
||||
Ok(layout.with_subtitle(&title).render(html! {
|
||||
article {
|
||||
h2 class="title" { a href=(entry.url) { (title) } }
|
||||
header {
|
||||
h2 class="title" { a href=(entry.url) { (title) } }
|
||||
}
|
||||
div {
|
||||
span class="published" {
|
||||
strong { "Published: " }
|
||||
|
||||
@@ -164,7 +164,7 @@ pub async fn stream(
|
||||
let mut crawls = crawls.lock().await;
|
||||
crawls.remove(&id.as_uuid())
|
||||
}
|
||||
.ok_or_else(|| Error::NotFound("feed stream", id.as_uuid()))?;
|
||||
.ok_or_else(|| Error::NotFoundUuid("feed stream", id.as_uuid()))?;
|
||||
|
||||
let stream = BroadcastStream::new(receiver);
|
||||
let feed_id = format!("feed-{}", id);
|
||||
|
||||
@@ -14,7 +14,7 @@ pub async fn get(State(pool): State<PgPool>, layout: Layout) -> Result<Response>
|
||||
let options = GetFeedsOptions::default();
|
||||
let feeds = Feed::get_all(&pool, &options).await?;
|
||||
Ok(layout.with_subtitle("feeds").render(html! {
|
||||
h2 { "Feeds" }
|
||||
header { h2 { "Feeds" } }
|
||||
div class="feeds" {
|
||||
ul id="feeds" {
|
||||
(feed_list(feeds, &options))
|
||||
|
||||
@@ -59,7 +59,7 @@ pub async fn stream(
|
||||
let mut imports = imports.lock().await;
|
||||
imports.remove(&id.as_uuid())
|
||||
}
|
||||
.ok_or_else(|| Error::NotFound("import stream", id.as_uuid()))?;
|
||||
.ok_or_else(|| Error::NotFoundUuid("import stream", id.as_uuid()))?;
|
||||
|
||||
let stream = BroadcastStream::new(receiver);
|
||||
let stream = stream.map(move |msg| match msg {
|
||||
|
||||
69
src/handlers/login.rs
Normal file
69
src/handlers/login.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use axum::response::{IntoResponse, Redirect, Response};
|
||||
use axum::{extract::State, Form};
|
||||
use maud::html;
|
||||
use serde::Deserialize;
|
||||
use serde_with::serde_as;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::auth::verify_password;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::partials::login_form::{login_form, LoginFormProps};
|
||||
use crate::{
|
||||
models::user::{AuthContext, User},
|
||||
partials::layout::Layout,
|
||||
};
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Deserialize)]
|
||||
pub struct Login {
|
||||
email: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
pub async fn get(layout: Layout) -> Result<Response> {
|
||||
Ok(layout.with_subtitle("login").render(html! {
|
||||
header {
|
||||
h2 { "Login" }
|
||||
}
|
||||
(login_form(LoginFormProps::default()))
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn post(
|
||||
State(pool): State<PgPool>,
|
||||
mut auth: AuthContext,
|
||||
Form(login): Form<Login>,
|
||||
) -> Result<Response> {
|
||||
let user: User = match User::get_by_email(&pool, login.email.clone()).await {
|
||||
Ok(user) => user,
|
||||
Err(err) => {
|
||||
if let Error::NotFoundString(_, _) = err {
|
||||
// Error::BadRequest("invalid email or password")
|
||||
return Ok(login_form(LoginFormProps {
|
||||
email: Some(login.email),
|
||||
general_error: Some("invalid email or password".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.into_response());
|
||||
} else {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
if verify_password(login.password, user.password_hash.clone())
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
// return Err(Error::BadRequest("invalid email or password"));
|
||||
return Ok(login_form(LoginFormProps {
|
||||
email: Some(login.email),
|
||||
general_error: Some("invalid email or password".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.into_response());
|
||||
}
|
||||
auth.login(&user)
|
||||
.await
|
||||
.map_err(|_| Error::InternalServerError)?;
|
||||
Ok(Redirect::to("/").into_response())
|
||||
}
|
||||
8
src/handlers/logout.rs
Normal file
8
src/handlers/logout.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use axum::response::Redirect;
|
||||
|
||||
use crate::models::user::AuthContext;
|
||||
|
||||
pub async fn get(mut auth: AuthContext) -> Redirect {
|
||||
auth.logout().await;
|
||||
Redirect::to("/")
|
||||
}
|
||||
@@ -6,3 +6,6 @@ pub mod import;
|
||||
pub mod feed;
|
||||
pub mod feeds;
|
||||
pub mod log;
|
||||
pub mod login;
|
||||
pub mod logout;
|
||||
pub mod signup;
|
||||
|
||||
111
src/handlers/signup.rs
Normal file
111
src/handlers/signup.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
use axum::response::{IntoResponse, Redirect, Response};
|
||||
use axum::{extract::State, Form};
|
||||
use maud::html;
|
||||
use serde::Deserialize;
|
||||
use serde_with::{serde_as, NoneAsEmptyString};
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::models::user::{AuthContext, CreateUser, User};
|
||||
use crate::partials::layout::Layout;
|
||||
use crate::partials::signup_form::{signup_form, SignupFormProps};
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Signup {
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
pub password_confirmation: String,
|
||||
#[serde_as(as = "NoneAsEmptyString")]
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn get(layout: Layout) -> Result<Response> {
|
||||
Ok(layout.with_subtitle("signup").render(html! {
|
||||
header {
|
||||
h2 { "Signup" }
|
||||
}
|
||||
(signup_form(SignupFormProps::default()))
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn post(
|
||||
State(pool): State<PgPool>,
|
||||
mut auth: AuthContext,
|
||||
Form(signup): Form<Signup>,
|
||||
) -> Result<Response> {
|
||||
if signup.password != signup.password_confirmation {
|
||||
// return Err(Error::BadRequest("passwords do not match"));
|
||||
return Ok(signup_form(SignupFormProps {
|
||||
email: Some(signup.email),
|
||||
name: signup.name,
|
||||
password_error: Some("passwords do not match".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.into_response());
|
||||
}
|
||||
let user = match User::create(
|
||||
&pool,
|
||||
CreateUser {
|
||||
email: signup.email.clone(),
|
||||
password: signup.password.clone(),
|
||||
name: signup.name.clone(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(user) => user,
|
||||
Err(err) => {
|
||||
if let Error::InvalidEntity(validation_errors) = err {
|
||||
let field_errors = validation_errors.field_errors();
|
||||
dbg!(&validation_errors);
|
||||
dbg!(&field_errors);
|
||||
return Ok(signup_form(SignupFormProps {
|
||||
email: Some(signup.email),
|
||||
name: signup.name,
|
||||
email_error: field_errors.get("email").map(|&errors| {
|
||||
errors
|
||||
.iter()
|
||||
.filter_map(|error| error.message.clone().map(|m| m.to_string()))
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
}),
|
||||
name_error: field_errors.get("name").map(|&errors| {
|
||||
errors
|
||||
.iter()
|
||||
.filter_map(|error| error.message.clone().map(|m| m.to_string()))
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
}),
|
||||
password_error: field_errors.get("password").map(|&errors| {
|
||||
errors
|
||||
.iter()
|
||||
.filter_map(|error| error.message.clone().map(|m| m.to_string()))
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
}),
|
||||
..Default::default()
|
||||
})
|
||||
.into_response());
|
||||
}
|
||||
if let Error::Sqlx(sqlx::error::Error::Database(db_error)) = &err {
|
||||
if let Some(constraint) = db_error.constraint() {
|
||||
if constraint == "users_email_idx" {
|
||||
return Ok(signup_form(SignupFormProps {
|
||||
email: Some(signup.email),
|
||||
name: signup.name,
|
||||
email_error: Some("email already exists".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.into_response());
|
||||
}
|
||||
}
|
||||
}
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
auth.login(&user)
|
||||
.await
|
||||
.map_err(|_| Error::InternalServerError)?;
|
||||
Ok(Redirect::to("/").into_response())
|
||||
}
|
||||
Reference in New Issue
Block a user