From 5f02fb265ab9257805131b9a235b81aedb0e6af7 Mon Sep 17 00:00:00 2001 From: Joe Ardent Date: Wed, 14 Jun 2023 22:05:50 -0700 Subject: [PATCH 1/4] move macro import to lib.rs --- src/lib.rs | 37 +++++++++++++++++++++++++++++++++++++ src/templates.rs | 36 +----------------------------------- src/watches/templates.rs | 3 +-- 3 files changed, 39 insertions(+), 37 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 28f9dc9..a8a2157 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,6 +18,7 @@ mod util; mod watches; // things we want in the crate namespace +use optional_optional_user::OptionalOptionalUser; use templates::*; use users::User; use watches::{templates::*, ShowKind, Watch}; @@ -62,3 +63,39 @@ pub async fn app(db_pool: sqlx::SqlitePool, session_secret: &[u8]) -> axum::Rout .layer(session_layer) .with_state(db_pool) } + +//-************************************************************************ +// tests for the proc macro for optional user +//-************************************************************************ +#[cfg(test)] +mod test { + use super::{CreateUserSuccess, MainPage, OptionalOptionalUser, User}; + + #[test] + fn main_page_has_optional_user() { + assert!(MainPage::default().has_optional_user()); + } + + #[test] + fn signup_success_has_no_optional_user() { + assert!(!CreateUserSuccess::default().has_optional_user()); + } + + #[test] + fn user_is_not_optional() { + #[derive(Default, OptionalOptionalUser)] + struct TestThing { + user: User, + } + assert!(!TestThing::default().has_optional_user()); + } + + #[test] + fn user_is_not_user() { + #[derive(Default, OptionalOptionalUser)] + struct TestThing { + user: Option, + } + assert!(!TestThing::default().has_optional_user()); + } +} diff --git a/src/templates.rs b/src/templates.rs index 6fc1b88..664533f 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -1,8 +1,7 @@ use askama::Template; -use optional_optional_user::OptionalOptionalUser; use serde::{Deserialize, Serialize}; -use crate::User; +use crate::{OptionalOptionalUser, User}; #[derive(Debug, Default, Template, Deserialize, Serialize, PartialEq, Eq, OptionalOptionalUser)] #[template(path = "signup.html")] @@ -47,36 +46,3 @@ pub struct LogoutPost; pub struct MainPage { pub user: Option, } - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn main_page_has_optional_user() { - assert!(MainPage::default().has_optional_user()); - } - - #[test] - fn signup_success_has_no_optional_user() { - assert!(!CreateUserSuccess::default().has_optional_user()); - } - - #[test] - fn user_is_not_optional() { - #[derive(Default, OptionalOptionalUser)] - struct TestThing { - user: User, - } - assert!(!TestThing::default().has_optional_user()); - } - - #[test] - fn user_is_not_user() { - #[derive(Default, OptionalOptionalUser)] - struct TestThing { - user: Option, - } - assert!(!TestThing::default().has_optional_user()); - } -} diff --git a/src/watches/templates.rs b/src/watches/templates.rs index 3e913f2..4a8d6c6 100644 --- a/src/watches/templates.rs +++ b/src/watches/templates.rs @@ -1,8 +1,7 @@ use askama::Template; -use optional_optional_user::OptionalOptionalUser; use serde::{Deserialize, Serialize}; -use crate::{User, Watch}; +use crate::{OptionalOptionalUser, User, Watch}; #[derive(Debug, Default, Template, Deserialize, Serialize, PartialEq, Eq, OptionalOptionalUser)] #[template(path = "get_watches.html")] From 1540153b679f918d4db6d7069c79df17dd4ede33 Mon Sep 17 00:00:00 2001 From: Joe Ardent Date: Wed, 14 Jun 2023 22:11:43 -0700 Subject: [PATCH 2/4] add search results page --- src/watches/handlers.rs | 17 +++++++++++------ src/watches/templates.rs | 6 +++++- templates/get_search_watches.html | 27 +++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/watches/handlers.rs b/src/watches/handlers.rs index 67e55f5..245b761 100644 --- a/src/watches/handlers.rs +++ b/src/watches/handlers.rs @@ -1,5 +1,3 @@ -use std::path::Path; - use axum::{ extract::{Form, Query, State}, http::StatusCode, @@ -9,6 +7,7 @@ use serde::Deserialize; use sqlx::{query_as, SqlitePool}; use uuid::Uuid; +use super::templates::GetSearchWatches; use crate::{AuthContext, GetWatches, ShowKind, User, Watch}; //-************************************************************************ @@ -79,15 +78,21 @@ pub async fn get_watches(auth: AuthContext, State(pool): State) -> i } pub async fn get_search_watch( - _auth: AuthContext, - State(pool): State, + auth: AuthContext, + State(_pool): State, search: Option>, -) { +) -> impl IntoResponse { let search = match search { Some(Query(SimpleSearchQuery { search })) => search, None => "".to_owned(), }; - let search = search.trim(); + let search = search.trim().to_string(); + let user = auth.current_user; + GetSearchWatches { + watches: vec![], + user, + search, + } } pub async fn post_search_watch() {} diff --git a/src/watches/templates.rs b/src/watches/templates.rs index 4a8d6c6..b60e0d3 100644 --- a/src/watches/templates.rs +++ b/src/watches/templates.rs @@ -12,4 +12,8 @@ pub struct GetWatches { #[derive(Debug, Default, Template, Deserialize, Serialize, PartialEq, Eq, OptionalOptionalUser)] #[template(path = "get_search_watches.html")] -pub struct GetSearchWatches {} +pub struct GetSearchWatches { + pub watches: Vec, + pub user: Option, + pub search: String, +} diff --git a/templates/get_search_watches.html b/templates/get_search_watches.html index e69de29..6835865 100644 --- a/templates/get_search_watches.html +++ b/templates/get_search_watches.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} +{% import "macros.html" as m %} + +{% block title %}Welcome to Witch Watch, Bish{% endblock %} + +{% block content %} + +

Whatcha Watchin?

+ +
{{self.search}}
+ +
+
    + {% for watch in watches %} +
  • {{watch.title}} -- {% call m::get_or_default(watch.release_date, "when??") %}:
  • + {% endfor %} +
+
+

+

+ +
+ +
+

+ +{% endblock %} From 48a123353409e9204fc48e0be80a387208aa6d4c Mon Sep 17 00:00:00 2001 From: Joe Ardent Date: Thu, 15 Jun 2023 13:13:12 -0700 Subject: [PATCH 3/4] Use graphemes for input length checks instead of number of bytes. --- src/signup.rs | 34 +++++++++++++++++++++++++++++++--- src/util.rs | 5 ++++- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/signup.rs b/src/signup.rs index 171a643..a2e73f0 100644 --- a/src/signup.rs +++ b/src/signup.rs @@ -81,10 +81,9 @@ pub async fn post_create_user( if password != verify { return Err(CreateUserErrorKind::PasswordMismatch.into()); } - - let password = password.trim(); + let pwlen = password.graphemes(true).size_hint().1.unwrap_or(0); let password = password.as_bytes(); - if !(4..=50).contains(&password.len()) { + if !(4..=50).contains(&pwlen) { return Err(CreateUserErrorKind::BadPassword.into()); } @@ -352,6 +351,35 @@ mod test { assert_eq!(&expected, body); } + #[tokio::test] + async fn multibyte_password_too_short() { + let pw = "🤡"; + // min length is 4 + assert_eq!(pw.len(), 4); + + let pool = get_db_pool().await; + let server = server_with_pool(&pool).await; + let form = + format!("username=test_user&displayname=Test+User&password={pw}&pw_verify={pw}"); + let body = massage(&form); + + let resp = server + .post("/signup") + // failure to sign up is not failure to submit the request + .expect_success() + .bytes(body) + .content_type(FORM_CONTENT_TYPE) + .await; + + // no user in db + let user = User::try_get("test_user", &pool).await; + assert!(user.is_err()); + + let body = std::str::from_utf8(resp.bytes()).unwrap(); + let expected = CreateUserError(CreateUserErrorKind::BadPassword).to_string(); + assert_eq!(&expected, body); + } + #[tokio::test] async fn username_short() { let pool = get_db_pool().await; diff --git a/src/util.rs b/src/util.rs index 466660c..022c0f8 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,5 +1,7 @@ use std::{error::Error, ops::Range}; +use unicode_segmentation::UnicodeSegmentation; + pub fn validate_optional_length( opt: &Option, len_range: Range, @@ -7,7 +9,8 @@ pub fn validate_optional_length( ) -> Result, E> { if let Some(opt) = opt { let opt = opt.trim(); - if !len_range.contains(&opt.len()) { + let len = opt.graphemes(true).size_hint().1.unwrap(); + if !len_range.contains(&len) { Err(err) } else { Ok(Some(opt.to_string())) From 7c42b6316a4de591d93eca94ca3d189b4e56b2e1 Mon Sep 17 00:00:00 2001 From: Joe Ardent Date: Thu, 15 Jun 2023 16:19:07 -0700 Subject: [PATCH 4/4] flesh out get_watch() --- src/lib.rs | 4 +++ src/watches/handlers.rs | 73 ++++++++++++++++++++++++++++++++++------ src/watches/templates.rs | 7 ++++ templates/get_watch.html | 33 ++++++++++++++++++ 4 files changed, 106 insertions(+), 11 deletions(-) create mode 100644 templates/get_watch.html diff --git a/src/lib.rs b/src/lib.rs index a8a2157..c07ca09 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,6 +23,8 @@ use templates::*; use users::User; use watches::{templates::*, ShowKind, Watch}; +use crate::watches::handlers::get_watch; + type AuthContext = axum_login::extractors::AuthContext>; @@ -47,6 +49,8 @@ pub async fn app(db_pool: sqlx::SqlitePool, session_secret: &[u8]) -> axum::Rout .route("/login", get(get_login).post(post_login)) .route("/logout", get(get_logout).post(post_logout)) .route("/watches", get(get_watches)) + .route("/watch", get(get_watch)) + .route("/watch/:id", get(get_watch)) .route("/search", get(get_search_watch).post(post_search_watch)) .route( "/add", diff --git a/src/watches/handlers.rs b/src/watches/handlers.rs index 245b761..d220ee5 100644 --- a/src/watches/handlers.rs +++ b/src/watches/handlers.rs @@ -1,5 +1,5 @@ use axum::{ - extract::{Form, Query, State}, + extract::{Form, Path, Query, State}, http::StatusCode, response::{IntoResponse, Response}, }; @@ -7,7 +7,7 @@ use serde::Deserialize; use sqlx::{query_as, SqlitePool}; use uuid::Uuid; -use super::templates::GetSearchWatches; +use super::templates::{GetSearchWatches, GetWatch}; use crate::{AuthContext, GetWatches, ShowKind, User, Watch}; //-************************************************************************ @@ -17,6 +17,8 @@ use crate::{AuthContext, GetWatches, ShowKind, User, Watch}; const GET_WATCHES_QUERY: &str = "select * from watches left join witch_watch on $1 = witch_watch.witch and watches.id = witch_watch.watch"; +const GET_WATCH_QUERY: &str = "select * from watches where id = $1"; + //-************************************************************************ // Error types for Watch creation //-************************************************************************ @@ -40,11 +42,34 @@ impl IntoResponse for WatchAddError { } } +//-************************************************************************ +// Types for receiving arguments from search queries +//-************************************************************************ + #[derive(Debug, Default, Clone, Deserialize)] pub struct SimpleSearchQuery { search: String, } +#[derive(Debug, Default, Clone, Deserialize)] +pub struct FullSearchQuery { + pub title: Option, + pub kind: Option, + pub year: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub enum SearchQuery { + Full(FullSearchQuery), + Simple(SimpleSearchQuery), +} + +impl Default for SearchQuery { + fn default() -> Self { + SearchQuery::Simple(SimpleSearchQuery::default()) + } +} + //-************************************************************************ // handlers //-************************************************************************ @@ -56,12 +81,34 @@ pub async fn put_add_watch() {} pub async fn post_add_watch() {} /// A single Watch -pub async fn get_watch() {} +pub async fn get_watch( + auth: AuthContext, + watch: Option>, + State(pool): State, +) -> impl IntoResponse { + let id = if let Some(Path(id)) = watch { + id + } else { + "".to_string() + }; + let id = id.trim(); + + let watch: Option = query_as(GET_WATCH_QUERY) + .bind(id) + .fetch_one(&pool) + .await + .ok(); + + GetWatch { + watch, + user: auth.current_user, + } +} /// everything the user has saved pub async fn get_watches(auth: AuthContext, State(pool): State) -> impl IntoResponse { - let user = &auth.current_user; - let watches: Vec = if user.is_some() { + let user = auth.current_user; + let watches: Vec = if (user).is_some() { query_as(GET_WATCHES_QUERY) .bind(user.as_ref().unwrap().id) .fetch_all(&pool) @@ -71,23 +118,27 @@ pub async fn get_watches(auth: AuthContext, State(pool): State) -> i vec![] }; - GetWatches { - watches, - user: user.clone(), - } + GetWatches { watches, user } } pub async fn get_search_watch( auth: AuthContext, State(_pool): State, - search: Option>, + search: Option>, ) -> impl IntoResponse { + use SearchQuery::*; let search = match search { - Some(Query(SimpleSearchQuery { search })) => search, + Some(Query(Simple(SimpleSearchQuery { search }))) => search, + Some(Query(Full(q))) => { + // obviously this is dumb + format!("{q:?}") + } None => "".to_owned(), }; let search = search.trim().to_string(); + let user = auth.current_user; + GetSearchWatches { watches: vec![], user, diff --git a/src/watches/templates.rs b/src/watches/templates.rs index b60e0d3..1f4525d 100644 --- a/src/watches/templates.rs +++ b/src/watches/templates.rs @@ -17,3 +17,10 @@ pub struct GetSearchWatches { pub user: Option, pub search: String, } + +#[derive(Debug, Default, Template, Deserialize, Serialize, PartialEq, Eq, OptionalOptionalUser)] +#[template(path = "get_watch.html")] +pub struct GetWatch { + pub watch: Option, + pub user: Option, +} diff --git a/templates/get_watch.html b/templates/get_watch.html new file mode 100644 index 0000000..4af344f --- /dev/null +++ b/templates/get_watch.html @@ -0,0 +1,33 @@ +{% extends "base.html" %} +{% import "macros.html" as m %} + +{% block title %}Welcome to Witch Watch, Bish{% endblock %} + +{% block content %} + +

Whatcha Watchin?

+ +{% match watch %} + +{% when Some with (watch) %} + +
+ {{watch.title}} -- {% call m::get_or_default(watch.release_date, "when??") %} +
+ +{% else %} + +
Sorry, maybe you meant to search for something?
+ +{% endmatch %} + + +

+

+ +
+ +
+

+ +{% endblock %}