Added image compression
Build and Publish / Build and Publish Docker Image (push) Successful in 1m59s
Build and Publish / Build and Publish Docker Image (push) Successful in 1m59s
This commit is contained in:
+97
-37
@@ -6,7 +6,12 @@ use cot::request::extractors::Path;
|
||||
use cot::response::{IntoResponse, Redirect, Response};
|
||||
use cot::router::{Route, Router};
|
||||
use cot::session::Session;
|
||||
use image::ImageFormat;
|
||||
use image::ImageReader;
|
||||
use image::codecs::jpeg::JpegEncoder;
|
||||
use image::imageops::FilterType;
|
||||
use serde::Deserialize;
|
||||
use std::io::Cursor;
|
||||
|
||||
use crate::i18n::{Lang, Translations};
|
||||
use crate::models::{Client, Lead, Media, Setting, Testimonial, User, Visit};
|
||||
@@ -14,6 +19,8 @@ use crate::telegram;
|
||||
|
||||
const SESSION_USER_ID: &str = "user_id";
|
||||
const SESSION_USER_NAME: &str = "user_name";
|
||||
const MAX_UPLOADED_IMAGE_DIMENSION: u32 = 1920;
|
||||
const UPLOADED_IMAGE_JPEG_QUALITY: u8 = 82;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
@@ -91,12 +98,64 @@ fn has_query_flag(request: &Request, flag: &str) -> bool {
|
||||
fn get_query_param(request: &Request, key: &str) -> Option<String> {
|
||||
let prefix = format!("{}=", key);
|
||||
request.uri().query().and_then(|q| {
|
||||
q.split('&').find_map(|p| {
|
||||
p.strip_prefix(&prefix).map(|v| v.to_string())
|
||||
})
|
||||
q.split('&')
|
||||
.find_map(|p| p.strip_prefix(&prefix).map(|v| v.to_string()))
|
||||
})
|
||||
}
|
||||
|
||||
fn image_format_from_ext(ext: &str) -> Option<ImageFormat> {
|
||||
match ext {
|
||||
"jpg" | "jpeg" => Some(ImageFormat::Jpeg),
|
||||
"png" => Some(ImageFormat::Png),
|
||||
"webp" => Some(ImageFormat::WebP),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn transcode_uploaded_image(data: &[u8], ext: &str) -> cot::Result<Option<Vec<u8>>> {
|
||||
let Some(format) = image_format_from_ext(ext) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let image = ImageReader::with_format(Cursor::new(data), format)
|
||||
.decode()
|
||||
.map_err(|e| cot::Error::internal(e.to_string()))?;
|
||||
let resized = image.resize(
|
||||
MAX_UPLOADED_IMAGE_DIMENSION,
|
||||
MAX_UPLOADED_IMAGE_DIMENSION,
|
||||
FilterType::Lanczos3,
|
||||
);
|
||||
let rgb = resized.to_rgb8();
|
||||
let mut encoded = Vec::new();
|
||||
let mut encoder = JpegEncoder::new_with_quality(&mut encoded, UPLOADED_IMAGE_JPEG_QUALITY);
|
||||
encoder
|
||||
.encode_image(&rgb)
|
||||
.map_err(|e| cot::Error::internal(e.to_string()))?;
|
||||
|
||||
Ok(Some(encoded))
|
||||
}
|
||||
|
||||
async fn save_uploaded_image(
|
||||
upload_dir: &str,
|
||||
file_id: uuid::Uuid,
|
||||
ext: &str,
|
||||
data: &[u8],
|
||||
) -> cot::Result<String> {
|
||||
if let Some(encoded) = transcode_uploaded_image(data, ext)? {
|
||||
let path = format!("{}/{}.jpg", upload_dir, file_id);
|
||||
tokio::fs::write(&path, &encoded)
|
||||
.await
|
||||
.map_err(|e| cot::Error::internal(e.to_string()))?;
|
||||
Ok(path)
|
||||
} else {
|
||||
let path = format!("{}/{}.{}", upload_dir, file_id, ext);
|
||||
tokio::fs::write(&path, data)
|
||||
.await
|
||||
.map_err(|e| cot::Error::internal(e.to_string()))?;
|
||||
Ok(path)
|
||||
}
|
||||
}
|
||||
|
||||
/// Soft pastel palette for client calendar colors.
|
||||
const CLIENT_COLORS: &[&str] = &[
|
||||
"#7c6ed4", "#5b9bd5", "#4caf93", "#e0915e", "#d46c8e", "#8e6bbf", "#5cb8a5", "#c77c4f",
|
||||
@@ -588,24 +647,27 @@ async fn oidc_start(request: Request, db: Database) -> cot::Result<Response> {
|
||||
let site_domain = oidc_setting(&db, "site_domain").await?;
|
||||
|
||||
if issuer_url.trim().is_empty() || client_id.trim().is_empty() {
|
||||
return Redirect::new(format!("/admin/login?lang={}&error=sso_provider", lang.code()))
|
||||
.into_response();
|
||||
return Redirect::new(format!(
|
||||
"/admin/login?lang={}&error=sso_provider",
|
||||
lang.code()
|
||||
))
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let authorization_endpoint = match oidc_discover(&issuer_url, "authorization_endpoint").await {
|
||||
Some(ep) => ep,
|
||||
None => {
|
||||
return Redirect::new(format!("/admin/login?lang={}&error=sso_provider", lang.code()))
|
||||
.into_response();
|
||||
return Redirect::new(format!(
|
||||
"/admin/login?lang={}&error=sso_provider",
|
||||
lang.code()
|
||||
))
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let state = rand_token();
|
||||
|
||||
let redirect_uri = format!(
|
||||
"{}/admin/oidc/callback",
|
||||
site_domain.trim_end_matches('/')
|
||||
);
|
||||
let redirect_uri = format!("{}/admin/oidc/callback", site_domain.trim_end_matches('/'));
|
||||
|
||||
let redirect_url = format!(
|
||||
"{}?response_type=code&client_id={}&redirect_uri={}&scope=openid+profile&state={}",
|
||||
@@ -678,10 +740,7 @@ async fn oidc_callback(request: Request, session: Session, db: Database) -> cot:
|
||||
}
|
||||
};
|
||||
|
||||
let redirect_uri = format!(
|
||||
"{}/admin/oidc/callback",
|
||||
site_domain.trim_end_matches('/')
|
||||
);
|
||||
let redirect_uri = format!("{}/admin/oidc/callback", site_domain.trim_end_matches('/'));
|
||||
|
||||
// Exchange code for tokens
|
||||
let token_resp = reqwest::Client::new()
|
||||
@@ -743,7 +802,11 @@ async fn oidc_callback(request: Request, session: Session, db: Database) -> cot:
|
||||
// Check group membership
|
||||
let allowed_groups = oidc_setting(&db, "oidc_allowed_groups").await?;
|
||||
if !allowed_groups.trim().is_empty() {
|
||||
let required: Vec<&str> = allowed_groups.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()).collect();
|
||||
let required: Vec<&str> = allowed_groups
|
||||
.split(',')
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
let user_groups: Vec<String> = claims
|
||||
.get("groups")
|
||||
.and_then(|v| v.as_array())
|
||||
@@ -755,9 +818,9 @@ async fn oidc_callback(request: Request, session: Session, db: Database) -> cot:
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let has_group = required.iter().any(|r| {
|
||||
user_groups.iter().any(|ug| ug.eq_ignore_ascii_case(r))
|
||||
});
|
||||
let has_group = required
|
||||
.iter()
|
||||
.any(|r| user_groups.iter().any(|ug| ug.eq_ignore_ascii_case(r)));
|
||||
|
||||
if !has_group {
|
||||
tracing::warn!(
|
||||
@@ -811,16 +874,16 @@ async fn oidc_callback(request: Request, session: Session, db: Database) -> cot:
|
||||
return Redirect::new(fail("sso_disabled")).into_response();
|
||||
}
|
||||
|
||||
let display = user
|
||||
let session_name = user
|
||||
.display_name
|
||||
.as_deref()
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or(&user.login)
|
||||
.to_string();
|
||||
|
||||
tracing::info!(target: "oidc", username = %user.login, display = %display, "SSO login: session established");
|
||||
tracing::info!(target: "oidc", username = %user.login, display_name = %session_name, "SSO login: session established");
|
||||
session.insert(SESSION_USER_ID, user.id.unwrap()).await?;
|
||||
session.insert(SESSION_USER_NAME, display).await?;
|
||||
session.insert(SESSION_USER_NAME, session_name).await?;
|
||||
|
||||
// Clear the oidc_state cookie
|
||||
let clear_cookie = "oidc_state=; Path=/admin/oidc; HttpOnly; Secure; SameSite=Lax; Max-Age=0";
|
||||
@@ -868,9 +931,7 @@ async fn admin_index(request: Request, session: Session, db: Database) -> cot::R
|
||||
|
||||
let mut all_feedbacks: Vec<RecentFeedback> = all_visits
|
||||
.iter()
|
||||
.filter(|v| {
|
||||
v.user_id.primary_key().unwrap() == user_id && v.client_feedback.is_some()
|
||||
})
|
||||
.filter(|v| v.user_id.primary_key().unwrap() == user_id && v.client_feedback.is_some())
|
||||
.map(|v| {
|
||||
let cid: i64 = v.client_id.primary_key().unwrap();
|
||||
let client_name = clients
|
||||
@@ -2008,7 +2069,6 @@ async fn media_upload_submit(
|
||||
};
|
||||
|
||||
let file_id = uuid::Uuid::new_v4();
|
||||
let file_path = format!("{}/{}.{}", upload_dir, file_id, ext);
|
||||
|
||||
let data = field
|
||||
.bytes()
|
||||
@@ -2017,9 +2077,15 @@ async fn media_upload_submit(
|
||||
if data.is_empty() {
|
||||
continue;
|
||||
}
|
||||
tokio::fs::write(&file_path, &data)
|
||||
.await
|
||||
.map_err(|e| cot::Error::internal(e.to_string()))?;
|
||||
let file_path = if file_type == "photo" {
|
||||
save_uploaded_image(&upload_dir, file_id, &ext, &data).await?
|
||||
} else {
|
||||
let path = format!("{}/{}.{}", upload_dir, file_id, ext);
|
||||
tokio::fs::write(&path, &data)
|
||||
.await
|
||||
.map_err(|e| cot::Error::internal(e.to_string()))?;
|
||||
path
|
||||
};
|
||||
|
||||
saved_files.push((file_path, file_type.to_string()));
|
||||
}
|
||||
@@ -2229,10 +2295,7 @@ async fn testimonial_add(
|
||||
.await
|
||||
.map_err(|e| cot::Error::internal(e.to_string()))?;
|
||||
let file_id = uuid::Uuid::new_v4();
|
||||
let path = format!("{}/{}.{}", upload_dir, file_id, ext);
|
||||
tokio::fs::write(&path, &data)
|
||||
.await
|
||||
.map_err(|e| cot::Error::internal(e.to_string()))?;
|
||||
let path = save_uploaded_image(upload_dir, file_id, &ext, &data).await?;
|
||||
image_path = Some(path);
|
||||
}
|
||||
_ => {}
|
||||
@@ -2380,10 +2443,7 @@ async fn testimonial_edit(
|
||||
.await
|
||||
.map_err(|e| cot::Error::internal(e.to_string()))?;
|
||||
let file_id = uuid::Uuid::new_v4();
|
||||
let path = format!("{}/{}.{}", upload_dir, file_id, ext);
|
||||
tokio::fs::write(&path, &data)
|
||||
.await
|
||||
.map_err(|e| cot::Error::internal(e.to_string()))?;
|
||||
let path = save_uploaded_image(upload_dir, file_id, &ext, &data).await?;
|
||||
new_image_path = Some(path);
|
||||
}
|
||||
_ => {}
|
||||
|
||||
Reference in New Issue
Block a user