Merge WIP material from "watches" branch.

This commit is contained in:
Joe Ardent 2023-06-09 15:03:52 -07:00
commit dc3f90228f
16 changed files with 282 additions and 61 deletions

View file

@ -20,11 +20,13 @@ create table if not exists watches (
id blob not null primary key, id blob not null primary key,
typ int not null, -- enum for movie or tv show or whatev typ int not null, -- enum for movie or tv show or whatev
title text not null, title text not null,
imdb text, -- possible url for imdb or other metadata-esque site to show the user metadata_url text, -- possible url for imdb or other metadata-esque site to show the user
runtime int, length int,
release_date int, release_date int,
added_by blob not null, -- ID of the user that added it
created_at int not null default (unixepoch()), 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 -- table of what people want to watch
@ -68,6 +70,6 @@ create table if not exists watch_notes (
-- indices, not needed for covens -- indices, not needed for covens
create index if not exists witch_dex on witches ( username, email ); 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 ww_dex on witch_watch ( witch, watch, public );
create index if not exists note_dex on watch_notes ( witch, watch, public ); create index if not exists note_dex on watch_notes ( witch, watch, public );

View file

@ -20,7 +20,7 @@ const MIN_CONNS: u32 = 5;
const TIMEOUT: u64 = 11; const TIMEOUT: u64 = 11;
const SESSION_TTL: Duration = Duration::from_secs((365.2422 * 24. * 3600.0) as u64); 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 = { let db_filename = {
std::env::var("DATABASE_FILE").unwrap_or_else(|_| { std::env::var("DATABASE_FILE").unwrap_or_else(|_| {
#[cfg(not(test))] #[cfg(not(test))]
@ -115,7 +115,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn it_migrates_the_db() { 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") let r = sqlx::query("select count(*) from witches")
.fetch_one(&db) .fetch_one(&db)
.await; .await;
@ -130,6 +130,7 @@ mod tests {
//-************************************************************************ //-************************************************************************
// Session store sub-module, not a public lib. // Session store sub-module, not a public lib.
//-************************************************************************ //-************************************************************************
#[allow(dead_code)]
mod session_store { mod session_store {
use async_session::{async_trait, chrono::Utc, log, serde_json, Result, Session}; use async_session::{async_trait, chrono::Utc, log, serde_json, Result, Session};
use sqlx::{pool::PoolConnection, Sqlite}; use sqlx::{pool::PoolConnection, Sqlite};

View file

@ -1,6 +1,6 @@
use axum::response::{IntoResponse, Redirect}; use axum::response::{IntoResponse, Redirect};
use crate::{templates::Index, AuthContext}; use crate::{AuthContext, MainPage};
pub async fn handle_slash_redir() -> impl IntoResponse { pub async fn handle_slash_redir() -> impl IntoResponse {
Redirect::to("/") Redirect::to("/")
@ -13,7 +13,7 @@ pub async fn handle_slash(auth: AuthContext) -> impl IntoResponse {
} else { } else {
tracing::debug!("Not logged in."); tracing::debug!("Not logged in.");
} }
Index { MainPage {
user: auth.current_user, user: auth.current_user,
} }
} }
@ -26,7 +26,7 @@ mod test {
#[tokio::test] #[tokio::test]
async fn slash_is_ok() { async fn slash_is_ok() {
let pool = db::get_pool().await; let pool = db::get_db_pool().await;
let secret = [0u8; 64]; let secret = [0u8; 64];
let app = crate::app(pool.clone(), &secret).await.into_make_service(); let app = crate::app(pool.clone(), &secret).await.into_make_service();
@ -37,7 +37,7 @@ mod test {
#[tokio::test] #[tokio::test]
async fn not_found_is_303() { async fn not_found_is_303() {
let pool = db::get_pool().await; let pool = db::get_db_pool().await;
let secret = [0u8; 64]; let secret = [0u8; 64];
let app = crate::app(pool, &secret).await.into_make_service(); let app = crate::app(pool, &secret).await.into_make_service();

View file

@ -1,38 +1,58 @@
#[macro_use] #[macro_use]
extern crate justerror; 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)] #[cfg(test)]
pub mod test_utils; pub mod test_utils;
pub type AuthContext = axum_login::extractors::AuthContext<Uuid, User, SqliteStore<User>>; /// 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<uuid::Uuid, User, axum_login::SqliteStore<User>>;
/// 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 session_layer = db::session_layer(db_pool.clone(), secret).await;
let auth_layer = db::auth_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("/", get(handle_slash).post(handle_slash))
.route("/signup", get(get_create_user).post(post_create_user)) .route("/signup", get(get_create_user).post(post_create_user))
.route("/signup_success/:id", get(handle_signup_success)) .route("/signup_success/:id", get(handle_signup_success))
.route("/login", get(get_login).post(post_login)) .route("/login", get(get_login).post(post_login))
.route("/logout", get(get_logout).post(post_logout)) .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) .fallback(handle_slash_redir)
.layer(middleware::from_fn_with_state( .layer(middleware::from_fn_with_state(
db_pool.clone(), db_pool.clone(),

View file

@ -10,11 +10,7 @@ use axum::{
}; };
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::{ use crate::{util::form_decode, AuthContext, LoginGet, LoginPost, LogoutGet, LogoutPost, User};
templates::{LoginGet, LoginPost, LogoutGet, LogoutPost},
util::form_decode,
AuthContext, User,
};
//-************************************************************************ //-************************************************************************
// Constants // Constants
@ -104,7 +100,7 @@ pub async fn post_logout(mut auth: AuthContext) -> impl IntoResponse {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::{ use crate::{
templates::{Index, LoginGet, LogoutGet, LogoutPost}, templates::{LoginGet, LogoutGet, LogoutPost, MainPage},
test_utils::{get_test_user, massage, server, FORM_CONTENT_TYPE}, test_utils::{get_test_user, massage, server, FORM_CONTENT_TYPE},
}; };
@ -198,7 +194,7 @@ mod test {
.await; .await;
assert_eq!(resp.status_code(), 303); assert_eq!(resp.status_code(), 303);
let logged_in = Index { let logged_in = MainPage {
user: Some(get_test_user()), user: Some(get_test_user()),
} }
.to_string(); .to_string();

View file

@ -2,7 +2,7 @@ use std::net::SocketAddr;
use rand_core::{OsRng, RngCore}; use rand_core::{OsRng, RngCore};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use witch_watch::db; use witch_watch::get_db_pool;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
@ -14,7 +14,7 @@ async fn main() {
.with(tracing_subscriber::fmt::layer()) .with(tracing_subscriber::fmt::layer())
.init(); .init();
let pool = db::get_pool().await; let pool = get_db_pool().await;
let secret = { let secret = {
let mut bytes = [0u8; 64]; let mut bytes = [0u8; 64];
@ -25,10 +25,10 @@ async fn main() {
let app = witch_watch::app(pool, &secret).await; 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:?}"); tracing::debug!("binding to {addr:?}");
axum::Server::bind(&SocketAddr::from(addr)) axum::Server::bind(&addr)
.serve(app.into_make_service()) .serve(app.into_make_service())
.await .await
.unwrap(); .unwrap();

View file

@ -11,10 +11,7 @@ use sqlx::{query_as, SqlitePool};
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{CreateUser, CreateUserSuccess, User};
templates::{CreateUser, CreateUserSuccess},
User,
};
pub(crate) const CREATE_QUERY: &str = pub(crate) const CREATE_QUERY: &str =
"insert into witches (id, username, displayname, email, pwhash) values ($1, $2, $3, $4, $5)"; "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); pub struct CreateUserError(#[from] CreateUserErrorKind);
impl IntoResponse for CreateUserError { impl IntoResponse for CreateUserError {
fn into_response(self) -> askama_axum::Response { fn into_response(self) -> Response {
match self.0 { match self.0 {
CreateUserErrorKind::UnknownDBError => { CreateUserErrorKind::UnknownDBError => {
(StatusCode::INTERNAL_SERVER_ERROR, format!("{self}")).into_response() (StatusCode::INTERNAL_SERVER_ERROR, format!("{self}")).into_response()
@ -236,7 +233,7 @@ mod test {
use axum::http::StatusCode; use axum::http::StatusCode;
use crate::{ use crate::{
db::get_pool, db::get_db_pool,
templates::{CreateUser, CreateUserSuccess}, templates::{CreateUser, CreateUserSuccess},
test_utils::{get_test_user, insert_user, massage, server_with_pool, FORM_CONTENT_TYPE}, test_utils::{get_test_user, insert_user, massage, server_with_pool, FORM_CONTENT_TYPE},
User, User,
@ -246,7 +243,7 @@ mod test {
#[tokio::test] #[tokio::test]
async fn post_create_user() { async fn post_create_user() {
let pool = get_pool().await; let pool = get_db_pool().await;
let server = server_with_pool(&pool).await; let server = server_with_pool(&pool).await;
let body = massage(GOOD_FORM); let body = massage(GOOD_FORM);
@ -266,7 +263,7 @@ mod test {
#[tokio::test] #[tokio::test]
async fn get_create_user() { async fn get_create_user() {
let pool = get_pool().await; let pool = get_db_pool().await;
let server = server_with_pool(&pool).await; let server = server_with_pool(&pool).await;
let resp = server.get("/signup").await; let resp = server.get("/signup").await;
@ -277,7 +274,7 @@ mod test {
#[tokio::test] #[tokio::test]
async fn handle_signup_success() { async fn handle_signup_success() {
let pool = get_pool().await; let pool = get_db_pool().await;
let server = server_with_pool(&pool).await; let server = server_with_pool(&pool).await;
let user = get_test_user(); let user = get_test_user();
@ -313,7 +310,7 @@ mod test {
#[tokio::test] #[tokio::test]
async fn password_mismatch() { async fn password_mismatch() {
let pool = get_pool().await; let pool = get_db_pool().await;
let server = server_with_pool(&pool).await; let server = server_with_pool(&pool).await;
let body = massage(PASSWORD_MISMATCH_FORM); let body = massage(PASSWORD_MISMATCH_FORM);
@ -336,7 +333,7 @@ mod test {
#[tokio::test] #[tokio::test]
async fn password_short() { async fn password_short() {
let pool = get_pool().await; let pool = get_db_pool().await;
let server = server_with_pool(&pool).await; let server = server_with_pool(&pool).await;
let body = massage(PASSWORD_SHORT_FORM); let body = massage(PASSWORD_SHORT_FORM);
@ -359,7 +356,7 @@ mod test {
#[tokio::test] #[tokio::test]
async fn password_long() { async fn password_long() {
let pool = get_pool().await; let pool = get_db_pool().await;
let server = server_with_pool(&pool).await; let server = server_with_pool(&pool).await;
let body = massage(PASSWORD_LONG_FORM); let body = massage(PASSWORD_LONG_FORM);
@ -382,7 +379,7 @@ mod test {
#[tokio::test] #[tokio::test]
async fn username_short() { async fn username_short() {
let pool = get_pool().await; let pool = get_db_pool().await;
let server = server_with_pool(&pool).await; let server = server_with_pool(&pool).await;
let body = massage(USERNAME_SHORT_FORM); let body = massage(USERNAME_SHORT_FORM);
@ -405,7 +402,7 @@ mod test {
#[tokio::test] #[tokio::test]
async fn username_long() { async fn username_long() {
let pool = get_pool().await; let pool = get_db_pool().await;
let server = server_with_pool(&pool).await; let server = server_with_pool(&pool).await;
let body = massage(USERNAME_LONG_FORM); let body = massage(USERNAME_LONG_FORM);
@ -428,7 +425,7 @@ mod test {
#[tokio::test] #[tokio::test]
async fn username_duplicate() { async fn username_duplicate() {
let pool = get_pool().await; let pool = get_db_pool().await;
let server = server_with_pool(&pool).await; let server = server_with_pool(&pool).await;
let body = massage(GOOD_FORM); let body = massage(GOOD_FORM);
@ -459,7 +456,7 @@ mod test {
#[tokio::test] #[tokio::test]
async fn displayname_long() { async fn displayname_long() {
let pool = get_pool().await; let pool = get_db_pool().await;
let server = server_with_pool(&pool).await; let server = server_with_pool(&pool).await;
let body = massage(DISPLAYNAME_LONG_FORM); let body = massage(DISPLAYNAME_LONG_FORM);
@ -482,7 +479,7 @@ mod test {
#[tokio::test] #[tokio::test]
async fn handle_signup_success() { async fn handle_signup_success() {
let pool = get_pool().await; let pool = get_db_pool().await;
let server = server_with_pool(&pool).await; let server = server_with_pool(&pool).await;
let path = format!("/signup_success/nope"); let path = format!("/signup_success/nope");

View file

@ -41,6 +41,6 @@ pub struct LogoutPost;
#[derive(Debug, Default, Template, Deserialize, Serialize, PartialEq, Eq)] #[derive(Debug, Default, Template, Deserialize, Serialize, PartialEq, Eq)]
#[template(path = "index.html")] #[template(path = "index.html")]
pub struct Index { pub struct MainPage {
pub user: Option<User>, pub user: Option<User>,
} }

View file

@ -19,7 +19,7 @@ pub fn get_test_user() -> User {
} }
pub async fn server() -> TestServer { 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 secret = [0u8; 64];
let user = get_test_user(); let user = get_test_user();

68
src/watches/handlers.rs Normal file
View file

@ -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<SqlitePool>) -> impl IntoResponse {
let user = &auth.current_user;
let watches: Vec<Watch> = 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() {}

79
src/watches/mod.rs Normal file
View file

@ -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<i32> 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<String>,
pub length: Option<i32>,
pub release_date: Option<i64>,
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()
}
}
}

11
src/watches/templates.rs Normal file
View file

@ -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<Watch>,
pub user: Option<User>,
}

View file

View file

@ -0,0 +1,37 @@
{% extends "base.html" %}
{% import "macros.html" as m %}
{% block title %}Welcome to Witch Watch, Bish{% endblock %}
{% block content %}
<h1>Whatcha Watchin?</h1>
{% match user %}
{% when Some with (usr) %}
<p>
Hello, {{ usr.username }}! It's nice to see you.
</p>
</br>
<p>Here are your things to watch:</p>
<div class="watchlist">
<ul>
{% for watch in watches %}
<li><span class="watchtitle">{{watch.title}}</span> -- {% call m::get_or_default(watch.release_date, "when??") %}: </li>
{% endfor %}
</ul>
</div>
<p>
<form action="/search" enctype="application/x-www-form-urlencoded" method="post">
<label for="search">Looking for something else to watch?</label>
<input type="text" name="search" id="search"></br>
<input type="submit" value="Let's go!">
</form>
</p>
{% else %}
<p>
Heya, why don't you <a href="/login">log in</a> or <a href="/signup">sign up</a>?
</p>
{% endmatch %}
{% endblock %}

View file

@ -9,7 +9,7 @@
{% match user %} {% match user %}
{% when Some with (usr) %} {% when Some with (usr) %}
<p> <p>
Hello, {{ usr.username }}! It's nice to see you. Hello, {{ usr.username }}! It's nice to see you. <a href="watches">Let's get watchin'!</a>
</p> </p>
</br> </br>
<p> <p>

10
templates/macros.html Normal file
View file

@ -0,0 +1,10 @@
{% macro get_or_default(val, def) %}
{% match val %}
{% when Some with (v) %}
{{v}}
{% else %}
{{def}}
{% endmatch %}
{% endmacro %}