feat: ship blog platform admin and deploy stack
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
#![allow(clippy::unnecessary_struct_initialization)]
|
||||
#![allow(clippy::unused_async)]
|
||||
|
||||
use axum::http::HeaderMap;
|
||||
use loco_rs::prelude::*;
|
||||
use sea_orm::{ActiveModelTrait, EntityTrait, IntoActiveModel, QueryOrder, Set};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -11,7 +12,9 @@ use uuid::Uuid;
|
||||
use crate::{
|
||||
controllers::admin::check_auth,
|
||||
models::_entities::{
|
||||
categories, friend_links, posts, site_settings::{self, ActiveModel, Entity, Model}, tags,
|
||||
categories, friend_links, posts,
|
||||
site_settings::{self, ActiveModel, Entity, Model},
|
||||
tags,
|
||||
},
|
||||
services::{ai, content},
|
||||
};
|
||||
@@ -130,6 +133,18 @@ pub struct SiteSettingsPayload {
|
||||
pub media_r2_access_key_id: Option<String>,
|
||||
#[serde(default, alias = "mediaR2SecretAccessKey")]
|
||||
pub media_r2_secret_access_key: Option<String>,
|
||||
#[serde(default, alias = "seoDefaultOgImage")]
|
||||
pub seo_default_og_image: Option<String>,
|
||||
#[serde(default, alias = "seoDefaultTwitterHandle")]
|
||||
pub seo_default_twitter_handle: Option<String>,
|
||||
#[serde(default, alias = "notificationWebhookUrl")]
|
||||
pub notification_webhook_url: Option<String>,
|
||||
#[serde(default, alias = "notificationCommentEnabled")]
|
||||
pub notification_comment_enabled: Option<bool>,
|
||||
#[serde(default, alias = "notificationFriendLinkEnabled")]
|
||||
pub notification_friend_link_enabled: Option<bool>,
|
||||
#[serde(default, alias = "searchSynonyms")]
|
||||
pub search_synonyms: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
@@ -154,6 +169,8 @@ pub struct PublicSiteSettingsResponse {
|
||||
pub music_playlist: Option<serde_json::Value>,
|
||||
pub ai_enabled: bool,
|
||||
pub paragraph_comments_enabled: bool,
|
||||
pub seo_default_og_image: Option<String>,
|
||||
pub seo_default_twitter_handle: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
@@ -171,6 +188,9 @@ pub struct HomePageResponse {
|
||||
pub tags: Vec<tags::Model>,
|
||||
pub friend_links: Vec<friend_links::Model>,
|
||||
pub categories: Vec<HomeCategorySummary>,
|
||||
pub content_overview: crate::services::analytics::ContentAnalyticsOverview,
|
||||
pub popular_posts: Vec<crate::services::analytics::AnalyticsPopularPost>,
|
||||
pub content_ranges: Vec<crate::services::analytics::PublicContentWindowHighlights>,
|
||||
}
|
||||
|
||||
fn normalize_optional_string(value: Option<String>) -> Option<String> {
|
||||
@@ -188,6 +208,13 @@ fn normalize_optional_int(value: Option<i32>, min: i32, max: i32) -> Option<i32>
|
||||
value.map(|item| item.clamp(min, max))
|
||||
}
|
||||
|
||||
fn normalize_string_list(values: Vec<String>) -> Vec<String> {
|
||||
values
|
||||
.into_iter()
|
||||
.filter_map(|item| normalize_optional_string(Some(item)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn create_ai_provider_id() -> String {
|
||||
format!("provider-{}", Uuid::new_v4().simple())
|
||||
}
|
||||
@@ -525,6 +552,27 @@ impl SiteSettingsPayload {
|
||||
item.media_r2_secret_access_key =
|
||||
normalize_optional_string(Some(media_r2_secret_access_key));
|
||||
}
|
||||
if let Some(seo_default_og_image) = self.seo_default_og_image {
|
||||
item.seo_default_og_image = normalize_optional_string(Some(seo_default_og_image));
|
||||
}
|
||||
if let Some(seo_default_twitter_handle) = self.seo_default_twitter_handle {
|
||||
item.seo_default_twitter_handle =
|
||||
normalize_optional_string(Some(seo_default_twitter_handle));
|
||||
}
|
||||
if let Some(notification_webhook_url) = self.notification_webhook_url {
|
||||
item.notification_webhook_url =
|
||||
normalize_optional_string(Some(notification_webhook_url));
|
||||
}
|
||||
if let Some(notification_comment_enabled) = self.notification_comment_enabled {
|
||||
item.notification_comment_enabled = Some(notification_comment_enabled);
|
||||
}
|
||||
if let Some(notification_friend_link_enabled) = self.notification_friend_link_enabled {
|
||||
item.notification_friend_link_enabled = Some(notification_friend_link_enabled);
|
||||
}
|
||||
if let Some(search_synonyms) = self.search_synonyms {
|
||||
let normalized = normalize_string_list(search_synonyms);
|
||||
item.search_synonyms = (!normalized.is_empty()).then(|| serde_json::json!(normalized));
|
||||
}
|
||||
|
||||
if provider_list_supplied {
|
||||
write_ai_provider_state(
|
||||
@@ -631,6 +679,12 @@ fn default_payload() -> SiteSettingsPayload {
|
||||
media_r2_public_base_url: None,
|
||||
media_r2_access_key_id: None,
|
||||
media_r2_secret_access_key: None,
|
||||
seo_default_og_image: None,
|
||||
seo_default_twitter_handle: None,
|
||||
notification_webhook_url: None,
|
||||
notification_comment_enabled: Some(false),
|
||||
notification_friend_link_enabled: Some(false),
|
||||
search_synonyms: Some(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -680,6 +734,8 @@ fn public_response(model: Model) -> PublicSiteSettingsResponse {
|
||||
music_playlist: model.music_playlist,
|
||||
ai_enabled: model.ai_enabled.unwrap_or(false),
|
||||
paragraph_comments_enabled: model.paragraph_comments_enabled.unwrap_or(true),
|
||||
seo_default_og_image: model.seo_default_og_image,
|
||||
seo_default_twitter_handle: model.seo_default_twitter_handle,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -691,9 +747,13 @@ pub async fn home(State(ctx): State<AppContext>) -> Result<Response> {
|
||||
let posts = posts::Entity::find()
|
||||
.order_by_desc(posts::Column::CreatedAt)
|
||||
.all(&ctx.db)
|
||||
.await?;
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|post| content::is_post_listed_publicly(post, chrono::Utc::now().fixed_offset()))
|
||||
.collect::<Vec<_>>();
|
||||
let tags = tags::Entity::find().all(&ctx.db).await?;
|
||||
let friend_links = friend_links::Entity::find()
|
||||
.filter(friend_links::Column::Status.eq("approved"))
|
||||
.order_by_desc(friend_links::Column::CreatedAt)
|
||||
.all(&ctx.db)
|
||||
.await?;
|
||||
@@ -722,6 +782,9 @@ pub async fn home(State(ctx): State<AppContext>) -> Result<Response> {
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let content_highlights =
|
||||
crate::services::analytics::build_public_content_highlights(&ctx, &posts).await?;
|
||||
let content_ranges = crate::services::analytics::build_public_content_windows(&ctx, &posts).await?;
|
||||
|
||||
format::json(HomePageResponse {
|
||||
site_settings,
|
||||
@@ -729,6 +792,9 @@ pub async fn home(State(ctx): State<AppContext>) -> Result<Response> {
|
||||
tags,
|
||||
friend_links,
|
||||
categories,
|
||||
content_overview: content_highlights.overview,
|
||||
popular_posts: content_highlights.popular_posts,
|
||||
content_ranges,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -739,10 +805,11 @@ pub async fn show(State(ctx): State<AppContext>) -> Result<Response> {
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn update(
|
||||
headers: HeaderMap,
|
||||
State(ctx): State<AppContext>,
|
||||
Json(params): Json<SiteSettingsPayload>,
|
||||
) -> Result<Response> {
|
||||
check_auth()?;
|
||||
check_auth(&headers)?;
|
||||
|
||||
let current = load_current(&ctx).await?;
|
||||
let mut item = current;
|
||||
|
||||
Reference in New Issue
Block a user