diff --git a/migrations/20230426221940_init.up.sql b/migrations/20230426221940_init.up.sql index 7d53573..f94e598 100644 --- a/migrations/20230426221940_init.up.sql +++ b/migrations/20230426221940_init.up.sql @@ -20,11 +20,13 @@ create table if not exists watches ( id blob not null primary key, typ int not null, -- enum for movie or tv show or whatev title text not null, - imdb text, -- possible url for imdb or other metadata-esque site to show the user - runtime int, + metadata_url text, -- possible url for imdb or other metadata-esque site to show the user + length int, release_date int, + added_by blob not null, -- ID of the user that added it created_at int not null default (unixepoch()), - last_updated int not null default (unixepoch()) + last_updated int not null default (unixepoch()), + foreign key (added_by) references witches (id) ); -- table of what people want to watch @@ -68,6 +70,6 @@ create table if not exists watch_notes ( -- indices, not needed for covens create index if not exists witch_dex on witches ( username, email ); -create index if not exists watch_dex on watches ( title, runtime, release_date ); +create index if not exists watch_dex on watches ( title, length, release_date, added_by ); create index if not exists ww_dex on witch_watch ( witch, watch, public ); create index if not exists note_dex on watch_notes ( witch, watch, public ); diff --git a/src/db.rs b/src/db.rs index a6e374c..7e9d39e 100644 --- a/src/db.rs +++ b/src/db.rs @@ -20,7 +20,7 @@ const MIN_CONNS: u32 = 5; const TIMEOUT: u64 = 11; const SESSION_TTL: Duration = Duration::from_secs((365.2422 * 24. * 3600.0) as u64); -pub async fn get_pool() -> SqlitePool { +pub async fn get_db_pool() -> SqlitePool { let db_filename = { std::env::var("DATABASE_FILE").unwrap_or_else(|_| { #[cfg(not(test))] @@ -115,7 +115,7 @@ mod tests { #[tokio::test] async fn it_migrates_the_db() { - let db = super::get_pool().await; + let db = super::get_db_pool().await; let r = sqlx::query("select count(*) from witches") .fetch_one(&db) .await; @@ -130,6 +130,7 @@ mod tests { //-************************************************************************ // Session store sub-module, not a public lib. //-************************************************************************ +#[allow(dead_code)] mod session_store { use async_session::{async_trait, chrono::Utc, log, serde_json, Result, Session}; use sqlx::{pool::PoolConnection, Sqlite}; diff --git a/src/generic_handlers.rs b/src/generic_handlers.rs index 09e0cd1..cbefcb1 100644 --- a/src/generic_handlers.rs +++ b/src/generic_handlers.rs @@ -1,6 +1,6 @@ use axum::response::{IntoResponse, Redirect}; -use crate::{templates::Index, AuthContext}; +use crate::{AuthContext, MainPage}; pub async fn handle_slash_redir() -> impl IntoResponse { Redirect::to("/") @@ -13,7 +13,7 @@ pub async fn handle_slash(auth: AuthContext) -> impl IntoResponse { } else { tracing::debug!("Not logged in."); } - Index { + MainPage { user: auth.current_user, } } @@ -26,7 +26,7 @@ mod test { #[tokio::test] async fn slash_is_ok() { - let pool = db::get_pool().await; + let pool = db::get_db_pool().await; let secret = [0u8; 64]; let app = crate::app(pool.clone(), &secret).await.into_make_service(); @@ -37,7 +37,7 @@ mod test { #[tokio::test] async fn not_found_is_303() { - let pool = db::get_pool().await; + let pool = db::get_db_pool().await; let secret = [0u8; 64]; let app = crate::app(pool, &secret).await.into_make_service(); diff --git a/src/lib.rs b/src/lib.rs index 6638c22..482ba6a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,38 +1,58 @@ #[macro_use] extern crate justerror; -use axum::{middleware, routing::get, Router}; -use axum_login::SqliteStore; -use generic_handlers::{handle_slash, handle_slash_redir}; -use login::{get_login, get_logout, post_login, post_logout}; -use signup::{get_create_user, handle_signup_success, post_create_user}; -use sqlx::SqlitePool; -pub use users::User; -use uuid::Uuid; - -pub mod db; -pub mod generic_handlers; -pub mod login; -pub mod signup; -pub(crate) mod templates; -pub mod users; -pub(crate) mod util; - #[cfg(test)] pub mod test_utils; -pub type AuthContext = axum_login::extractors::AuthContext>; +/// This is used in the bin crate and in tests. +pub use db::get_db_pool; -pub async fn app(db_pool: SqlitePool, secret: &[u8]) -> Router { +// everything else is private to the crate +mod db; +mod generic_handlers; +mod login; +mod signup; +mod templates; +mod users; +mod util; +mod watches; + +// things we want in the crate namespace +use templates::*; +use users::User; +use watches::{templates::*, ShowKind, Watch}; + +type AuthContext = + axum_login::extractors::AuthContext>; + +/// Returns the router to be used as a service or test object, you do you. +pub async fn app(db_pool: sqlx::SqlitePool, secret: &[u8]) -> axum::Router { + use axum::{middleware, routing::get}; let session_layer = db::session_layer(db_pool.clone(), secret).await; let auth_layer = db::auth_layer(db_pool.clone(), secret).await; - Router::new() + // don't bother bringing handlers into the whole crate namespace + use generic_handlers::{handle_slash, handle_slash_redir}; + use login::{get_login, get_logout, post_login, post_logout}; + use signup::{get_create_user, handle_signup_success, post_create_user}; + use watches::handlers::{ + get_search_watch, get_watches, post_add_watch, post_search_watch, put_add_watch, + }; + + axum::Router::new() .route("/", get(handle_slash).post(handle_slash)) .route("/signup", get(get_create_user).post(post_create_user)) .route("/signup_success/:id", get(handle_signup_success)) .route("/login", get(get_login).post(post_login)) .route("/logout", get(get_logout).post(post_logout)) + .route("/watches", get(get_watches)) + .route("/search", get(get_search_watch).post(post_search_watch)) + .route( + "/add", + get(get_search_watch) + .put(put_add_watch) + .post(post_add_watch), + ) .fallback(handle_slash_redir) .layer(middleware::from_fn_with_state( db_pool.clone(), diff --git a/src/login.rs b/src/login.rs index f9532f2..5467de1 100644 --- a/src/login.rs +++ b/src/login.rs @@ -10,11 +10,7 @@ use axum::{ }; use sqlx::SqlitePool; -use crate::{ - templates::{LoginGet, LoginPost, LogoutGet, LogoutPost}, - util::form_decode, - AuthContext, User, -}; +use crate::{util::form_decode, AuthContext, LoginGet, LoginPost, LogoutGet, LogoutPost, User}; //-************************************************************************ // Constants @@ -104,7 +100,7 @@ pub async fn post_logout(mut auth: AuthContext) -> impl IntoResponse { #[cfg(test)] mod test { use crate::{ - templates::{Index, LoginGet, LogoutGet, LogoutPost}, + templates::{LoginGet, LogoutGet, LogoutPost, MainPage}, test_utils::{get_test_user, massage, server, FORM_CONTENT_TYPE}, }; @@ -198,7 +194,7 @@ mod test { .await; assert_eq!(resp.status_code(), 303); - let logged_in = Index { + let logged_in = MainPage { user: Some(get_test_user()), } .to_string(); diff --git a/src/main.rs b/src/main.rs index 7418799..9ee5d62 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ use std::net::SocketAddr; use rand_core::{OsRng, RngCore}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; -use witch_watch::db; +use witch_watch::get_db_pool; #[tokio::main] async fn main() { @@ -14,7 +14,7 @@ async fn main() { .with(tracing_subscriber::fmt::layer()) .init(); - let pool = db::get_pool().await; + let pool = get_db_pool().await; let secret = { let mut bytes = [0u8; 64]; @@ -25,10 +25,10 @@ async fn main() { let app = witch_watch::app(pool, &secret).await; - let addr = ([127, 0, 0, 1], 3000); + let addr: SocketAddr = ([127, 0, 0, 1], 3000).into(); tracing::debug!("binding to {addr:?}"); - axum::Server::bind(&SocketAddr::from(addr)) + axum::Server::bind(&addr) .serve(app.into_make_service()) .await .unwrap(); diff --git a/src/signup.rs b/src/signup.rs index ff6785f..fb1e081 100644 --- a/src/signup.rs +++ b/src/signup.rs @@ -11,10 +11,7 @@ use sqlx::{query_as, SqlitePool}; use unicode_segmentation::UnicodeSegmentation; use uuid::Uuid; -use crate::{ - templates::{CreateUser, CreateUserSuccess}, - User, -}; +use crate::{CreateUser, CreateUserSuccess, User}; pub(crate) const CREATE_QUERY: &str = "insert into witches (id, username, displayname, email, pwhash) values ($1, $2, $3, $4, $5)"; @@ -29,7 +26,7 @@ const ID_QUERY: &str = "select * from witches where id = $1"; pub struct CreateUserError(#[from] CreateUserErrorKind); impl IntoResponse for CreateUserError { - fn into_response(self) -> askama_axum::Response { + fn into_response(self) -> Response { match self.0 { CreateUserErrorKind::UnknownDBError => { (StatusCode::INTERNAL_SERVER_ERROR, format!("{self}")).into_response() @@ -236,7 +233,7 @@ mod test { use axum::http::StatusCode; use crate::{ - db::get_pool, + db::get_db_pool, templates::{CreateUser, CreateUserSuccess}, test_utils::{get_test_user, insert_user, massage, server_with_pool, FORM_CONTENT_TYPE}, User, @@ -246,7 +243,7 @@ mod test { #[tokio::test] async fn post_create_user() { - let pool = get_pool().await; + let pool = get_db_pool().await; let server = server_with_pool(&pool).await; let body = massage(GOOD_FORM); @@ -266,7 +263,7 @@ mod test { #[tokio::test] async fn get_create_user() { - let pool = get_pool().await; + let pool = get_db_pool().await; let server = server_with_pool(&pool).await; let resp = server.get("/signup").await; @@ -277,7 +274,7 @@ mod test { #[tokio::test] async fn handle_signup_success() { - let pool = get_pool().await; + let pool = get_db_pool().await; let server = server_with_pool(&pool).await; let user = get_test_user(); @@ -313,7 +310,7 @@ mod test { #[tokio::test] async fn password_mismatch() { - let pool = get_pool().await; + let pool = get_db_pool().await; let server = server_with_pool(&pool).await; let body = massage(PASSWORD_MISMATCH_FORM); @@ -336,7 +333,7 @@ mod test { #[tokio::test] async fn password_short() { - let pool = get_pool().await; + let pool = get_db_pool().await; let server = server_with_pool(&pool).await; let body = massage(PASSWORD_SHORT_FORM); @@ -359,7 +356,7 @@ mod test { #[tokio::test] async fn password_long() { - let pool = get_pool().await; + let pool = get_db_pool().await; let server = server_with_pool(&pool).await; let body = massage(PASSWORD_LONG_FORM); @@ -382,7 +379,7 @@ mod test { #[tokio::test] async fn username_short() { - let pool = get_pool().await; + let pool = get_db_pool().await; let server = server_with_pool(&pool).await; let body = massage(USERNAME_SHORT_FORM); @@ -405,7 +402,7 @@ mod test { #[tokio::test] async fn username_long() { - let pool = get_pool().await; + let pool = get_db_pool().await; let server = server_with_pool(&pool).await; let body = massage(USERNAME_LONG_FORM); @@ -428,7 +425,7 @@ mod test { #[tokio::test] async fn username_duplicate() { - let pool = get_pool().await; + let pool = get_db_pool().await; let server = server_with_pool(&pool).await; let body = massage(GOOD_FORM); @@ -459,7 +456,7 @@ mod test { #[tokio::test] async fn displayname_long() { - let pool = get_pool().await; + let pool = get_db_pool().await; let server = server_with_pool(&pool).await; let body = massage(DISPLAYNAME_LONG_FORM); @@ -482,7 +479,7 @@ mod test { #[tokio::test] async fn handle_signup_success() { - let pool = get_pool().await; + let pool = get_db_pool().await; let server = server_with_pool(&pool).await; let path = format!("/signup_success/nope"); diff --git a/src/templates.rs b/src/templates.rs index 9f8748d..4d624f6 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -41,6 +41,6 @@ pub struct LogoutPost; #[derive(Debug, Default, Template, Deserialize, Serialize, PartialEq, Eq)] #[template(path = "index.html")] -pub struct Index { +pub struct MainPage { pub user: Option, } diff --git a/src/test_utils.rs b/src/test_utils.rs index 20ae1b8..94d6378 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -19,7 +19,7 @@ pub fn get_test_user() -> User { } pub async fn server() -> TestServer { - let pool = crate::db::get_pool().await; + let pool = crate::db::get_db_pool().await; let secret = [0u8; 64]; let user = get_test_user(); diff --git a/src/watches/handlers.rs b/src/watches/handlers.rs new file mode 100644 index 0000000..bc103ad --- /dev/null +++ b/src/watches/handlers.rs @@ -0,0 +1,68 @@ +use axum::{ + extract::{Form, Path, State}, + http::StatusCode, + response::{IntoResponse, Response}, +}; +use sqlx::{query_as, SqlitePool}; +use uuid::Uuid; + +use crate::{AuthContext, GetWatches, ShowKind, User, Watch}; + +//-************************************************************************ +// Constants +//-************************************************************************ + +const GET_WATCHES_QUERY: &str = + "select * from watches left join witch_watch on $1 = witch_watch.witch and watches.id = witch_watch.watch"; + +//-************************************************************************ +// Error types for Watch creation +//-************************************************************************ + +#[Error] +pub struct WatchAddError(#[from] WatchAddErrorKind); + +#[Error] +#[non_exhaustive] +pub enum WatchAddErrorKind { + UnknownDBError, +} + +impl IntoResponse for WatchAddError { + fn into_response(self) -> Response { + match self.0 { + WatchAddErrorKind::UnknownDBError => { + (StatusCode::INTERNAL_SERVER_ERROR, format!("{self}")).into_response() + } + } + } +} + +/// Add a Watch to the whole system +pub async fn put_add_watch() {} + +/// A single Watch +pub async fn get_watch() {} + +/// 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() { + query_as(GET_WATCHES_QUERY) + .bind(user.as_ref().unwrap().id) + .fetch_all(&pool) + .await + .unwrap_or_default() + } else { + vec![] + }; + + GetWatches { + watches, + user: user.clone(), + } +} + +pub async fn get_search_watch() {} + +pub async fn post_search_watch() {} diff --git a/src/watches/mod.rs b/src/watches/mod.rs new file mode 100644 index 0000000..2dd680b --- /dev/null +++ b/src/watches/mod.rs @@ -0,0 +1,79 @@ +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +pub mod handlers; +pub mod templates; + +#[derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, sqlx::Type, +)] +#[repr(i32)] +pub enum ShowKind { + Movie = 0, + Series = 1, + LimitedSeries = 2, + Short = 3, + Unknown = 4, +} + +impl std::fmt::Display for ShowKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + todo!() + } +} + +impl Default for ShowKind { + fn default() -> Self { + Self::Unknown + } +} + +impl From for ShowKind { + fn from(value: i32) -> Self { + match value { + 0 => Self::Movie, + 1 => Self::Series, + 2 => Self::LimitedSeries, + 3 => Self::Short, + 4 => Self::Unknown, + _ => Self::Unknown, + } + } +} + +#[derive( + Debug, + Default, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + Serialize, + Deserialize, + sqlx::FromRow, +)] +pub struct Watch { + pub id: Uuid, + pub kind: ShowKind, + pub title: String, + pub metadata_url: Option, + pub length: Option, + pub release_date: Option, + added_by: Uuid, // this shouldn't be exposed to randos + created_at: i64, + last_updated: i64, +} + +impl Watch { + pub fn new(title: &str, added_by: Uuid) -> Self { + let id = Uuid::new_v4(); + Self { + id, + title: title.to_string(), + added_by, + ..Default::default() + } + } +} diff --git a/src/watches/templates.rs b/src/watches/templates.rs new file mode 100644 index 0000000..1ab4075 --- /dev/null +++ b/src/watches/templates.rs @@ -0,0 +1,11 @@ +use askama::Template; +use serde::{Deserialize, Serialize}; + +use crate::{User, Watch}; + +#[derive(Debug, Default, Template, Deserialize, Serialize, PartialEq, Eq)] +#[template(path = "get_watches.html")] +pub struct GetWatches { + pub watches: Vec, + pub user: Option, +} diff --git a/templates/get_search_watches.html b/templates/get_search_watches.html new file mode 100644 index 0000000..e69de29 diff --git a/templates/get_watches.html b/templates/get_watches.html new file mode 100644 index 0000000..408382c --- /dev/null +++ b/templates/get_watches.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} +{% import "macros.html" as m %} + +{% block title %}Welcome to Witch Watch, Bish{% endblock %} + +{% block content %} + +

Whatcha Watchin?

+ +{% match user %} + {% when Some with (usr) %} +

+Hello, {{ usr.username }}! It's nice to see you. +

+
+

Here are your things to watch:

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

+

+ +
+ +
+

+{% else %} +

+ Heya, why don't you log in or sign up? +

+{% endmatch %} + +{% endblock %} diff --git a/templates/index.html b/templates/index.html index e7c47f2..92c44f3 100644 --- a/templates/index.html +++ b/templates/index.html @@ -9,7 +9,7 @@ {% match user %} {% when Some with (usr) %}

-Hello, {{ usr.username }}! It's nice to see you. + Hello, {{ usr.username }}! It's nice to see you. Let's get watchin'!


diff --git a/templates/macros.html b/templates/macros.html new file mode 100644 index 0000000..78ce461 --- /dev/null +++ b/templates/macros.html @@ -0,0 +1,10 @@ +{% macro get_or_default(val, def) %} + +{% match val %} +{% when Some with (v) %} +{{v}} +{% else %} +{{def}} +{% endmatch %} + +{% endmacro %}