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 %}