pinakes-server: update remaining route imports and handlers

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I67206fd813d514f8903041eea0a4cd266a6a6964
This commit is contained in:
raf 2026-03-08 00:42:20 +03:00
commit eb6c0a3577
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
20 changed files with 169 additions and 87 deletions

View file

@ -4,7 +4,11 @@ use axum::{
}; };
use pinakes_core::model::Pagination; use pinakes_core::model::Pagination;
use crate::{dto::*, error::ApiError, state::AppState}; use crate::{
dto::{AuditEntryResponse, PaginationParams},
error::ApiError,
state::AppState,
};
pub async fn list_audit( pub async fn list_audit(
State(state): State<AppState>, State(state): State<AppState>,

View file

@ -5,7 +5,16 @@ use axum::{
use pinakes_core::model::{CollectionKind, MediaId}; use pinakes_core::model::{CollectionKind, MediaId};
use uuid::Uuid; use uuid::Uuid;
use crate::{dto::*, error::ApiError, state::AppState}; use crate::{
dto::{
AddMemberRequest,
CollectionResponse,
CreateCollectionRequest,
MediaResponse,
},
error::ApiError,
state::AppState,
};
pub async fn create_collection( pub async fn create_collection(
State(state): State<AppState>, State(state): State<AppState>,

View file

@ -1,6 +1,18 @@
use axum::{Json, extract::State}; use axum::{Json, extract::State};
use crate::{dto::*, error::ApiError, state::AppState}; use crate::{
dto::{
ConfigResponse,
RootDirRequest,
ScanningConfigResponse,
ServerConfigResponse,
UiConfigResponse,
UpdateScanningRequest,
UpdateUiConfigRequest,
},
error::ApiError,
state::AppState,
};
pub async fn get_config( pub async fn get_config(
State(state): State<AppState>, State(state): State<AppState>,
@ -15,18 +27,11 @@ pub async fn get_config(
let config_writable = match &state.config_path { let config_writable = match &state.config_path {
Some(path) => { Some(path) => {
if path.exists() { if path.exists() {
std::fs::metadata(path) std::fs::metadata(path).is_ok_and(|m| !m.permissions().readonly())
.map(|m| !m.permissions().readonly())
.unwrap_or(false)
} else { } else {
path path.parent().is_some_and(|parent| {
.parent() std::fs::metadata(parent).is_ok_and(|m| !m.permissions().readonly())
.map(|parent| { })
std::fs::metadata(parent)
.map(|m| !m.permissions().readonly())
.unwrap_or(false)
})
.unwrap_or(false)
} }
}, },
None => false, None => false,
@ -128,18 +133,11 @@ pub async fn update_scanning_config(
let config_writable = match &state.config_path { let config_writable = match &state.config_path {
Some(path) => { Some(path) => {
if path.exists() { if path.exists() {
std::fs::metadata(path) std::fs::metadata(path).is_ok_and(|m| !m.permissions().readonly())
.map(|m| !m.permissions().readonly())
.unwrap_or(false)
} else { } else {
path path.parent().is_some_and(|parent| {
.parent() std::fs::metadata(parent).is_ok_and(|m| !m.permissions().readonly())
.map(|parent| { })
std::fs::metadata(parent)
.map(|m| !m.permissions().readonly())
.unwrap_or(false)
})
.unwrap_or(false)
} }
}, },
None => false, None => false,

View file

@ -5,7 +5,11 @@ use axum::{
use pinakes_core::model::MediaId; use pinakes_core::model::MediaId;
use uuid::Uuid; use uuid::Uuid;
use crate::{dto::*, error::ApiError, state::AppState}; use crate::{
dto::{BatchDeleteRequest, ExternalMetadataResponse},
error::ApiError,
state::AppState,
};
pub async fn trigger_enrichment( pub async fn trigger_enrichment(
State(state): State<AppState>, State(state): State<AppState>,

View file

@ -65,7 +65,7 @@ pub async fn health(State(state): State<AppState>) -> Json<HealthResponse> {
Err(e) => { Err(e) => {
response.status = "degraded".to_string(); response.status = "degraded".to_string();
DatabaseHealth { DatabaseHealth {
status: format!("error: {}", e), status: format!("error: {e}"),
latency_ms: db_start.elapsed().as_millis() as u64, latency_ms: db_start.elapsed().as_millis() as u64,
media_count: None, media_count: None,
} }
@ -168,7 +168,7 @@ pub async fn health_detailed(
let db_start = Instant::now(); let db_start = Instant::now();
let (db_status, media_count) = match state.storage.count_media().await { let (db_status, media_count) = match state.storage.count_media().await {
Ok(count) => ("ok".to_string(), Some(count)), Ok(count) => ("ok".to_string(), Some(count)),
Err(e) => (format!("error: {}", e), None), Err(e) => (format!("error: {e}"), None),
}; };
let db_latency = db_start.elapsed().as_millis() as u64; let db_latency = db_start.elapsed().as_millis() as u64;

View file

@ -56,7 +56,7 @@ pub async fn generate_all_thumbnails(
State(state): State<AppState>, State(state): State<AppState>,
body: Option<Json<GenerateThumbnailsRequest>>, body: Option<Json<GenerateThumbnailsRequest>>,
) -> Result<Json<serde_json::Value>, ApiError> { ) -> Result<Json<serde_json::Value>, ApiError> {
let only_missing = body.map(|b| b.only_missing).unwrap_or(false); let only_missing = body.is_some_and(|b| b.only_missing);
let media_ids = state let media_ids = state
.storage .storage
.list_media_ids_for_thumbnails(only_missing) .list_media_ids_for_thumbnails(only_missing)

View file

@ -175,7 +175,7 @@ pub struct GraphQuery {
pub depth: u32, pub depth: u32,
} }
fn default_depth() -> u32 { const fn default_depth() -> u32 {
2 2
} }
@ -280,7 +280,7 @@ pub async fn reindex_links(
// Read the file content // Read the file content
let content = tokio::fs::read_to_string(&media.path) let content = tokio::fs::read_to_string(&media.path)
.await .await
.map_err(|e| ApiError::internal(format!("Failed to read file: {}", e)))?; .map_err(|e| ApiError::internal(format!("Failed to read file: {e}")))?;
// Extract links // Extract links
let links = pinakes_core::links::extract_links(media_id, &content); let links = pinakes_core::links::extract_links(media_id, &content);

View file

@ -33,7 +33,7 @@ pub struct TimelineQuery {
pub limit: u64, pub limit: u64,
} }
fn default_timeline_limit() -> u64 { const fn default_timeline_limit() -> u64 {
10000 10000
} }

View file

@ -5,7 +5,19 @@ use axum::{
use pinakes_core::{model::MediaId, playlists::Playlist, users::UserId}; use pinakes_core::{model::MediaId, playlists::Playlist, users::UserId};
use uuid::Uuid; use uuid::Uuid;
use crate::{auth::resolve_user_id, dto::*, error::ApiError, state::AppState}; use crate::{
auth::resolve_user_id,
dto::{
CreatePlaylistRequest,
MediaResponse,
PlaylistItemRequest,
PlaylistResponse,
ReorderPlaylistRequest,
UpdatePlaylistRequest,
},
error::ApiError,
state::AppState,
};
/// Check whether a user has access to a playlist. /// Check whether a user has access to a playlist.
/// ///
@ -138,12 +150,11 @@ pub async fn add_item(
) -> Result<Json<serde_json::Value>, ApiError> { ) -> Result<Json<serde_json::Value>, ApiError> {
let user_id = resolve_user_id(&state.storage, &username).await?; let user_id = resolve_user_id(&state.storage, &username).await?;
check_playlist_access(&state.storage, id, user_id, true).await?; check_playlist_access(&state.storage, id, user_id, true).await?;
let position = match req.position { let position = if let Some(p) = req.position {
Some(p) => p, p
None => { } else {
let items = state.storage.get_playlist_items(id).await?; let items = state.storage.get_playlist_items(id).await?;
items.len() as i32 items.len() as i32
},
}; };
state state
.storage .storage

View file

@ -3,7 +3,11 @@ use axum::{
extract::{Path, State}, extract::{Path, State},
}; };
use crate::{dto::*, error::ApiError, state::AppState}; use crate::{
dto::{InstallPluginRequest, PluginResponse, TogglePluginRequest},
error::ApiError,
state::AppState,
};
/// List all installed plugins /// List all installed plugins
pub async fn list_plugins( pub async fn list_plugins(
@ -37,8 +41,7 @@ pub async fn get_plugin(
let plugin = plugin_manager.get_plugin(&id).await.ok_or_else(|| { let plugin = plugin_manager.get_plugin(&id).await.ok_or_else(|| {
ApiError(pinakes_core::error::PinakesError::NotFound(format!( ApiError(pinakes_core::error::PinakesError::NotFound(format!(
"Plugin not found: {}", "Plugin not found: {id}"
id
))) )))
})?; })?;
@ -63,7 +66,7 @@ pub async fn install_plugin(
.await .await
.map_err(|e| { .map_err(|e| {
ApiError(pinakes_core::error::PinakesError::InvalidOperation( ApiError(pinakes_core::error::PinakesError::InvalidOperation(
format!("Failed to install plugin: {}", e), format!("Failed to install plugin: {e}"),
)) ))
})?; })?;
@ -91,7 +94,7 @@ pub async fn uninstall_plugin(
plugin_manager.uninstall_plugin(&id).await.map_err(|e| { plugin_manager.uninstall_plugin(&id).await.map_err(|e| {
ApiError(pinakes_core::error::PinakesError::InvalidOperation( ApiError(pinakes_core::error::PinakesError::InvalidOperation(
format!("Failed to uninstall plugin: {}", e), format!("Failed to uninstall plugin: {e}"),
)) ))
})?; })?;
@ -113,13 +116,13 @@ pub async fn toggle_plugin(
if req.enabled { if req.enabled {
plugin_manager.enable_plugin(&id).await.map_err(|e| { plugin_manager.enable_plugin(&id).await.map_err(|e| {
ApiError(pinakes_core::error::PinakesError::InvalidOperation( ApiError(pinakes_core::error::PinakesError::InvalidOperation(
format!("Failed to enable plugin: {}", e), format!("Failed to enable plugin: {e}"),
)) ))
})?; })?;
} else { } else {
plugin_manager.disable_plugin(&id).await.map_err(|e| { plugin_manager.disable_plugin(&id).await.map_err(|e| {
ApiError(pinakes_core::error::PinakesError::InvalidOperation( ApiError(pinakes_core::error::PinakesError::InvalidOperation(
format!("Failed to disable plugin: {}", e), format!("Failed to disable plugin: {e}"),
)) ))
})?; })?;
} }
@ -143,7 +146,7 @@ pub async fn reload_plugin(
plugin_manager.reload_plugin(&id).await.map_err(|e| { plugin_manager.reload_plugin(&id).await.map_err(|e| {
ApiError(pinakes_core::error::PinakesError::InvalidOperation( ApiError(pinakes_core::error::PinakesError::InvalidOperation(
format!("Failed to reload plugin: {}", e), format!("Failed to reload plugin: {e}"),
)) ))
})?; })?;

View file

@ -1,6 +1,10 @@
use axum::{Json, extract::State}; use axum::{Json, extract::State};
use crate::{dto::*, error::ApiError, state::AppState}; use crate::{
dto::{ScanJobResponse, ScanRequest, ScanStatusResponse},
error::ApiError,
state::AppState,
};
/// Trigger a scan as a background job. Returns the job ID immediately. /// Trigger a scan as a background job. Returns the job ID immediately.
pub async fn trigger_scan( pub async fn trigger_scan(

View file

@ -7,7 +7,11 @@ use pinakes_core::{
search::{SearchRequest, SortOrder, parse_search_query}, search::{SearchRequest, SortOrder, parse_search_query},
}; };
use crate::{dto::*, error::ApiError, state::AppState}; use crate::{
dto::{MediaResponse, SearchParams, SearchRequestBody, SearchResponse},
error::ApiError,
state::AppState,
};
fn resolve_sort(sort: Option<&str>) -> SortOrder { fn resolve_sort(sort: Option<&str>) -> SortOrder {
match sort { match sort {

View file

@ -6,7 +6,21 @@ use pinakes_core::model::{MediaId, Pagination};
use serde::Deserialize; use serde::Deserialize;
use uuid::Uuid; use uuid::Uuid;
use crate::{auth::resolve_user_id, dto::*, error::ApiError, state::AppState}; use crate::{
auth::resolve_user_id,
dto::{
CommentResponse,
CreateCommentRequest,
CreateRatingRequest,
CreateShareLinkRequest,
FavoriteRequest,
MediaResponse,
RatingResponse,
ShareLinkResponse,
},
error::ApiError,
state::AppState,
};
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct ShareLinkQuery { pub struct ShareLinkQuery {
@ -133,8 +147,7 @@ pub async fn create_share_link(
{ {
return Err(ApiError( return Err(ApiError(
pinakes_core::error::PinakesError::InvalidOperation(format!( pinakes_core::error::PinakesError::InvalidOperation(format!(
"expires_in_hours cannot exceed {}", "expires_in_hours cannot exceed {MAX_EXPIRY_HOURS}"
MAX_EXPIRY_HOURS
)), )),
)); ));
} }

View file

@ -98,7 +98,7 @@ pub async fn hls_variant_playlist(
); );
for i in 0..num_segments.max(1) { for i in 0..num_segments.max(1) {
let seg_dur = if i == num_segments.saturating_sub(1) && duration > 0.0 { let seg_dur = if i == num_segments.saturating_sub(1) && duration > 0.0 {
duration - (i as f64 * segment_duration) (i as f64).mul_add(-segment_duration, duration)
} else { } else {
segment_duration segment_duration
}; };
@ -143,7 +143,7 @@ pub async fn hls_segment(
if segment_path.exists() { if segment_path.exists() {
let data = tokio::fs::read(&segment_path).await.map_err(|e| { let data = tokio::fs::read(&segment_path).await.map_err(|e| {
ApiError(pinakes_core::error::PinakesError::InvalidOperation( ApiError(pinakes_core::error::PinakesError::InvalidOperation(
format!("failed to read segment: {}", e), format!("failed to read segment: {e}"),
)) ))
})?; })?;
@ -246,7 +246,7 @@ pub async fn dash_segment(
if segment_path.exists() { if segment_path.exists() {
let data = tokio::fs::read(&segment_path).await.map_err(|e| { let data = tokio::fs::read(&segment_path).await.map_err(|e| {
ApiError(pinakes_core::error::PinakesError::InvalidOperation( ApiError(pinakes_core::error::PinakesError::InvalidOperation(
format!("failed to read segment: {}", e), format!("failed to read segment: {e}"),
)) ))
})?; })?;

View file

@ -8,7 +8,11 @@ use pinakes_core::{
}; };
use uuid::Uuid; use uuid::Uuid;
use crate::{dto::*, error::ApiError, state::AppState}; use crate::{
dto::{AddSubtitleRequest, SubtitleResponse, UpdateSubtitleOffsetRequest},
error::ApiError,
state::AppState,
};
pub async fn list_subtitles( pub async fn list_subtitles(
State(state): State<AppState>, State(state): State<AppState>,

View file

@ -5,7 +5,11 @@ use axum::{
use pinakes_core::model::MediaId; use pinakes_core::model::MediaId;
use uuid::Uuid; use uuid::Uuid;
use crate::{dto::*, error::ApiError, state::AppState}; use crate::{
dto::{CreateTagRequest, TagMediaRequest, TagResponse},
error::ApiError,
state::AppState,
};
pub async fn create_tag( pub async fn create_tag(
State(state): State<AppState>, State(state): State<AppState>,

View file

@ -5,7 +5,11 @@ use axum::{
use pinakes_core::model::MediaId; use pinakes_core::model::MediaId;
use uuid::Uuid; use uuid::Uuid;
use crate::{dto::*, error::ApiError, state::AppState}; use crate::{
dto::{CreateTranscodeRequest, PaginationParams, TranscodeSessionResponse},
error::ApiError,
state::AppState,
};
pub async fn start_transcode( pub async fn start_transcode(
State(state): State<AppState>, State(state): State<AppState>,

View file

@ -4,7 +4,16 @@ use axum::{
}; };
use pinakes_core::users::{CreateUserRequest, UpdateUserRequest, UserId}; use pinakes_core::users::{CreateUserRequest, UpdateUserRequest, UserId};
use crate::{dto::*, error::ApiError, state::AppState}; use crate::{
dto::{
GrantLibraryAccessRequest,
RevokeLibraryAccessRequest,
UserLibraryResponse,
UserResponse,
},
error::ApiError,
state::AppState,
};
/// List all users (admin only) /// List all users (admin only)
pub async fn list_users( pub async fn list_users(
@ -175,7 +184,7 @@ pub async fn grant_library_access(
/// Revoke library access from a user (admin only) /// Revoke library access from a user (admin only)
/// ///
/// Uses a JSON body instead of a path parameter because root_path may contain /// Uses a JSON body instead of a path parameter because `root_path` may contain
/// slashes that conflict with URL routing. /// slashes that conflict with URL routing.
pub async fn revoke_library_access( pub async fn revoke_library_access(
State(state): State<AppState>, State(state): State<AppState>,

View file

@ -19,6 +19,7 @@ use pinakes_core::{
ManagedStorageConfig, ManagedStorageConfig,
PhotoConfig, PhotoConfig,
PluginsConfig, PluginsConfig,
RateLimitConfig,
ScanningConfig, ScanningConfig,
ServerConfig, ServerConfig,
SharingConfig, SharingConfig,
@ -41,19 +42,19 @@ use pinakes_core::{
use tokio::sync::RwLock; use tokio::sync::RwLock;
use tower::ServiceExt; use tower::ServiceExt;
/// Fake socket address for tests (governor needs ConnectInfo<SocketAddr>) /// Fake socket address for tests (governor needs `ConnectInfo`<SocketAddr>)
fn test_addr() -> ConnectInfo<SocketAddr> { fn test_addr() -> ConnectInfo<SocketAddr> {
ConnectInfo("127.0.0.1:9999".parse().unwrap()) ConnectInfo("127.0.0.1:9999".parse().unwrap())
} }
/// Build a GET request with ConnectInfo for rate limiter compatibility /// Build a GET request with `ConnectInfo` for rate limiter compatibility
fn get(uri: &str) -> Request<Body> { fn get(uri: &str) -> Request<Body> {
let mut req = Request::builder().uri(uri).body(Body::empty()).unwrap(); let mut req = Request::builder().uri(uri).body(Body::empty()).unwrap();
req.extensions_mut().insert(test_addr()); req.extensions_mut().insert(test_addr());
req req
} }
/// Build a POST request with ConnectInfo /// Build a POST request with `ConnectInfo`
fn post_json(uri: &str, body: &str) -> Request<Body> { fn post_json(uri: &str, body: &str) -> Request<Body> {
let mut req = Request::builder() let mut req = Request::builder()
.method("POST") .method("POST")
@ -69,7 +70,7 @@ fn post_json(uri: &str, body: &str) -> Request<Body> {
fn get_authed(uri: &str, token: &str) -> Request<Body> { fn get_authed(uri: &str, token: &str) -> Request<Body> {
let mut req = Request::builder() let mut req = Request::builder()
.uri(uri) .uri(uri)
.header("authorization", format!("Bearer {}", token)) .header("authorization", format!("Bearer {token}"))
.body(Body::empty()) .body(Body::empty())
.unwrap(); .unwrap();
req.extensions_mut().insert(test_addr()); req.extensions_mut().insert(test_addr());
@ -82,7 +83,7 @@ fn post_json_authed(uri: &str, body: &str, token: &str) -> Request<Body> {
.method("POST") .method("POST")
.uri(uri) .uri(uri)
.header("content-type", "application/json") .header("content-type", "application/json")
.header("authorization", format!("Bearer {}", token)) .header("authorization", format!("Bearer {token}"))
.body(Body::from(body.to_string())) .body(Body::from(body.to_string()))
.unwrap(); .unwrap();
req.extensions_mut().insert(test_addr()); req.extensions_mut().insert(test_addr());
@ -94,7 +95,7 @@ fn delete_authed(uri: &str, token: &str) -> Request<Body> {
let mut req = Request::builder() let mut req = Request::builder()
.method("DELETE") .method("DELETE")
.uri(uri) .uri(uri)
.header("authorization", format!("Bearer {}", token)) .header("authorization", format!("Bearer {token}"))
.body(Body::empty()) .body(Body::empty())
.unwrap(); .unwrap();
req.extensions_mut().insert(test_addr()); req.extensions_mut().insert(test_addr());
@ -107,7 +108,7 @@ fn patch_json_authed(uri: &str, body: &str, token: &str) -> Request<Body> {
.method("PATCH") .method("PATCH")
.uri(uri) .uri(uri)
.header("content-type", "application/json") .header("content-type", "application/json")
.header("authorization", format!("Bearer {}", token)) .header("authorization", format!("Bearer {token}"))
.body(Body::from(body.to_string())) .body(Body::from(body.to_string()))
.unwrap(); .unwrap();
req.extensions_mut().insert(test_addr()); req.extensions_mut().insert(test_addr());
@ -136,7 +137,10 @@ fn default_config() -> Config {
api_key: None, api_key: None,
tls: TlsConfig::default(), tls: TlsConfig::default(),
authentication_disabled: true, authentication_disabled: true,
cors_enabled: false,
cors_origins: vec![],
}, },
rate_limits: RateLimitConfig::default(),
ui: UiConfig::default(), ui: UiConfig::default(),
accounts: AccountsConfig::default(), accounts: AccountsConfig::default(),
jobs: JobsConfig::default(), jobs: JobsConfig::default(),
@ -164,7 +168,7 @@ async fn setup_app() -> axum::Router {
let config = default_config(); let config = default_config();
let job_queue = let job_queue =
JobQueue::new(1, |_id, _kind, _cancel, _jobs| tokio::spawn(async {})); JobQueue::new(1, 0, |_id, _kind, _cancel, _jobs| tokio::spawn(async {}));
let config = Arc::new(RwLock::new(config)); let config = Arc::new(RwLock::new(config));
let scheduler = pinakes_core::scheduler::TaskScheduler::new( let scheduler = pinakes_core::scheduler::TaskScheduler::new(
job_queue.clone(), job_queue.clone(),
@ -186,9 +190,10 @@ async fn setup_app() -> axum::Router {
managed_storage: None, managed_storage: None,
chunked_upload_manager: None, chunked_upload_manager: None,
session_semaphore: Arc::new(tokio::sync::Semaphore::new(64)), session_semaphore: Arc::new(tokio::sync::Semaphore::new(64)),
webhook_dispatcher: None,
}; };
pinakes_server::app::create_router(state) pinakes_server::app::create_router(state, &RateLimitConfig::default())
} }
/// Hash a password for test user accounts /// Hash a password for test user accounts
@ -197,7 +202,7 @@ fn hash_password(password: &str) -> String {
} }
/// Set up an app with accounts enabled and three pre-seeded users. /// Set up an app with accounts enabled and three pre-seeded users.
/// Returns (Router, admin_token, editor_token, viewer_token). /// Returns (Router, `admin_token`, `editor_token`, `viewer_token`).
async fn setup_app_with_auth() -> (axum::Router, String, String, String) { async fn setup_app_with_auth() -> (axum::Router, String, String, String) {
let backend = SqliteBackend::in_memory().expect("in-memory SQLite"); let backend = SqliteBackend::in_memory().expect("in-memory SQLite");
backend.run_migrations().await.expect("migrations"); backend.run_migrations().await.expect("migrations");
@ -239,7 +244,7 @@ async fn setup_app_with_auth() -> (axum::Router, String, String, String) {
]; ];
let job_queue = let job_queue =
JobQueue::new(1, |_id, _kind, _cancel, _jobs| tokio::spawn(async {})); JobQueue::new(1, 0, |_id, _kind, _cancel, _jobs| tokio::spawn(async {}));
let config = Arc::new(RwLock::new(config)); let config = Arc::new(RwLock::new(config));
let scheduler = pinakes_core::scheduler::TaskScheduler::new( let scheduler = pinakes_core::scheduler::TaskScheduler::new(
job_queue.clone(), job_queue.clone(),
@ -261,9 +266,11 @@ async fn setup_app_with_auth() -> (axum::Router, String, String, String) {
managed_storage: None, managed_storage: None,
chunked_upload_manager: None, chunked_upload_manager: None,
session_semaphore: Arc::new(tokio::sync::Semaphore::new(64)), session_semaphore: Arc::new(tokio::sync::Semaphore::new(64)),
webhook_dispatcher: None,
}; };
let app = pinakes_server::app::create_router(state); let app =
pinakes_server::app::create_router(state, &RateLimitConfig::default());
// Login each user to get tokens // Login each user to get tokens
let admin_token = login_user(app.clone(), "admin", "adminpass").await; let admin_token = login_user(app.clone(), "admin", "adminpass").await;
@ -278,8 +285,7 @@ async fn login_user(
username: &str, username: &str,
password: &str, password: &str,
) -> String { ) -> String {
let body = let body = format!(r#"{{"username":"{username}","password":"{password}"}}"#);
format!(r#"{{"username":"{}","password":"{}"}}"#, username, password);
let response = app let response = app
.oneshot(post_json("/api/v1/auth/login", &body)) .oneshot(post_json("/api/v1/auth/login", &body))
.await .await
@ -287,8 +293,7 @@ async fn login_user(
assert_eq!( assert_eq!(
response.status(), response.status(),
StatusCode::OK, StatusCode::OK,
"login failed for user {}", "login failed for user {username}"
username
); );
let body = response.into_body().collect().await.unwrap().to_bytes(); let body = response.into_body().collect().await.unwrap().to_bytes();
let result: serde_json::Value = serde_json::from_slice(&body).unwrap(); let result: serde_json::Value = serde_json::from_slice(&body).unwrap();
@ -449,7 +454,7 @@ async fn test_user_management_crud() {
// Get specific user // Get specific user
let response = app let response = app
.clone() .clone()
.oneshot(get(&format!("/api/v1/users/{}", user_id))) .oneshot(get(&format!("/api/v1/users/{user_id}")))
.await .await
.unwrap(); .unwrap();
@ -462,7 +467,7 @@ async fn test_user_management_crud() {
// Delete user // Delete user
let mut req = Request::builder() let mut req = Request::builder()
.method("DELETE") .method("DELETE")
.uri(&format!("/api/v1/users/{}", user_id)) .uri(format!("/api/v1/users/{user_id}"))
.body(Body::empty()) .body(Body::empty())
.unwrap(); .unwrap();
req.extensions_mut().insert(test_addr()); req.extensions_mut().insert(test_addr());
@ -472,7 +477,7 @@ async fn test_user_management_crud() {
// Verify user is deleted // Verify user is deleted
let response = app let response = app
.oneshot(get(&format!("/api/v1/users/{}", user_id))) .oneshot(get(&format!("/api/v1/users/{user_id}")))
.await .await
.unwrap(); .unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND); assert_eq!(response.status(), StatusCode::NOT_FOUND);
@ -796,7 +801,7 @@ async fn test_playlist_crud() {
let response = app let response = app
.clone() .clone()
.oneshot(get_authed( .oneshot(get_authed(
&format!("/api/v1/playlists/{}", playlist_id), &format!("/api/v1/playlists/{playlist_id}"),
&editor_token, &editor_token,
)) ))
.await .await
@ -807,7 +812,7 @@ async fn test_playlist_crud() {
let response = app let response = app
.clone() .clone()
.oneshot(patch_json_authed( .oneshot(patch_json_authed(
&format!("/api/v1/playlists/{}", playlist_id), &format!("/api/v1/playlists/{playlist_id}"),
r#"{"name":"Updated Playlist","description":"A test description"}"#, r#"{"name":"Updated Playlist","description":"A test description"}"#,
&editor_token, &editor_token,
)) ))
@ -821,7 +826,7 @@ async fn test_playlist_crud() {
let response = app let response = app
.clone() .clone()
.oneshot(delete_authed( .oneshot(delete_authed(
&format!("/api/v1/playlists/{}", playlist_id), &format!("/api/v1/playlists/{playlist_id}"),
&editor_token, &editor_token,
)) ))
.await .await
@ -972,7 +977,7 @@ async fn test_oversized_comment() {
let (app, _, editor_token, _) = setup_app_with_auth().await; let (app, _, editor_token, _) = setup_app_with_auth().await;
let long_text: String = "x".repeat(10_001); let long_text: String = "x".repeat(10_001);
let body = format!(r#"{{"text":"{}"}}"#, long_text); let body = format!(r#"{{"text":"{long_text}"}}"#);
let response = app let response = app
.oneshot(post_json_authed( .oneshot(post_json_authed(
"/api/v1/media/00000000-0000-0000-0000-000000000000/comments", "/api/v1/media/00000000-0000-0000-0000-000000000000/comments",

View file

@ -19,6 +19,7 @@ use pinakes_core::{
ManagedStorageConfig, ManagedStorageConfig,
PhotoConfig, PhotoConfig,
PluginsConfig, PluginsConfig,
RateLimitConfig,
ScanningConfig, ScanningConfig,
ServerConfig, ServerConfig,
SharingConfig, SharingConfig,
@ -40,12 +41,12 @@ use pinakes_core::{
use tokio::sync::RwLock; use tokio::sync::RwLock;
use tower::ServiceExt; use tower::ServiceExt;
/// Fake socket address for tests (governor needs ConnectInfo<SocketAddr>) /// Fake socket address for tests (governor needs `ConnectInfo`<SocketAddr>)
fn test_addr() -> ConnectInfo<SocketAddr> { fn test_addr() -> ConnectInfo<SocketAddr> {
ConnectInfo("127.0.0.1:9999".parse().unwrap()) ConnectInfo("127.0.0.1:9999".parse().unwrap())
} }
/// Build a GET request with ConnectInfo for rate limiter compatibility /// Build a GET request with `ConnectInfo` for rate limiter compatibility
fn get(uri: &str) -> Request<Body> { fn get(uri: &str) -> Request<Body> {
let mut req = Request::builder().uri(uri).body(Body::empty()).unwrap(); let mut req = Request::builder().uri(uri).body(Body::empty()).unwrap();
req.extensions_mut().insert(test_addr()); req.extensions_mut().insert(test_addr());
@ -103,7 +104,10 @@ async fn setup_app_with_plugins()
api_key: None, api_key: None,
tls: TlsConfig::default(), tls: TlsConfig::default(),
authentication_disabled: true, authentication_disabled: true,
cors_enabled: false,
cors_origins: vec![],
}, },
rate_limits: RateLimitConfig::default(),
ui: UiConfig::default(), ui: UiConfig::default(),
accounts: AccountsConfig::default(), accounts: AccountsConfig::default(),
jobs: JobsConfig::default(), jobs: JobsConfig::default(),
@ -123,7 +127,7 @@ async fn setup_app_with_plugins()
}; };
let job_queue = let job_queue =
JobQueue::new(1, |_id, _kind, _cancel, _jobs| tokio::spawn(async {})); JobQueue::new(1, 0, |_id, _kind, _cancel, _jobs| tokio::spawn(async {}));
let config = Arc::new(RwLock::new(config)); let config = Arc::new(RwLock::new(config));
let scheduler = pinakes_core::scheduler::TaskScheduler::new( let scheduler = pinakes_core::scheduler::TaskScheduler::new(
job_queue.clone(), job_queue.clone(),
@ -145,9 +149,11 @@ async fn setup_app_with_plugins()
managed_storage: None, managed_storage: None,
chunked_upload_manager: None, chunked_upload_manager: None,
session_semaphore: Arc::new(tokio::sync::Semaphore::new(64)), session_semaphore: Arc::new(tokio::sync::Semaphore::new(64)),
webhook_dispatcher: None,
}; };
let router = pinakes_server::app::create_router(state); let router =
pinakes_server::app::create_router(state, &RateLimitConfig::default());
(router, plugin_manager, temp_dir) (router, plugin_manager, temp_dir)
} }