Merge WIP material from "watches" branch.
This commit is contained in:
commit
dc3f90228f
16 changed files with 282 additions and 61 deletions
|
@ -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 );
|
||||||
|
|
|
@ -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};
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
60
src/lib.rs
60
src/lib.rs
|
@ -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(),
|
||||||
|
|
10
src/login.rs
10
src/login.rs
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
68
src/watches/handlers.rs
Normal 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
79
src/watches/mod.rs
Normal 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
11
src/watches/templates.rs
Normal 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>,
|
||||||
|
}
|
0
templates/get_search_watches.html
Normal file
0
templates/get_search_watches.html
Normal file
37
templates/get_watches.html
Normal file
37
templates/get_watches.html
Normal 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 %}
|
|
@ -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
10
templates/macros.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{% macro get_or_default(val, def) %}
|
||||||
|
|
||||||
|
{% match val %}
|
||||||
|
{% when Some with (v) %}
|
||||||
|
{{v}}
|
||||||
|
{% else %}
|
||||||
|
{{def}}
|
||||||
|
{% endmatch %}
|
||||||
|
|
||||||
|
{% endmacro %}
|
Loading…
Reference in a new issue