Skip to content

Commit

Permalink
feat: improve blog auth flow
Browse files Browse the repository at this point in the history
  • Loading branch information
jdevries3133 committed Aug 26, 2024
1 parent 3c65025 commit ff57abf
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 14 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ reqwest = { version = "0.11.23", features = ["rustls-tls", "json"], default-feat
rustls = "0.21.7"
serde = { version = "1.0.171", features = ["derive"] }
serde_json = { version = "1.0", features = ["raw_value"] }
serde_urlencoded = "0.7.1"
sha2 = "0.10.7"
sqlx = { version = "0.8.1", features = ["json", "postgres", "uuid", "chrono", "runtime-async-std-rustls" ] }
tokio = { version = "1.29.1", features = ["full"] }
Expand Down
44 changes: 43 additions & 1 deletion src/auth/anon.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,56 @@
use super::{pw::hash_new, register::create_user};
use crate::{config, htmx, preferences::save_user_preference, prelude::*};
use axum::extract::Query;
use chrono::Days;
use regex::Regex;
use serde_urlencoded::to_string;
use uuid::Uuid;

/// This is part of the `init-anon` route. After initting an anonymous user,
/// we will always redirect the user somewhere. By constructing the
/// [Self::CustomNextRoute]
#[derive(Debug)]
pub enum InitAnonNextRoute {
AxumTemplate,
DefaultNextRoute,
CustomNextRoute(Box<Route>),
}

impl InitAnonNextRoute {
pub fn as_string(&self) -> String {
match self {
Self::CustomNextRoute(route) => {
to_string([("next", &route.as_string())])
.map(|query| format!("/authentication/init-anon?{query}"))
.unwrap_or_else(|err| {
eprintln!(
"Route {route:?} cannot be URL encoded ({err})"
);
Self::DefaultNextRoute.as_string()
})
}
Self::AxumTemplate => "/authentication/init-anon".into(),
Self::DefaultNextRoute => {
"/authentication/init-anon?next=%2fhome".into()
}
}
}
}

#[derive(Deserialize)]
pub struct AnonForm {
timezone: Tz,
}

#[derive(Deserialize)]
pub struct AnonParams {
next: Option<String>,
}

pub async fn init_anon(
State(AppState { db }): State<AppState>,
headers: HeaderMap,
Query(AnonParams { next }): Query<AnonParams>,
Form(AnonForm { timezone }): Form<AnonForm>,
) -> Result<impl IntoResponse, ServerError> {
let session = match Session::from_headers(&headers) {
Expand Down Expand Up @@ -51,7 +90,10 @@ pub async fn init_anon(
let response_headers = HeaderMap::new();
let headers = session.update_headers(response_headers);

Ok(htmx::redirect_2(headers, &Route::UserHome.as_string()))
Ok(htmx::redirect_2(
headers,
&next.unwrap_or(Route::UserHome.as_string()),
))
}

pub fn is_anon(username: &str) -> bool {
Expand Down
4 changes: 2 additions & 2 deletions src/auth/login.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
use super::authenticate::authenticate;
use crate::{config, htmx, prelude::*};
use crate::{auth::InitAnonNextRoute, config, htmx, prelude::*};
use axum::http::{HeaderValue, StatusCode};

pub struct LoginForm;
impl Component for LoginForm {
fn render(&self) -> String {
let login_route = Route::Login;
let password_reset = Route::PasswordReset;
let init_anon = Route::InitAnon;
let init_anon = Route::InitAnon(InitAnonNextRoute::DefaultNextRoute);
format!(
r##"
<div id="form-container">
Expand Down
2 changes: 1 addition & 1 deletion src/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ mod register;
mod reset;
mod session;

pub use anon::{init_anon, is_anon};
pub use anon::{init_anon, is_anon, InitAnonNextRoute};
pub use authenticate::authenticate;
pub use login::{get_login_form, handle_login, logout};
pub use register::{get_registration_form, handle_registration, RegisterForm};
Expand Down
82 changes: 76 additions & 6 deletions src/blog/post_page.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use super::post::{Comment, Post};
use crate::{
auth::{is_anon, InitAnonNextRoute},
components::{BackIcon, Brand, Span},
config::ADMINISTRATOR_USER_IDS,
prelude::*,
Expand Down Expand Up @@ -38,7 +39,11 @@ struct CommentUI<'a> {
}
impl Component for CommentUI<'_> {
fn render(&self) -> String {
let username = clean(&self.comment.username);
let username = if is_anon(&self.comment.username) {
"anon".to_string()
} else {
clean(&self.comment.username)
};
let body = clean(&self.comment.body);
let delete_button = if self.can_delete {
let delete_route = Route::DeleteComment(Some(self.comment.id));
Expand Down Expand Up @@ -86,6 +91,7 @@ struct CreateCommentForm {
}
impl Component for CreateCommentForm {
fn render(&self) -> String {
let home = Route::UserHome;
let submit = Route::BlogCommentSubmission;
let post_id = self.post_id;
format!(
Expand Down Expand Up @@ -113,12 +119,69 @@ impl Component for CreateCommentForm {
class="self-start text-xs p-1 my-2 bg-emerald-100
hover:bg-emerald-200 rounded-full text-black"
>Submit</button>
<a href="{home}">
<button
class="
bg-gradient-to-tr
from-blue-700
to-indigo-700
from-blue-100
to-indigo-200
p-2
rounded
shadow-md
hover:shadow-sm
font-extrabold
text-white
my-2
"
>Try Bean Count!</button>
</a>
</form>
"#
)
}
}

struct NoAuthCommentActions {
post_id: i32,
}
impl Component for NoAuthCommentActions {
fn render(&self) -> String {
let init_anon = Route::InitAnon(InitAnonNextRoute::CustomNextRoute(
Box::new(Route::BlogPost(Some(self.post_id))),
));
let login = Route::Login;
format!(
r#"
<div class="flex gap-2">
<form method="POST" action="{init_anon}">
<input type="hidden" value="" name="timezone" id="timezone" />
<button
class="self-start text-xs p-1 bg-emerald-100
hover:bg-emerald-200 rounded-full text-black"
>Create an account to comment</button>
</form>
<a class="text-xs" href="{login}">
<button
class="self-start text-xs p-1 rounded-full border-2
jorder-emerald-200"
>Or, login if you have an account
</button>
</a>
</div>
<script>
(() => {{
for (const el of document.querySelectorAll("[name='timezone'")) {{
el.value = Intl.DateTimeFormat().resolvedOptions().timeZone;
}}
}})();
</script>
"#
)
}
}

struct PostPage<'a> {
post: &'a Post,
comments: &'a [Comment],
Expand All @@ -128,10 +191,17 @@ impl Component for PostPage<'_> {
fn render(&self) -> String {
let post = self.post.render();
let brand = Brand {}.render();
let comment_form = CreateCommentForm {
post_id: self.post.id,
}
.render();
let action = if self.user_id.is_some() {
CreateCommentForm {
post_id: self.post.id,
}
.render()
} else {
NoAuthCommentActions {
post_id: self.post.id,
}
.render()
};
let comments =
self.comments
.iter()
Expand All @@ -155,7 +225,7 @@ impl Component for PostPage<'_> {
{post}
</div>
<div>
{comment_form}
{action}
</div>
<div>
{comments}
Expand Down
3 changes: 2 additions & 1 deletion src/components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,8 @@ pub struct Home {}
impl Component for Home {
fn render(&self) -> String {
let login_route = Route::Login;
let init_anon = Route::InitAnon;
let init_anon =
Route::InitAnon(auth::InitAnonNextRoute::DefaultNextRoute);
let footer = Footer {}.render();
let brand = Brand {}.render();
format!(
Expand Down
10 changes: 7 additions & 3 deletions src/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use axum::{
/// For each route, the parameters are inside an Option<T>. If no parameters
/// are provided, we'll construct the route with the `:id` template in it
/// for the Axum router.
#[derive(Debug)]
pub enum Route {
About,
AddFoodToToday(Option<i32>),
Expand All @@ -49,7 +50,7 @@ pub enum Route {
GotoStripePortal,
HandleChat,
Htmx,
InitAnon,
InitAnon(auth::InitAnonNextRoute),
ListFood,
Login,
Logout,
Expand Down Expand Up @@ -130,7 +131,7 @@ impl Route {
Self::GotoStripePortal => "/stripe-portal".into(),
Self::HandleChat => "/chat".into(),
Self::Htmx => "/generated/htmx-2.0.2".into(),
Self::InitAnon => "/authentication/init-anon".into(),
Self::InitAnon(next_route) => next_route.as_string(),
Self::ListFood => "/list-food".into(),
Self::Login => "/authentication/login".into(),
Self::Logout => "/authentication/logout".into(),
Expand Down Expand Up @@ -274,7 +275,10 @@ fn get_public_routes() -> Router<models::AppState> {
get(controllers::get_tiny_icon),
)
.route(&Route::Htmx.as_string(), get(controllers::get_htmx_js))
.route(&Route::InitAnon.as_string(), post(auth::init_anon))
.route(
&Route::InitAnon(auth::InitAnonNextRoute::AxumTemplate).as_string(),
post(auth::init_anon),
)
.route(&Route::Login.as_string(), get(auth::get_login_form))
.route(&Route::Login.as_string(), post(auth::handle_login))
.route(&Route::Logout.as_string(), get(auth::logout))
Expand Down

0 comments on commit ff57abf

Please sign in to comment.