Better error pages and clean up dangling tags
This commit is contained in:
parent
4c1a8a9489
commit
e679d167fc
10 changed files with 189 additions and 50 deletions
12
README.md
12
README.md
|
|
@ -13,21 +13,15 @@ Still very much an early WIP.
|
|||
- Post pools.
|
||||
- RSS feeds.
|
||||
|
||||
### Known issues
|
||||
### Possible roadmap
|
||||
|
||||
- No way to close tag autocompletion on mobile.
|
||||
|
||||
### Roadmap
|
||||
|
||||
- [ ] Logging and improved error handling
|
||||
- [ ] Lossless compression
|
||||
- [ ] Post selection
|
||||
- [ ] Caching
|
||||
- [ ] Text media
|
||||
- [ ] Improve CSS
|
||||
- [ ] Background tasks for garbage collection (dangling tags)
|
||||
- [ ] User management
|
||||
- [ ] Display thumbnails on post selection
|
||||
- [ ] Testing
|
||||
- [ ] Lossless compression
|
||||
- [ ] Migrate to Cot...?
|
||||
|
||||
## Running
|
||||
|
|
|
|||
|
|
@ -221,3 +221,9 @@ impl ExpiredDeletion for SessionStorage {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<axum_login::Error<Backend>> for SameyError {
|
||||
fn from(value: axum_login::Error<Backend>) -> Self {
|
||||
value.into()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
73
src/error.rs
73
src/error.rs
|
|
@ -4,10 +4,28 @@ use axum::{
|
|||
response::{Html, IntoResponse, Response},
|
||||
};
|
||||
|
||||
#[derive(askama::Template)]
|
||||
#[template(path = "pages/bad_request.html")]
|
||||
struct BadRequestTemplate<'a> {
|
||||
error: &'a str,
|
||||
}
|
||||
|
||||
#[derive(askama::Template)]
|
||||
#[template(path = "pages/unauthorized.html")]
|
||||
struct UnauthorizedTemplate;
|
||||
|
||||
#[derive(askama::Template)]
|
||||
#[template(path = "pages/forbidden.html")]
|
||||
struct ForbiddenTemplate;
|
||||
|
||||
#[derive(askama::Template)]
|
||||
#[template(path = "pages/not_found.html")]
|
||||
struct NotFoundTemplate;
|
||||
|
||||
#[derive(askama::Template)]
|
||||
#[template(path = "pages/internal_server_error.html")]
|
||||
struct InternalServerErrorTemplate;
|
||||
|
||||
/// Errors from Samey.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum SameyError {
|
||||
|
|
@ -54,7 +72,6 @@ pub enum SameyError {
|
|||
|
||||
impl IntoResponse for SameyError {
|
||||
fn into_response(self) -> Response {
|
||||
println!("Server error - {}", &self);
|
||||
match &self {
|
||||
SameyError::IntConversion(_)
|
||||
| SameyError::IntParse(_)
|
||||
|
|
@ -64,11 +81,37 @@ impl IntoResponse for SameyError {
|
|||
| SameyError::Database(_)
|
||||
| SameyError::Image(_)
|
||||
| SameyError::Other(_) => {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong!").into_response()
|
||||
println!("Internal server error - {:?}", &self);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Html(
|
||||
InternalServerErrorTemplate {}
|
||||
.render()
|
||||
.expect("shouldn't fail to render InternalServerErrorTemplate"),
|
||||
),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
SameyError::Multipart(_) | SameyError::BadRequest(_) => {
|
||||
(StatusCode::BAD_REQUEST, "Invalid request").into_response()
|
||||
SameyError::Multipart(error) => (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Html(
|
||||
BadRequestTemplate {
|
||||
error: &error.body_text(),
|
||||
}
|
||||
.render()
|
||||
.expect("shouldn't fail to render BadRequestTemplate"),
|
||||
),
|
||||
)
|
||||
.into_response(),
|
||||
SameyError::BadRequest(error) => (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Html(
|
||||
BadRequestTemplate { error }
|
||||
.render()
|
||||
.expect("shouldn't fail to render BadRequestTemplate"),
|
||||
),
|
||||
)
|
||||
.into_response(),
|
||||
SameyError::NotFound => (
|
||||
StatusCode::NOT_FOUND,
|
||||
Html(
|
||||
|
|
@ -78,10 +121,24 @@ impl IntoResponse for SameyError {
|
|||
),
|
||||
)
|
||||
.into_response(),
|
||||
SameyError::Authentication(_) => {
|
||||
(StatusCode::UNAUTHORIZED, "Not authorized").into_response()
|
||||
}
|
||||
SameyError::Forbidden => (StatusCode::FORBIDDEN, "Forbidden").into_response(),
|
||||
SameyError::Authentication(_) => (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Html(
|
||||
UnauthorizedTemplate {}
|
||||
.render()
|
||||
.expect("shouldn't fail to render UnauthorizedTemplate"),
|
||||
),
|
||||
)
|
||||
.into_response(),
|
||||
SameyError::Forbidden => (
|
||||
StatusCode::FORBIDDEN,
|
||||
Html(
|
||||
ForbiddenTemplate {}
|
||||
.render()
|
||||
.expect("shouldn't fail to render ForbiddenTemplate"),
|
||||
),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
21
src/query.rs
21
src/query.rs
|
|
@ -3,9 +3,9 @@ use std::collections::HashSet;
|
|||
use chrono::NaiveDateTime;
|
||||
use samey_migration::{Expr, Query};
|
||||
use sea_orm::{
|
||||
ColumnTrait, Condition, DatabaseConnection, EntityTrait, FromQueryResult, IntoSimpleExpr,
|
||||
QueryFilter, QueryOrder, QuerySelect, RelationTrait, Select, SelectColumns, SelectModel,
|
||||
Selector,
|
||||
ColumnTrait, Condition, DatabaseConnection, EntityTrait, FromQueryResult, IntoIdentity,
|
||||
IntoSimpleExpr, QueryFilter, QueryOrder, QuerySelect, RelationTrait, Select, SelectColumns,
|
||||
SelectModel, Selector,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
|
|
@ -276,3 +276,18 @@ pub(crate) fn filter_posts_by_user(
|
|||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn clean_dangling_tags(db: &DatabaseConnection) -> Result<(), SameyError> {
|
||||
let dangling_tags = SameyTag::find()
|
||||
.select_column_as(samey_tag_post::Column::Id.count(), "count")
|
||||
.left_join(SameyTagPost)
|
||||
.group_by(samey_tag::Column::Id)
|
||||
.having(Expr::column("count".into_identity()).eq(0))
|
||||
.all(db)
|
||||
.await?;
|
||||
SameyTag::delete_many()
|
||||
.filter(samey_tag::Column::Id.is_in(dangling_tags.into_iter().map(|tag| tag.id)))
|
||||
.exec(db)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
58
src/views.rs
58
src/views.rs
|
|
@ -40,8 +40,8 @@ use crate::{
|
|||
},
|
||||
error::SameyError,
|
||||
query::{
|
||||
PoolPost, PostOverview, PostPoolData, filter_posts_by_user, get_pool_data_for_post,
|
||||
get_posts_in_pool, get_tags_for_post, search_posts,
|
||||
PoolPost, PostOverview, PostPoolData, clean_dangling_tags, filter_posts_by_user,
|
||||
get_pool_data_for_post, get_posts_in_pool, get_tags_for_post, search_posts,
|
||||
},
|
||||
video::{generate_thumbnail, get_dimensions_for_video},
|
||||
};
|
||||
|
|
@ -183,24 +183,17 @@ pub(crate) async fn login(
|
|||
mut auth_session: AuthSession,
|
||||
Form(credentials): Form<Credentials>,
|
||||
) -> Result<impl IntoResponse, SameyError> {
|
||||
let user = match auth_session.authenticate(credentials).await {
|
||||
Ok(Some(user)) => user,
|
||||
Ok(None) => return Err(SameyError::Authentication("Invalid credentials".into())),
|
||||
Err(_) => return Err(SameyError::Other("Auth session error".into())),
|
||||
let user = match auth_session.authenticate(credentials).await? {
|
||||
Some(user) => user,
|
||||
None => return Err(SameyError::Authentication("Invalid credentials".into())),
|
||||
};
|
||||
|
||||
auth_session
|
||||
.login(&user)
|
||||
.await
|
||||
.map_err(|_| SameyError::Other("Login failed".into()))?;
|
||||
auth_session.login(&user).await?;
|
||||
Ok(Redirect::to("/"))
|
||||
}
|
||||
|
||||
pub(crate) async fn logout(mut auth_session: AuthSession) -> Result<impl IntoResponse, SameyError> {
|
||||
auth_session
|
||||
.logout()
|
||||
.await
|
||||
.map_err(|_| SameyError::Other("Logout error".into()))?;
|
||||
auth_session.logout().await?;
|
||||
Ok(Redirect::to("/"))
|
||||
}
|
||||
|
||||
|
|
@ -260,10 +253,9 @@ impl FromStr for Format {
|
|||
"application/x-matroska" | "video/mastroska" => Ok(Self::Video(".mkv")),
|
||||
"video/quicktime" => Ok(Self::Video(".mov")),
|
||||
_ => Ok(Self::Image(
|
||||
ImageFormat::from_mime_type(content_type).ok_or(SameyError::Other(format!(
|
||||
"Unknown content type: {}",
|
||||
content_type
|
||||
)))?,
|
||||
ImageFormat::from_mime_type(content_type).ok_or(SameyError::BadRequest(
|
||||
format!("Unknown content type: {}", content_type),
|
||||
))?,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
|
@ -334,7 +326,7 @@ pub(crate) async fn upload(
|
|||
"media-file" => {
|
||||
let content_type = field
|
||||
.content_type()
|
||||
.ok_or(SameyError::Other("Missing content type".into()))?;
|
||||
.ok_or(SameyError::BadRequest("Missing content type".into()))?;
|
||||
match Format::from_str(content_type)? {
|
||||
format @ Format::Video(video_format) => {
|
||||
media_type = Some(format.media_type());
|
||||
|
|
@ -502,7 +494,9 @@ pub(crate) async fn upload(
|
|||
|
||||
Ok(Redirect::to(&format!("/post/{}", uploaded_post)))
|
||||
} else {
|
||||
Err(SameyError::Other("Missing parameters for upload".into()))
|
||||
Err(SameyError::BadRequest(
|
||||
"Missing parameters for upload".into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1376,25 +1370,24 @@ pub(crate) async fn update_settings(
|
|||
|
||||
let mut configs = vec![];
|
||||
|
||||
if !body.application_name.is_empty() {
|
||||
let application_name = body.application_name.trim();
|
||||
if !application_name.is_empty() {
|
||||
let _ = mem::replace(
|
||||
&mut app_config.write().await.application_name,
|
||||
body.application_name.clone(),
|
||||
application_name.into(),
|
||||
);
|
||||
configs.push(samey_config::ActiveModel {
|
||||
key: Set(APPLICATION_NAME_KEY.into()),
|
||||
data: Set(body.application_name.into()),
|
||||
data: Set(application_name.into()),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
let _ = mem::replace(
|
||||
&mut app_config.write().await.base_url,
|
||||
body.base_url.clone(),
|
||||
);
|
||||
let base_url = body.base_url.trim_end_matches('/');
|
||||
let _ = mem::replace(&mut app_config.write().await.base_url, base_url.into());
|
||||
configs.push(samey_config::ActiveModel {
|
||||
key: Set(BASE_URL_KEY.into()),
|
||||
data: Set(body.base_url.into()),
|
||||
data: Set(base_url.into()),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
|
|
@ -1772,6 +1765,12 @@ pub(crate) async fn submit_post_details(
|
|||
.all(&db)
|
||||
.await?;
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = clean_dangling_tags(&db).await {
|
||||
println!("Error when cleaning dangling tags - {}", err);
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Html(
|
||||
SubmitPostDetailsTemplate {
|
||||
post,
|
||||
|
|
@ -1887,6 +1886,9 @@ pub(crate) async fn delete_post(
|
|||
tokio::spawn(async move {
|
||||
let _ = std::fs::remove_file(files_dir.join(post.media));
|
||||
let _ = std::fs::remove_file(files_dir.join(post.thumbnail));
|
||||
if let Err(err) = clean_dangling_tags(&db).await {
|
||||
println!("Error when cleaning dangling tags - {}", err);
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Redirect::to("/"))
|
||||
|
|
|
|||
|
|
@ -1,4 +1,10 @@
|
|||
{% for tag in tags %}
|
||||
{% if !tags.is_empty() %}
|
||||
<li>
|
||||
<button hx-delete="/remove" hx-target="closest ul" hx-swap="innerHTML">
|
||||
(close)
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}{% for tag in tags %}
|
||||
<li>
|
||||
<button
|
||||
hx-post="/select_tag"
|
||||
|
|
|
|||
14
templates/pages/bad_request.html
Normal file
14
templates/pages/bad_request.html
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Bad request</title>
|
||||
{% include "fragments/common_headers.html" %}
|
||||
</head>
|
||||
<body>
|
||||
<div><a href="/">< To home</a></div>
|
||||
<main>
|
||||
<h1>Bad request</h1>
|
||||
<p>The provided parameters are invalid: {{ error }}</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
14
templates/pages/forbidden.html
Normal file
14
templates/pages/forbidden.html
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Forbidden</title>
|
||||
{% include "fragments/common_headers.html" %}
|
||||
</head>
|
||||
<body>
|
||||
<div><a href="/">< To home</a></div>
|
||||
<main>
|
||||
<h1>Forbidden</h1>
|
||||
<p>The requested action is not allowed.</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
17
templates/pages/internal_server_error.html
Normal file
17
templates/pages/internal_server_error.html
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Internal server error</title>
|
||||
{% include "fragments/common_headers.html" %}
|
||||
</head>
|
||||
<body>
|
||||
<div><a href="/">< To home</a></div>
|
||||
<main>
|
||||
<h1>Internal server error</h1>
|
||||
<p>
|
||||
Something went wrong! Ask your administrator to check the application
|
||||
logs.
|
||||
</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
14
templates/pages/unauthorized.html
Normal file
14
templates/pages/unauthorized.html
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Unauthorized</title>
|
||||
{% include "fragments/common_headers.html" %}
|
||||
</head>
|
||||
<body>
|
||||
<div><a href="/">< To home</a></div>
|
||||
<main>
|
||||
<h1>Unauthorized</h1>
|
||||
<p>The provided credentials are invalid.</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue