update deps, fuck with errors

This commit is contained in:
Joe Ardent 2025-08-28 18:20:01 -07:00
parent 72ca947cf6
commit fc26533460
11 changed files with 788 additions and 559 deletions

780
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
[package] [package]
name = "what2watch" name = "what2watch"
version = "0.0.1" version = "0.0.1"
edition = "2021" edition = "2024"
default-run = "what2watch" default-run = "what2watch"
[dependencies] [dependencies]
@ -10,16 +10,14 @@ optional_optional_user = {path = "optional_optional_user"}
# regular external deps # regular external deps
argon2 = "0.5" argon2 = "0.5"
askama = { version = "0.12", features = ["with-axum"] } askama = { version = "0.14" }
askama_axum = "0.4" axum = { version = "0.8", features = ["macros"] }
async-trait = "0.1" axum-login = "0.18"
axum = { version = "0.7", features = ["macros"] } axum-macros = "0.5"
axum-login = "0.14"
axum-macros = "0.4"
chrono = { version = "0.4", default-features = false, features = ["std", "clock", "serde"] } chrono = { version = "0.4", default-features = false, features = ["std", "clock", "serde"] }
clap = { version = "4", features = ["derive", "env", "unicode", "suggestions", "usage"] } clap = { version = "4", features = ["derive", "env", "unicode", "suggestions", "usage"] }
confy = "0.6" confy = "1"
dirs = "5" dirs = "6"
http = "1" http = "1"
julid-rs = "1" julid-rs = "1"
justerror = "1" justerror = "1"
@ -29,18 +27,16 @@ password-hash = { version = "0.5", features = ["std", "getrandom"] }
rand = "0.8" rand = "0.8"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
sha256 = { version = "1", default-features = false } sha256 = { version = "1", default-features = false }
sqlx = { version = "0.7", default-features = false, features = ["runtime-tokio", "sqlite", "tls-none", "migrate", "chrono"] } sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio", "sqlite", "tls-none", "migrate", "chrono"] }
thiserror = "1" thiserror = "1"
tokio = { version = "1", features = ["rt-multi-thread", "signal", "tracing"], default-features = false } tokio = { version = "1", features = ["rt-multi-thread", "signal", "tracing"], default-features = false }
tower = { version = "0.4", features = ["util", "timeout"], default-features = false } tower = { version = "0.5", features = ["util", "timeout"], default-features = false }
tower-http = { version = "0.5", features = ["add-extension", "trace", "tracing", "fs"], default-features = false } tower-http = { version = "0.6", features = ["add-extension", "trace", "tracing", "fs"], default-features = false }
tower-sessions = { version = "0.11", default-features = false } tower-sessions = { version = "0.14", default-features = false }
tower-sessions-sqlx-store = { version = "0.11.0", default-features = false, features = ["sqlite"] } tower-sessions-sqlx-store = { version = "0.15.0", default-features = false, features = ["sqlite"] }
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
unicode-segmentation = "1" unicode-segmentation = "1"
[dev-dependencies] [dev-dependencies]
axum-test = "14" axum-test = "14"

View file

@ -1,4 +1,3 @@
use async_trait::async_trait;
use axum_login::{AuthUser, AuthnBackend, UserId}; use axum_login::{AuthUser, AuthnBackend, UserId};
use julid::Julid; use julid::Julid;
use password_auth::verify_password; use password_auth::verify_password;
@ -37,7 +36,6 @@ pub enum AuthErrorKind {
Unknown, Unknown,
} }
#[async_trait]
impl AuthnBackend for AuthStore { impl AuthnBackend for AuthStore {
type User = User; type User = User;
type Credentials = Credentials; type Credentials = Credentials;

View file

@ -1,6 +1,7 @@
use askama::Template;
use axum::response::{IntoResponse, Redirect}; use axum::response::{IntoResponse, Redirect};
use crate::{AuthSession, MainPage}; use crate::{AuthSession, MainPage, Wender};
pub async fn handle_slash_redir() -> impl IntoResponse { pub async fn handle_slash_redir() -> impl IntoResponse {
Redirect::to("/") Redirect::to("/")
@ -14,7 +15,7 @@ pub async fn handle_slash(auth: AuthSession) -> impl IntoResponse {
} else { } else {
tracing::debug!("Not logged in."); tracing::debug!("Not logged in.");
} }
MainPage { user: auth.user } MainPage { user: auth.user }.render().wender()
} }
#[cfg(test)] #[cfg(test)]

View file

@ -1,8 +1,11 @@
use axum::{ use axum::{
body::Body,
middleware, middleware,
response::IntoResponse,
routing::{get, post, IntoMakeService}, routing::{get, post, IntoMakeService},
}; };
use axum_login::AuthManagerLayerBuilder; use axum_login::AuthManagerLayerBuilder;
use http::StatusCode;
use sqlx::SqlitePool; use sqlx::SqlitePool;
#[macro_use] #[macro_use]
extern crate justerror; extern crate justerror;
@ -42,6 +45,14 @@ use optional_optional_user::OptionalOptionalUser;
use templates::*; use templates::*;
use watches::templates::*; use watches::templates::*;
#[Error]
pub enum WatchError {
Auth(auth::AuthError),
Signup(signup::SignupError),
Watches(watches::WatchesError),
Render,
}
/// Returns the router to be used as a service or test object, you do you. /// Returns the router to be used as a service or test object, you do you.
pub async fn app(db_pool: sqlx::SqlitePool) -> IntoMakeService<axum::Router> { pub async fn app(db_pool: sqlx::SqlitePool) -> IntoMakeService<axum::Router> {
// don't bother bringing handlers into the whole crate namespace // don't bother bringing handlers into the whole crate namespace
@ -71,19 +82,19 @@ pub async fn app(db_pool: sqlx::SqlitePool) -> IntoMakeService<axum::Router> {
axum::Router::new() axum::Router::new()
.route("/", get(handle_slash).post(handle_slash)) .route("/", get(handle_slash).post(handle_slash))
.nest_service("/assets", assets_svc) .nest_service("/assets", assets_svc)
.route("/signup", post(post_create_user)) .route("/signup", get(get_create_user).post(post_create_user))
.route("/signup/:invitation", get(get_create_user)) .route("/signup/{invitation}", get(get_create_user))
.route("/signup_success/:user", get(get_signup_success)) .route("/signup_success/{user}", get(get_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("/watches", get(get_watches))
.route("/watch", get(get_watch)) .route("/watch", get(get_watch))
.route("/watch/:watch", get(get_watch)) .route("/watch/{watch}", get(get_watch))
.route( .route(
"/watch/add", "/watch/add",
get(get_add_new_watch).post(post_add_new_watch), get(get_add_new_watch).post(post_add_new_watch),
) )
.route("/watch/status/:watch", get(get_watch_status)) .route("/watch/status/{watch}", get(get_watch_status))
.route( .route(
"/quest/add", "/quest/add",
get(get_search_watch).post(post_add_watch_quest), get(get_search_watch).post(post_add_watch_quest),
@ -100,6 +111,55 @@ pub async fn app(db_pool: sqlx::SqlitePool) -> IntoMakeService<axum::Router> {
.into_make_service() .into_make_service()
} }
//-************************************************************************
// internal stuff
//-************************************************************************
pub(crate) trait Wender {
fn wender(self) -> Body;
}
impl Wender for askama::Result<String> {
fn wender(self) -> Body {
let b = self.unwrap_or_else(|e| {
tracing::error!("got error rendering template: {e}");
"".to_string()
});
Body::new(b)
}
}
impl IntoResponse for WatchError {
fn into_response(self) -> axum::response::Response {
match self {
Self::Auth(ae) => ae.into_response(),
Self::Signup(se) => se.into_response(),
Self::Watches(we) => we.into_response(),
Self::Render => {
(StatusCode::INTERNAL_SERVER_ERROR, "could not render page").into_response()
}
}
}
}
impl From<auth::AuthError> for WatchError {
fn from(value: auth::AuthError) -> Self {
Self::Auth(value)
}
}
impl From<signup::SignupError> for WatchError {
fn from(value: signup::SignupError) -> Self {
Self::Signup(value)
}
}
impl From<watches::WatchesError> for WatchError {
fn from(value: watches::WatchesError) -> Self {
Self::Watches(value)
}
}
#[cfg(test)] #[cfg(test)]
pub mod test_utils; pub mod test_utils;

View file

@ -1,3 +1,4 @@
use askama::Template;
use axum::{ use axum::{
http::StatusCode, http::StatusCode,
response::{IntoResponse, Redirect, Response}, response::{IntoResponse, Redirect, Response},
@ -7,7 +8,7 @@ use serde::{Deserialize, Serialize};
use crate::{ use crate::{
auth::{AuthError, AuthErrorKind, Credentials}, auth::{AuthError, AuthErrorKind, Credentials},
AuthSession, LoginPage, LogoutPage, LogoutSuccessPage, AuthSession, LoginPage, LogoutPage, LogoutSuccessPage, WatchError, Wender,
}; };
//-************************************************************************ //-************************************************************************
@ -53,16 +54,20 @@ impl From<LoginPostForm> for Credentials {
pub async fn post_login( pub async fn post_login(
mut auth: AuthSession, mut auth: AuthSession,
Form(mut login_form): Form<LoginPostForm>, Form(mut login_form): Form<LoginPostForm>,
) -> Result<impl IntoResponse, AuthError> { ) -> Result<impl IntoResponse, WatchError> {
let dest = login_form.destination.take(); let dest = login_form.destination.take();
let user = match auth.authenticate(login_form.clone().into()).await { let user = match auth.authenticate(login_form.clone().into()).await {
Ok(Some(user)) => user, Ok(Some(user)) => user,
Ok(None) => return Ok(LoginPage::default().into_response()), Ok(None) => return Ok(LoginPage::default().render().wender().into_response()),
Err(_) => return Err(AuthErrorKind::Internal.into()), Err(_) => {
let err: AuthError = AuthErrorKind::Internal.into();
return Err(err.into());
}
}; };
if auth.login(&user).await.is_err() { if auth.login(&user).await.is_err() {
return Err(AuthErrorKind::Internal.into()); let err: AuthError = AuthErrorKind::Internal.into();
return Err(err.into());
} }
if let Some(ref next) = dest { if let Some(ref next) = dest {
@ -73,16 +78,16 @@ pub async fn post_login(
} }
pub async fn get_login() -> impl IntoResponse { pub async fn get_login() -> impl IntoResponse {
LoginPage::default() LoginPage::default().render().wender()
} }
pub async fn get_logout() -> impl IntoResponse { pub async fn get_logout() -> impl IntoResponse {
LogoutPage LogoutPage.render().wender()
} }
pub async fn post_logout(mut auth: AuthSession) -> impl IntoResponse { pub async fn post_logout(mut auth: AuthSession) -> impl IntoResponse {
match auth.logout().await { match auth.logout().await {
Ok(_) => LogoutSuccessPage.into_response(), Ok(_) => LogoutSuccessPage.render().wender().into_response(),
Err(e) => { Err(e) => {
tracing::debug!("{e}"); tracing::debug!("{e}");
let e: AuthError = AuthErrorKind::Internal.into(); let e: AuthError = AuthErrorKind::Internal.into();

View file

@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::{ use crate::{
misc_util::empty_string_as_none, AuthSession, OptionalOptionalUser, Star, User, Watch, misc_util::empty_string_as_none, AuthSession, OptionalOptionalUser, Star, User, Watch, Wender,
}; };
#[derive(Debug, Default, Template, Deserialize, Serialize, PartialEq, Eq, OptionalOptionalUser)] #[derive(Debug, Default, Template, Deserialize, Serialize, PartialEq, Eq, OptionalOptionalUser)]
@ -60,6 +60,8 @@ pub async fn get_search_watch(
results: watches, results: watches,
user, user,
} }
.render()
.wender()
} }
pub async fn get_search_star( pub async fn get_search_star(
@ -83,4 +85,6 @@ pub async fn get_search_star(
results: watches, results: watches,
user, user,
} }
.render()
.wender()
} }

View file

@ -2,6 +2,7 @@ use argon2::{
password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, password_hash::{rand_core::OsRng, PasswordHasher, SaltString},
Argon2, Argon2,
}; };
use askama::Template;
use axum::{ use axum::{
extract::{Form, Path, State}, extract::{Form, Path, State},
http::StatusCode, http::StatusCode,
@ -12,48 +13,8 @@ use serde::Deserialize;
use sqlx::{query_as, Sqlite, SqlitePool}; use sqlx::{query_as, Sqlite, SqlitePool};
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use super::{templates::*, Invitation}; use super::{templates::*, CreateUserError, CreateUserErrorKind, Invitation, SignupError};
use crate::{misc_util::empty_string_as_none, User}; use crate::{misc_util::empty_string_as_none, User, WatchError, Wender};
//-************************************************************************
// Error types for user creation
//-************************************************************************
#[Error(desc = "Could not create user.")]
#[non_exhaustive]
pub struct CreateUserError(#[from] CreateUserErrorKind);
impl IntoResponse for CreateUserError {
fn into_response(self) -> Response {
match self.0 {
CreateUserErrorKind::UnknownDBError => {
(StatusCode::INTERNAL_SERVER_ERROR, format!("{self}")).into_response()
}
CreateUserErrorKind::BadInvitation => (
StatusCode::OK,
SignupErrorPage("Sorry, that invitation isn't valid.".to_string()),
)
.into_response(),
_ => (StatusCode::OK, format!("{self}")).into_response(),
}
}
}
#[Error]
#[non_exhaustive]
pub enum CreateUserErrorKind {
BadInvitation,
AlreadyExists,
#[error(desc = "Usernames must be between 1 and 20 characters long")]
BadUsername,
PasswordMismatch,
#[error(desc = "Password must have at least 4 and at most 50 characters")]
BadPassword,
#[error(desc = "Display name must be less than 100 characters long")]
BadDisplayname,
BadEmail,
UnknownDBError,
}
#[derive(Debug, Default, Deserialize, PartialEq, Eq)] #[derive(Debug, Default, Deserialize, PartialEq, Eq)]
pub struct SignupForm { pub struct SignupForm {
@ -76,15 +37,25 @@ pub struct SignupForm {
pub async fn get_create_user( pub async fn get_create_user(
State(_pool): State<SqlitePool>, State(_pool): State<SqlitePool>,
invitation: Option<Path<String>>, invitation: Option<Path<String>>,
) -> Result<impl IntoResponse, CreateUserError> { ) -> Result<impl IntoResponse, WatchError> {
let invitation = invitation.ok_or(CreateUserErrorKind::BadInvitation)?; let Path(invitation) = invitation.ok_or_else(|| {
let invitation = let e: CreateUserError = CreateUserErrorKind::BadInvitation.into();
Julid::from_str(&invitation.0).map_err(|_| CreateUserErrorKind::BadInvitation)?; let e: SignupError = e.into();
e
})?;
let invitation = Julid::from_str(&invitation).map_err(|_| {
let e: CreateUserError = CreateUserErrorKind::BadInvitation.into();
let e: SignupError = e.into();
e
})?;
Ok(SignupPage { Ok(SignupPage {
invitation, invitation,
..Default::default() ..Default::default()
}) }
.render()
.wender()
.into_response())
} }
/// Post Handler: validates form values and calls the actual, private user /// Post Handler: validates form values and calls the actual, private user
@ -93,7 +64,7 @@ pub async fn get_create_user(
pub async fn post_create_user( pub async fn post_create_user(
State(pool): State<SqlitePool>, State(pool): State<SqlitePool>,
Form(signup): Form<SignupForm>, Form(signup): Form<SignupForm>,
) -> Result<impl IntoResponse, CreateUserError> { ) -> Result<impl IntoResponse, SignupError> {
use crate::misc_util::validate_optional_length; use crate::misc_util::validate_optional_length;
let username = signup.username.trim(); let username = signup.username.trim();
let password = signup.password.trim(); let password = signup.password.trim();
@ -156,7 +127,10 @@ pub async fn get_signup_success(
.unwrap_or_default() .unwrap_or_default()
}; };
let mut resp = SignupSuccessPage(user.clone()).into_response(); let mut resp = SignupSuccessPage(user.clone())
.render()
.wender()
.into_response();
if user.username.is_empty() || id.is_alpha() { if user.username.is_empty() || id.is_alpha() {
// redirect to front page if we got here without a valid user ID // redirect to front page if we got here without a valid user ID
@ -178,7 +152,7 @@ pub(crate) async fn create_user(
password: &[u8], password: &[u8],
pool: &SqlitePool, pool: &SqlitePool,
invitation: &str, invitation: &str,
) -> Result<User, CreateUserError> { ) -> Result<User, SignupError> {
const CREATE_QUERY: &str = const CREATE_QUERY: &str =
"insert into users (username, displayname, email, pwhash, invited_by) values ($1, $2, $3, $4, $5) returning *"; "insert into users (username, displayname, email, pwhash, invited_by) values ($1, $2, $3, $4, $5) returning *";
@ -192,7 +166,7 @@ pub(crate) async fn create_user(
let mut tx = pool.begin().await.map_err(|e| { let mut tx = pool.begin().await.map_err(|e| {
tracing::debug!("db error: {e}"); tracing::debug!("db error: {e}");
CreateUserErrorKind::UnknownDBError CreateUserErrorKind::DBError
})?; })?;
let invitation = Julid::from_str(invitation).map_err(|_| CreateUserErrorKind::BadInvitation)?; let invitation = Julid::from_str(invitation).map_err(|_| CreateUserErrorKind::BadInvitation)?;
@ -215,16 +189,16 @@ pub(crate) async fn create_user(
if exit == 2067u32 || exit == 1555 { if exit == 2067u32 || exit == 1555 {
CreateUserErrorKind::AlreadyExists CreateUserErrorKind::AlreadyExists
} else { } else {
CreateUserErrorKind::UnknownDBError CreateUserErrorKind::DBError
} }
} }
_ => CreateUserErrorKind::UnknownDBError, _ => CreateUserErrorKind::DBError,
} }
})?; })?;
tx.commit().await.map_err(|e| { tx.commit().await.map_err(|e| {
tracing::debug!("db error: {e}"); tracing::debug!("db error: {e}");
CreateUserErrorKind::UnknownDBError CreateUserErrorKind::DBError
})?; })?;
Ok(user) Ok(user)
@ -240,7 +214,7 @@ async fn validate_invitation(
.await .await
.map_err(|e| { .map_err(|e| {
tracing::debug!("db error: {e}"); tracing::debug!("db error: {e}");
CreateUserErrorKind::UnknownDBError CreateUserErrorKind::DBError
})? })?
.ok_or(CreateUserErrorKind::BadInvitation)?; .ok_or(CreateUserErrorKind::BadInvitation)?;
@ -263,7 +237,7 @@ async fn validate_invitation(
.await .await
.map_err(|e| { .map_err(|e| {
tracing::debug!("db error: {e}"); tracing::debug!("db error: {e}");
CreateUserErrorKind::UnknownDBError CreateUserErrorKind::DBError
})?; })?;
Ok(invitation.owner) Ok(invitation.owner)

View file

@ -1,15 +1,29 @@
use std::time::Duration; use std::time::Duration;
use askama::Template;
use axum::response::{IntoResponse, Response};
use http::StatusCode;
use julid::Julid; use julid::Julid;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use templates::{SignupErrorPage, SignupPage};
use crate::WatchDate; use crate::{WatchDate, WatchError};
pub mod handlers; pub mod handlers;
pub mod templates; pub mod templates;
#[Error(desc = "Could not create user.")] //-************************************************************************
// Error types for user creation
//-************************************************************************
#[Error]
pub enum SignupError {
Invite(CreateInviteError),
User(CreateUserError),
}
#[Error(desc = "Could not create invitation.")]
#[non_exhaustive] #[non_exhaustive]
#[derive(PartialEq, Eq)] #[derive(PartialEq, Eq)]
pub struct CreateInviteError(#[from] CreateInviteErrorKind); pub struct CreateInviteError(#[from] CreateInviteErrorKind);
@ -23,6 +37,26 @@ pub enum CreateInviteErrorKind {
Unknown, Unknown,
} }
#[Error(desc = "Could not create user.")]
#[non_exhaustive]
pub struct CreateUserError(#[from] CreateUserErrorKind);
#[Error]
#[non_exhaustive]
pub enum CreateUserErrorKind {
BadInvitation,
AlreadyExists,
#[error(desc = "Usernames must be between 1 and 20 characters long")]
BadUsername,
PasswordMismatch,
#[error(desc = "Password must have at least 4 and at most 50 characters")]
BadPassword,
#[error(desc = "Display name must be less than 100 characters long")]
BadDisplayname,
BadEmail,
DBError,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::FromRow)]
pub struct Invitation { pub struct Invitation {
id: Julid, id: Julid,
@ -95,6 +129,87 @@ impl Invitation {
} }
} }
//-************************************************************************
// internal impls
//-************************************************************************
impl From<CreateUserError> for SignupError {
fn from(value: CreateUserError) -> Self {
SignupError::User(value)
}
}
impl From<CreateUserErrorKind> for SignupError {
fn from(value: CreateUserErrorKind) -> Self {
let e: CreateUserError = value.into();
e.into()
}
}
impl From<CreateInviteError> for SignupError {
fn from(value: CreateInviteError) -> Self {
SignupError::Invite(value)
}
}
impl From<CreateInviteErrorKind> for SignupError {
fn from(value: CreateInviteErrorKind) -> Self {
let e: CreateInviteError = value.into();
e.into()
}
}
impl IntoResponse for SignupError {
fn into_response(self) -> Response {
match self {
Self::Invite(ie) => ie.into_response(),
Self::User(ue) => ue.into_response(),
}
}
}
impl IntoResponse for CreateInviteError {
fn into_response(self) -> Response {
match self.0 {
CreateInviteErrorKind::DBError => {
(StatusCode::INTERNAL_SERVER_ERROR, format!("{self}")).into_response()
}
CreateInviteErrorKind::NoOwner => (
StatusCode::OK,
SignupErrorPage("Sorry, invitations require an owner".to_string())
.render()
.map_err(|e| {
tracing::error!("could not render create user error page, got {e}");
StatusCode::INTERNAL_SERVER_ERROR
}),
)
.into_response(),
_ => (StatusCode::OK, format!("{self}")).into_response(),
}
}
}
impl IntoResponse for CreateUserError {
fn into_response(self) -> Response {
match self.0 {
CreateUserErrorKind::DBError => {
(StatusCode::INTERNAL_SERVER_ERROR, format!("{self}")).into_response()
}
CreateUserErrorKind::BadInvitation => (
StatusCode::OK,
SignupErrorPage("Sorry, that invitation isn't valid.".to_string())
.render()
.map_err(|e| {
tracing::error!("could not render create user error page, got {e}");
StatusCode::INTERNAL_SERVER_ERROR
}),
)
.into_response(),
_ => (StatusCode::OK, format!("{self}")).into_response(),
}
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use tokio::runtime::Runtime; use tokio::runtime::Runtime;

View file

@ -1,16 +1,21 @@
use askama::Template;
use axum::{ use axum::{
extract::{Form, Path, State}, extract::{Form, Path, State},
http::StatusCode, http::StatusCode,
response::{IntoResponse, Redirect, Response}, response::{IntoResponse, Redirect},
}; };
use http::HeaderValue; use http::HeaderValue;
use julid::Julid; use julid::Julid;
use serde::Deserialize; use serde::Deserialize;
use sqlx::{query, query_as, query_scalar, SqlitePool}; use sqlx::{query, query_as, query_scalar, SqlitePool};
use super::templates::{AddNewWatchPage, GetWatchPage, WatchStatusMenus}; use super::{
templates::{AddNewWatchPage, GetWatchPage, WatchStatusMenus},
AddError, AddErrorKind, EditError, EditErrorKind, WatchesError,
};
use crate::{ use crate::{
misc_util::empty_string_as_none, AuthSession, MyWatchesPage, ShowKind, Watch, WatchQuest, misc_util::empty_string_as_none, AuthSession, MyWatchesPage, ShowKind, Watch, WatchError,
WatchQuest, Wender,
}; };
//-************************************************************************ //-************************************************************************
@ -28,62 +33,6 @@ const ADD_WATCH_QUEST_QUERY: &str =
const CHECKMARK: &str = "&#10003;"; const CHECKMARK: &str = "&#10003;";
//-************************************************************************
// Error types for Watch creation
//-************************************************************************
#[Error]
pub struct AddError(#[from] AddErrorKind);
#[Error]
#[non_exhaustive]
pub enum AddErrorKind {
UnknownDBError,
NotSignedIn,
}
impl IntoResponse for AddError {
fn into_response(self) -> Response {
match &self.0 {
AddErrorKind::UnknownDBError => {
(StatusCode::INTERNAL_SERVER_ERROR, format!("{self}")).into_response()
}
AddErrorKind::NotSignedIn => (
StatusCode::OK,
"Ope, you need to sign in first!".to_string(),
)
.into_response(),
}
}
}
#[Error]
pub struct EditError(#[from] EditErrorKind);
#[Error]
#[non_exhaustive]
pub enum EditErrorKind {
UnknownDBError,
NotSignedIn,
NotFound,
}
impl IntoResponse for EditError {
fn into_response(self) -> Response {
match &self.0 {
EditErrorKind::UnknownDBError => {
(StatusCode::INTERNAL_SERVER_ERROR, format!("{self}")).into_response()
}
EditErrorKind::NotSignedIn => (
StatusCode::OK,
"Ope, you need to sign in first!".to_string(),
)
.into_response(),
EditErrorKind::NotFound => (StatusCode::OK, "Could not find watch").into_response(),
}
}
}
//-************************************************************************ //-************************************************************************
// Types for receiving arguments from forms // Types for receiving arguments from forms
//-************************************************************************ //-************************************************************************
@ -120,7 +69,7 @@ pub struct PostEditQuest {
//-************************************************************************ //-************************************************************************
pub async fn get_add_new_watch(auth: AuthSession) -> impl IntoResponse { pub async fn get_add_new_watch(auth: AuthSession) -> impl IntoResponse {
AddNewWatchPage { user: auth.user } AddNewWatchPage { user: auth.user }.render().wender()
} }
/// Add a Watch to your watchlist (side effects system-add) /// Add a Watch to your watchlist (side effects system-add)
@ -128,7 +77,7 @@ pub async fn post_add_new_watch(
auth: AuthSession, auth: AuthSession,
State(pool): State<SqlitePool>, State(pool): State<SqlitePool>,
Form(form): Form<PostAddNewWatch>, Form(form): Form<PostAddNewWatch>,
) -> Result<impl IntoResponse, AddError> { ) -> Result<impl IntoResponse, WatchesError> {
if let Some(user) = auth.user { if let Some(user) = auth.user {
{ {
let watch = Watch { let watch = Watch {
@ -147,15 +96,14 @@ pub async fn post_add_new_watch(
watched: false, watched: false,
watch: watch_id, watch: watch_id,
}; };
add_watch_quest_impl(&pool, &quest) add_watch_quest_impl(&pool, &quest).await?;
.await
.map_err(|_| AddErrorKind::UnknownDBError)?;
let location = format!("/watch/{watch_id}"); let location = format!("/watch/{watch_id}");
Ok(Redirect::to(&location)) Ok(Redirect::to(&location))
} }
} else { } else {
Err(AddErrorKind::NotSignedIn.into()) let e: AddError = AddErrorKind::NotSignedIn.into();
Err(e.into())
} }
} }
@ -171,7 +119,7 @@ async fn add_new_watch_impl(db_pool: &SqlitePool, watch: &Watch) -> Result<Julid
.await .await
.map_err(|err| { .map_err(|err| {
tracing::error!("Got error: {err}"); tracing::error!("Got error: {err}");
AddErrorKind::UnknownDBError AddErrorKind::DBError
})?; })?;
Ok(watch_id) Ok(watch_id)
} }
@ -181,7 +129,7 @@ pub async fn post_add_watch_quest(
auth: AuthSession, auth: AuthSession,
State(pool): State<SqlitePool>, State(pool): State<SqlitePool>,
Form(form): Form<PostAddExistingWatch>, Form(form): Form<PostAddExistingWatch>,
) -> Result<impl IntoResponse, AddError> { ) -> Result<impl IntoResponse, WatchesError> {
if let Some(user) = auth.user { if let Some(user) = auth.user {
let quest = WatchQuest { let quest = WatchQuest {
user: user.id, user: user.id,
@ -189,9 +137,8 @@ pub async fn post_add_watch_quest(
public: form.public, public: form.public,
watched: false, watched: false,
}; };
add_watch_quest_impl(&pool, &quest) add_watch_quest_impl(&pool, &quest).await?;
.await
.map_err(|_| AddErrorKind::UnknownDBError)?;
let resp = checkmark(form.public); let resp = checkmark(form.public);
Ok(resp.into_response()) Ok(resp.into_response())
} else { } else {
@ -203,7 +150,7 @@ pub async fn post_add_watch_quest(
} }
} }
pub async fn add_watch_quest_impl(pool: &SqlitePool, quest: &WatchQuest) -> Result<(), ()> { async fn add_watch_quest_impl(pool: &SqlitePool, quest: &WatchQuest) -> Result<(), AddError> {
query(ADD_WATCH_QUEST_QUERY) query(ADD_WATCH_QUEST_QUERY)
.bind(quest.user) .bind(quest.user)
.bind(quest.watch) .bind(quest.watch)
@ -213,6 +160,7 @@ pub async fn add_watch_quest_impl(pool: &SqlitePool, quest: &WatchQuest) -> Resu
.await .await
.map_err(|err| { .map_err(|err| {
tracing::error!("Got error: {err}"); tracing::error!("Got error: {err}");
AddErrorKind::DBError
})?; })?;
Ok(()) Ok(())
} }
@ -222,13 +170,14 @@ pub async fn edit_watch_quest(
auth: AuthSession, auth: AuthSession,
State(pool): State<SqlitePool>, State(pool): State<SqlitePool>,
Form(form): Form<PostEditQuest>, Form(form): Form<PostEditQuest>,
) -> Result<impl IntoResponse, EditError> { ) -> Result<impl IntoResponse, WatchesError> {
if let Some(user) = auth.user { if let Some(user) = auth.user {
let watch = Julid::from_str(form.watch.trim()).map_err(|_| EditErrorKind::NotFound)?; let watch = Julid::from_str(form.watch.trim()).map_err(|_| EditErrorKind::NotFound)?;
Ok( Ok(
edit_watch_quest_impl(&pool, form.act.trim(), user.id, watch) edit_watch_quest_impl(&pool, form.act.trim(), user.id, watch)
.await? .await?
.into_response(), .render()
.wender(),
) )
} else { } else {
Err(EditErrorKind::NotSignedIn.into()) Err(EditErrorKind::NotSignedIn.into())
@ -240,7 +189,7 @@ async fn edit_watch_quest_impl(
action: &str, action: &str,
user: Julid, user: Julid,
watch: Julid, watch: Julid,
) -> Result<WatchStatusMenus, EditErrorKind> { ) -> Result<WatchStatusMenus, EditError> {
let quest: Option<WatchQuest> = query_as(GET_QUEST_QUERY) let quest: Option<WatchQuest> = query_as(GET_QUEST_QUERY)
.bind(user) .bind(user)
.bind(watch) .bind(watch)
@ -248,7 +197,7 @@ async fn edit_watch_quest_impl(
.await .await
.map_err(|e| { .map_err(|e| {
tracing::error!("Got error from checking watch status: {e:?}"); tracing::error!("Got error from checking watch status: {e:?}");
EditErrorKind::UnknownDBError EditErrorKind::DBError
})?; })?;
if let Some(quest) = quest { if let Some(quest) = quest {
match action { match action {
@ -262,7 +211,7 @@ async fn edit_watch_quest_impl(
.await .await
.map_err(|e| { .map_err(|e| {
tracing::error!("Error removing quest: {e}"); tracing::error!("Error removing quest: {e}");
EditErrorKind::UnknownDBError EditErrorKind::DBError
})?; })?;
Ok(WatchStatusMenus { watch, quest: None }) Ok(WatchStatusMenus { watch, quest: None })
} }
@ -278,7 +227,7 @@ async fn edit_watch_quest_impl(
.await .await
.map_err(|e| { .map_err(|e| {
tracing::error!("Error updating quest: {e}"); tracing::error!("Error updating quest: {e}");
EditErrorKind::UnknownDBError EditErrorKind::DBError
})?; })?;
let quest = WatchQuest { watched, ..quest }; let quest = WatchQuest { watched, ..quest };
Ok(WatchStatusMenus { Ok(WatchStatusMenus {
@ -298,7 +247,7 @@ async fn edit_watch_quest_impl(
.await .await
.map_err(|e| { .map_err(|e| {
tracing::error!("Error updating quest: {e}"); tracing::error!("Error updating quest: {e}");
EditErrorKind::UnknownDBError EditErrorKind::DBError
})?; })?;
let quest = WatchQuest { public, ..quest }; let quest = WatchQuest { public, ..quest };
Ok(WatchStatusMenus { Ok(WatchStatusMenus {
@ -312,7 +261,7 @@ async fn edit_watch_quest_impl(
}), }),
} }
} else { } else {
Err(EditErrorKind::NotFound) Err(EditErrorKind::NotFound.into())
} }
} }
@ -336,14 +285,20 @@ pub async fn get_watch(
watch, watch,
user: auth.user, user: auth.user,
} }
.render()
.map_err(|e| {
tracing::error!("could not render watch page, got {e}");
StatusCode::INTERNAL_SERVER_ERROR
})
.into_response()
} }
/// everything the user has saved /// everything the user has saved
pub async fn get_watches(auth: AuthSession, State(pool): State<SqlitePool>) -> impl IntoResponse { pub async fn get_watches(auth: AuthSession, State(pool): State<SqlitePool>) -> impl IntoResponse {
let user = auth.user; let user = auth.user;
let watches: Vec<Watch> = if (user).is_some() { let watches: Vec<Watch> = if let Some(ref user) = user {
query_as(GET_SAVED_WATCHES_QUERY) query_as(GET_SAVED_WATCHES_QUERY)
.bind(user.as_ref().unwrap().id) .bind(user.id)
.fetch_all(&pool) .fetch_all(&pool)
.await .await
.unwrap_or_default() .unwrap_or_default()
@ -352,6 +307,9 @@ pub async fn get_watches(auth: AuthSession, State(pool): State<SqlitePool>) -> i
}; };
MyWatchesPage { watches, user } MyWatchesPage { watches, user }
.render()
.wender()
.into_response()
} }
pub async fn get_watch_status( pub async fn get_watch_status(
@ -369,7 +327,10 @@ pub async fn get_watch_status(
.map_err(|e| { .map_err(|e| {
tracing::error!("Got error from checking watch status: {e:?}"); tracing::error!("Got error from checking watch status: {e:?}");
})?; })?;
Ok(WatchStatusMenus { watch, quest }.into_response()) Ok(WatchStatusMenus { watch, quest }
.render()
.wender()
.into_response())
} else { } else {
Ok("<a href='/login'>Login to add</a>".into_response()) Ok("<a href='/login'>Login to add</a>".into_response())
} }

View file

@ -1,9 +1,44 @@
use axum::response::{IntoResponse, Response};
use http::StatusCode;
use julid::Julid; use julid::Julid;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::WatchError;
pub mod handlers; pub mod handlers;
pub mod templates; pub mod templates;
//-************************************************************************
// Error types for Watch creation
//-************************************************************************
#[Error]
pub enum WatchesError {
Add(AddError),
Edit(EditError),
}
#[Error]
pub struct AddError(#[from] AddErrorKind);
#[Error]
#[non_exhaustive]
pub enum AddErrorKind {
DBError,
NotSignedIn,
}
#[Error]
pub struct EditError(#[from] EditErrorKind);
#[Error]
#[non_exhaustive]
pub enum EditErrorKind {
DBError,
NotSignedIn,
NotFound,
}
#[derive( #[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, sqlx::Type, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, sqlx::Type,
)] )]
@ -104,3 +139,73 @@ impl WatchQuest {
} }
} }
} }
//-************************************************************************
// error impls
//-************************************************************************
impl IntoResponse for WatchesError {
fn into_response(self) -> Response {
match self {
Self::Add(ae) => ae.into_response(),
Self::Edit(ee) => ee.into_response(),
}
}
}
impl From<AddError> for WatchesError {
fn from(value: AddError) -> Self {
WatchesError::Add(value)
}
}
impl From<AddErrorKind> for WatchesError {
fn from(value: AddErrorKind) -> Self {
let e: AddError = value.into();
e.into()
}
}
impl From<EditError> for WatchesError {
fn from(value: EditError) -> Self {
WatchesError::Edit(value)
}
}
impl From<EditErrorKind> for WatchesError {
fn from(value: EditErrorKind) -> Self {
let e: EditError = value.into();
e.into()
}
}
impl IntoResponse for EditError {
fn into_response(self) -> Response {
match &self.0 {
EditErrorKind::DBError => {
(StatusCode::INTERNAL_SERVER_ERROR, format!("{self}")).into_response()
}
EditErrorKind::NotSignedIn => (
StatusCode::OK,
"Ope, you need to sign in first!".to_string(),
)
.into_response(),
EditErrorKind::NotFound => (StatusCode::OK, "Could not find watch").into_response(),
}
}
}
impl IntoResponse for AddError {
fn into_response(self) -> Response {
match &self.0 {
AddErrorKind::DBError => {
(StatusCode::INTERNAL_SERVER_ERROR, format!("{self}")).into_response()
}
AddErrorKind::NotSignedIn => (
StatusCode::OK,
"Ope, you need to sign in first!".to_string(),
)
.into_response(),
}
}
}