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.
|
- Post pools.
|
||||||
- RSS feeds.
|
- 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
|
- [ ] Caching
|
||||||
- [ ] Text media
|
- [ ] Text media
|
||||||
- [ ] Improve CSS
|
- [ ] Improve CSS
|
||||||
- [ ] Background tasks for garbage collection (dangling tags)
|
|
||||||
- [ ] User management
|
- [ ] User management
|
||||||
|
- [ ] Display thumbnails on post selection
|
||||||
- [ ] Testing
|
- [ ] Testing
|
||||||
|
- [ ] Lossless compression
|
||||||
- [ ] Migrate to Cot...?
|
- [ ] Migrate to Cot...?
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
|
|
|
||||||
|
|
@ -221,3 +221,9 @@ impl ExpiredDeletion for SessionStorage {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<axum_login::Error<Backend>> for SameyError {
|
||||||
|
fn from(value: axum_login::Error<Backend>) -> Self {
|
||||||
|
value.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
75
src/error.rs
75
src/error.rs
|
|
@ -4,10 +4,28 @@ use axum::{
|
||||||
response::{Html, IntoResponse, Response},
|
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)]
|
#[derive(askama::Template)]
|
||||||
#[template(path = "pages/not_found.html")]
|
#[template(path = "pages/not_found.html")]
|
||||||
struct NotFoundTemplate;
|
struct NotFoundTemplate;
|
||||||
|
|
||||||
|
#[derive(askama::Template)]
|
||||||
|
#[template(path = "pages/internal_server_error.html")]
|
||||||
|
struct InternalServerErrorTemplate;
|
||||||
|
|
||||||
/// Errors from Samey.
|
/// Errors from Samey.
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum SameyError {
|
pub enum SameyError {
|
||||||
|
|
@ -54,7 +72,6 @@ pub enum SameyError {
|
||||||
|
|
||||||
impl IntoResponse for SameyError {
|
impl IntoResponse for SameyError {
|
||||||
fn into_response(self) -> Response {
|
fn into_response(self) -> Response {
|
||||||
println!("Server error - {}", &self);
|
|
||||||
match &self {
|
match &self {
|
||||||
SameyError::IntConversion(_)
|
SameyError::IntConversion(_)
|
||||||
| SameyError::IntParse(_)
|
| SameyError::IntParse(_)
|
||||||
|
|
@ -64,11 +81,37 @@ impl IntoResponse for SameyError {
|
||||||
| SameyError::Database(_)
|
| SameyError::Database(_)
|
||||||
| SameyError::Image(_)
|
| SameyError::Image(_)
|
||||||
| SameyError::Other(_) => {
|
| SameyError::Other(_) => {
|
||||||
(StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong!").into_response()
|
println!("Internal server error - {:?}", &self);
|
||||||
}
|
(
|
||||||
SameyError::Multipart(_) | SameyError::BadRequest(_) => {
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
(StatusCode::BAD_REQUEST, "Invalid request").into_response()
|
Html(
|
||||||
|
InternalServerErrorTemplate {}
|
||||||
|
.render()
|
||||||
|
.expect("shouldn't fail to render InternalServerErrorTemplate"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.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 => (
|
SameyError::NotFound => (
|
||||||
StatusCode::NOT_FOUND,
|
StatusCode::NOT_FOUND,
|
||||||
Html(
|
Html(
|
||||||
|
|
@ -78,10 +121,24 @@ impl IntoResponse for SameyError {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.into_response(),
|
.into_response(),
|
||||||
SameyError::Authentication(_) => {
|
SameyError::Authentication(_) => (
|
||||||
(StatusCode::UNAUTHORIZED, "Not authorized").into_response()
|
StatusCode::UNAUTHORIZED,
|
||||||
}
|
Html(
|
||||||
SameyError::Forbidden => (StatusCode::FORBIDDEN, "Forbidden").into_response(),
|
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 chrono::NaiveDateTime;
|
||||||
use samey_migration::{Expr, Query};
|
use samey_migration::{Expr, Query};
|
||||||
use sea_orm::{
|
use sea_orm::{
|
||||||
ColumnTrait, Condition, DatabaseConnection, EntityTrait, FromQueryResult, IntoSimpleExpr,
|
ColumnTrait, Condition, DatabaseConnection, EntityTrait, FromQueryResult, IntoIdentity,
|
||||||
QueryFilter, QueryOrder, QuerySelect, RelationTrait, Select, SelectColumns, SelectModel,
|
IntoSimpleExpr, QueryFilter, QueryOrder, QuerySelect, RelationTrait, Select, SelectColumns,
|
||||||
Selector,
|
SelectModel, Selector,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
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,
|
error::SameyError,
|
||||||
query::{
|
query::{
|
||||||
PoolPost, PostOverview, PostPoolData, filter_posts_by_user, get_pool_data_for_post,
|
PoolPost, PostOverview, PostPoolData, clean_dangling_tags, filter_posts_by_user,
|
||||||
get_posts_in_pool, get_tags_for_post, search_posts,
|
get_pool_data_for_post, get_posts_in_pool, get_tags_for_post, search_posts,
|
||||||
},
|
},
|
||||||
video::{generate_thumbnail, get_dimensions_for_video},
|
video::{generate_thumbnail, get_dimensions_for_video},
|
||||||
};
|
};
|
||||||
|
|
@ -183,24 +183,17 @@ pub(crate) async fn login(
|
||||||
mut auth_session: AuthSession,
|
mut auth_session: AuthSession,
|
||||||
Form(credentials): Form<Credentials>,
|
Form(credentials): Form<Credentials>,
|
||||||
) -> Result<impl IntoResponse, SameyError> {
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
let user = match auth_session.authenticate(credentials).await {
|
let user = match auth_session.authenticate(credentials).await? {
|
||||||
Ok(Some(user)) => user,
|
Some(user) => user,
|
||||||
Ok(None) => return Err(SameyError::Authentication("Invalid credentials".into())),
|
None => return Err(SameyError::Authentication("Invalid credentials".into())),
|
||||||
Err(_) => return Err(SameyError::Other("Auth session error".into())),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
auth_session
|
auth_session.login(&user).await?;
|
||||||
.login(&user)
|
|
||||||
.await
|
|
||||||
.map_err(|_| SameyError::Other("Login failed".into()))?;
|
|
||||||
Ok(Redirect::to("/"))
|
Ok(Redirect::to("/"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn logout(mut auth_session: AuthSession) -> Result<impl IntoResponse, SameyError> {
|
pub(crate) async fn logout(mut auth_session: AuthSession) -> Result<impl IntoResponse, SameyError> {
|
||||||
auth_session
|
auth_session.logout().await?;
|
||||||
.logout()
|
|
||||||
.await
|
|
||||||
.map_err(|_| SameyError::Other("Logout error".into()))?;
|
|
||||||
Ok(Redirect::to("/"))
|
Ok(Redirect::to("/"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -260,10 +253,9 @@ impl FromStr for Format {
|
||||||
"application/x-matroska" | "video/mastroska" => Ok(Self::Video(".mkv")),
|
"application/x-matroska" | "video/mastroska" => Ok(Self::Video(".mkv")),
|
||||||
"video/quicktime" => Ok(Self::Video(".mov")),
|
"video/quicktime" => Ok(Self::Video(".mov")),
|
||||||
_ => Ok(Self::Image(
|
_ => Ok(Self::Image(
|
||||||
ImageFormat::from_mime_type(content_type).ok_or(SameyError::Other(format!(
|
ImageFormat::from_mime_type(content_type).ok_or(SameyError::BadRequest(
|
||||||
"Unknown content type: {}",
|
format!("Unknown content type: {}", content_type),
|
||||||
content_type
|
))?,
|
||||||
)))?,
|
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -334,7 +326,7 @@ pub(crate) async fn upload(
|
||||||
"media-file" => {
|
"media-file" => {
|
||||||
let content_type = field
|
let content_type = field
|
||||||
.content_type()
|
.content_type()
|
||||||
.ok_or(SameyError::Other("Missing content type".into()))?;
|
.ok_or(SameyError::BadRequest("Missing content type".into()))?;
|
||||||
match Format::from_str(content_type)? {
|
match Format::from_str(content_type)? {
|
||||||
format @ Format::Video(video_format) => {
|
format @ Format::Video(video_format) => {
|
||||||
media_type = Some(format.media_type());
|
media_type = Some(format.media_type());
|
||||||
|
|
@ -502,7 +494,9 @@ pub(crate) async fn upload(
|
||||||
|
|
||||||
Ok(Redirect::to(&format!("/post/{}", uploaded_post)))
|
Ok(Redirect::to(&format!("/post/{}", uploaded_post)))
|
||||||
} else {
|
} 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![];
|
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(
|
let _ = mem::replace(
|
||||||
&mut app_config.write().await.application_name,
|
&mut app_config.write().await.application_name,
|
||||||
body.application_name.clone(),
|
application_name.into(),
|
||||||
);
|
);
|
||||||
configs.push(samey_config::ActiveModel {
|
configs.push(samey_config::ActiveModel {
|
||||||
key: Set(APPLICATION_NAME_KEY.into()),
|
key: Set(APPLICATION_NAME_KEY.into()),
|
||||||
data: Set(body.application_name.into()),
|
data: Set(application_name.into()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = mem::replace(
|
let base_url = body.base_url.trim_end_matches('/');
|
||||||
&mut app_config.write().await.base_url,
|
let _ = mem::replace(&mut app_config.write().await.base_url, base_url.into());
|
||||||
body.base_url.clone(),
|
|
||||||
);
|
|
||||||
configs.push(samey_config::ActiveModel {
|
configs.push(samey_config::ActiveModel {
|
||||||
key: Set(BASE_URL_KEY.into()),
|
key: Set(BASE_URL_KEY.into()),
|
||||||
data: Set(body.base_url.into()),
|
data: Set(base_url.into()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1772,6 +1765,12 @@ pub(crate) async fn submit_post_details(
|
||||||
.all(&db)
|
.all(&db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(err) = clean_dangling_tags(&db).await {
|
||||||
|
println!("Error when cleaning dangling tags - {}", err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
SubmitPostDetailsTemplate {
|
SubmitPostDetailsTemplate {
|
||||||
post,
|
post,
|
||||||
|
|
@ -1887,6 +1886,9 @@ pub(crate) async fn delete_post(
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let _ = std::fs::remove_file(files_dir.join(post.media));
|
let _ = std::fs::remove_file(files_dir.join(post.media));
|
||||||
let _ = std::fs::remove_file(files_dir.join(post.thumbnail));
|
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("/"))
|
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>
|
<li>
|
||||||
<button
|
<button
|
||||||
hx-post="/select_tag"
|
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