pinakes-server: TLS support; session persistence and security polish
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: If2c9c3e3af62bbf9f33a97be89ac40bc6a6a6964
This commit is contained in:
parent
758aba0f7a
commit
87a4482576
19 changed files with 1835 additions and 111 deletions
|
|
@ -44,6 +44,24 @@ pub fn create_router_with_tls(
|
|||
.unwrap(),
|
||||
);
|
||||
|
||||
// Rate limit for search: 10 requests/min per IP
|
||||
let search_governor = Arc::new(
|
||||
GovernorConfigBuilder::default()
|
||||
.per_second(6) // replenish one every 6 seconds (10/min)
|
||||
.burst_size(10)
|
||||
.finish()
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
// Rate limit for streaming: 5 requests per IP (very restrictive for concurrent streams)
|
||||
let stream_governor = Arc::new(
|
||||
GovernorConfigBuilder::default()
|
||||
.per_second(60) // replenish slowly (one per minute)
|
||||
.burst_size(5) // max 5 concurrent connections
|
||||
.finish()
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
// Login route with strict rate limiting
|
||||
let login_route = Router::new()
|
||||
.route("/auth/login", post(routes::auth::login))
|
||||
|
|
@ -58,6 +76,21 @@ pub fn create_router_with_tls(
|
|||
.route("/health/live", get(routes::health::liveness))
|
||||
.route("/health/ready", get(routes::health::readiness));
|
||||
|
||||
// Search routes with enhanced rate limiting (10 req/min)
|
||||
let search_routes = Router::new()
|
||||
.route("/search", get(routes::search::search))
|
||||
.route("/search", post(routes::search::search_post))
|
||||
.layer(GovernorLayer {
|
||||
config: search_governor,
|
||||
});
|
||||
|
||||
// Streaming routes with enhanced rate limiting (5 concurrent)
|
||||
let streaming_routes = Router::new()
|
||||
.route("/media/{id}/stream", get(routes::media::stream_media))
|
||||
.layer(GovernorLayer {
|
||||
config: stream_governor,
|
||||
});
|
||||
|
||||
// Read-only routes: any authenticated user (Viewer+)
|
||||
let viewer_routes = Router::new()
|
||||
.route("/health", get(routes::health::health))
|
||||
|
|
@ -65,11 +98,8 @@ pub fn create_router_with_tls(
|
|||
.route("/media/count", get(routes::media::get_media_count))
|
||||
.route("/media", get(routes::media::list_media))
|
||||
.route("/media/{id}", get(routes::media::get_media))
|
||||
.route("/media/{id}/stream", get(routes::media::stream_media))
|
||||
.route("/media/{id}/thumbnail", get(routes::media::get_thumbnail))
|
||||
.route("/media/{media_id}/tags", get(routes::tags::get_media_tags))
|
||||
.route("/search", get(routes::search::search))
|
||||
.route("/search", post(routes::search::search_post))
|
||||
.route("/tags", get(routes::tags::list_tags))
|
||||
.route("/tags/{id}", get(routes::tags::get_tag))
|
||||
.route("/collections", get(routes::collections::list_collections))
|
||||
|
|
@ -107,6 +137,7 @@ pub fn create_router_with_tls(
|
|||
// Auth endpoints (self-service) — login handled separately with stricter rate limit
|
||||
.route("/auth/logout", post(routes::auth::logout))
|
||||
.route("/auth/me", get(routes::auth::me))
|
||||
.route("/auth/revoke-all", post(routes::auth::revoke_all_sessions))
|
||||
// Social: ratings & comments (read)
|
||||
.route(
|
||||
"/media/{id}/ratings",
|
||||
|
|
@ -374,6 +405,8 @@ pub fn create_router_with_tls(
|
|||
"/users/{id}/libraries",
|
||||
delete(routes::users::revoke_library_access),
|
||||
)
|
||||
// Session management (admin)
|
||||
.route("/auth/sessions", get(routes::auth::list_active_sessions))
|
||||
.layer(middleware::from_fn(auth::require_admin));
|
||||
|
||||
// CORS: allow same-origin by default, plus the desktop UI origin
|
||||
|
|
@ -396,6 +429,8 @@ pub fn create_router_with_tls(
|
|||
// Create protected routes with auth middleware
|
||||
let protected_api = Router::new()
|
||||
.merge(viewer_routes)
|
||||
.merge(search_routes)
|
||||
.merge(streaming_routes)
|
||||
.merge(editor_routes)
|
||||
.merge(admin_routes)
|
||||
.layer(middleware::from_fn_with_state(
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ fn constant_time_eq(a: &str, b: &str) -> bool {
|
|||
|
||||
/// Axum middleware that checks for a valid Bearer token.
|
||||
///
|
||||
/// If `accounts.enabled == true`: look up bearer token in session store.
|
||||
/// If `accounts.enabled == true`: look up bearer token in database session store.
|
||||
/// If `accounts.enabled == false`: use existing api_key logic (unchanged behavior).
|
||||
/// Skips authentication for the `/health` and `/auth/login` path suffixes.
|
||||
pub async fn require_auth(
|
||||
|
|
@ -38,8 +38,19 @@ pub async fn require_auth(
|
|||
|
||||
let config = state.config.read().await;
|
||||
|
||||
// Check if authentication is explicitly disabled
|
||||
if config.server.authentication_disabled {
|
||||
drop(config);
|
||||
tracing::warn!("authentication is disabled - allowing all requests");
|
||||
request.extensions_mut().insert(UserRole::Admin);
|
||||
request.extensions_mut().insert("admin".to_string());
|
||||
return next.run(request).await;
|
||||
}
|
||||
|
||||
if config.accounts.enabled {
|
||||
// Session-based auth
|
||||
drop(config);
|
||||
|
||||
// Session-based auth using database
|
||||
let token = request
|
||||
.headers()
|
||||
.get("authorization")
|
||||
|
|
@ -47,32 +58,63 @@ pub async fn require_auth(
|
|||
.and_then(|s| s.strip_prefix("Bearer "))
|
||||
.map(|s| s.to_string());
|
||||
|
||||
drop(config);
|
||||
|
||||
let Some(token) = token else {
|
||||
tracing::debug!(path = %path, "rejected: missing Authorization header");
|
||||
return unauthorized("missing Authorization header");
|
||||
};
|
||||
|
||||
let sessions = state.sessions.read().await;
|
||||
let Some(session) = sessions.get(&token) else {
|
||||
tracing::debug!(path = %path, "rejected: invalid session token");
|
||||
return unauthorized("invalid or expired session token");
|
||||
// Look up session in database
|
||||
let session_result = state.storage.get_session(&token).await;
|
||||
let session = match session_result {
|
||||
Ok(Some(session)) => session,
|
||||
Ok(None) => {
|
||||
tracing::debug!(path = %path, "rejected: invalid session token");
|
||||
return unauthorized("invalid or expired session token");
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(error = %e, "failed to query session from database");
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, "database error").into_response();
|
||||
}
|
||||
};
|
||||
|
||||
// Check session expiry
|
||||
if session.is_expired() {
|
||||
let now = chrono::Utc::now();
|
||||
if session.expires_at < now {
|
||||
let username = session.username.clone();
|
||||
drop(sessions);
|
||||
// Remove expired session
|
||||
let mut sessions_mut = state.sessions.write().await;
|
||||
sessions_mut.remove(&token);
|
||||
// Delete expired session asynchronously (fire-and-forget)
|
||||
let storage = state.storage.clone();
|
||||
let token_owned = token.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = storage.delete_session(&token_owned).await {
|
||||
tracing::error!(error = %e, "failed to delete expired session");
|
||||
}
|
||||
});
|
||||
tracing::info!(username = %username, "session expired");
|
||||
return unauthorized("session expired");
|
||||
}
|
||||
|
||||
// Update last_accessed timestamp asynchronously (fire-and-forget)
|
||||
let storage = state.storage.clone();
|
||||
let token_owned = token.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = storage.touch_session(&token_owned).await {
|
||||
tracing::warn!(error = %e, "failed to update session last_accessed");
|
||||
}
|
||||
});
|
||||
|
||||
// Parse role from string
|
||||
let role = match session.role.as_str() {
|
||||
"admin" => UserRole::Admin,
|
||||
"editor" => UserRole::Editor,
|
||||
"viewer" => UserRole::Viewer,
|
||||
_ => {
|
||||
tracing::warn!(role = %session.role, "unknown role, defaulting to viewer");
|
||||
UserRole::Viewer
|
||||
}
|
||||
};
|
||||
|
||||
// Inject role and username into request extensions
|
||||
request.extensions_mut().insert(session.role);
|
||||
request.extensions_mut().insert(role);
|
||||
request.extensions_mut().insert(session.username.clone());
|
||||
} else {
|
||||
// Legacy API key auth
|
||||
|
|
@ -81,35 +123,38 @@ pub async fn require_auth(
|
|||
.or_else(|| config.server.api_key.clone());
|
||||
drop(config);
|
||||
|
||||
if let Some(ref expected_key) = api_key {
|
||||
if expected_key.is_empty() {
|
||||
// Empty key means no auth required
|
||||
request.extensions_mut().insert(UserRole::Admin);
|
||||
request.extensions_mut().insert("admin".to_string());
|
||||
return next.run(request).await;
|
||||
let Some(ref expected_key) = api_key else {
|
||||
tracing::error!("no authentication configured");
|
||||
return unauthorized("authentication not configured");
|
||||
};
|
||||
|
||||
if expected_key.is_empty() {
|
||||
// Empty key is not allowed - must use authentication_disabled flag
|
||||
tracing::error!("empty api_key rejected, use authentication_disabled flag instead");
|
||||
return unauthorized("authentication not properly configured");
|
||||
}
|
||||
|
||||
let auth_header = request
|
||||
.headers()
|
||||
.get("authorization")
|
||||
.and_then(|v| v.to_str().ok());
|
||||
|
||||
match auth_header {
|
||||
Some(header) if header.starts_with("Bearer ") => {
|
||||
let token = &header[7..];
|
||||
if !constant_time_eq(token, expected_key.as_str()) {
|
||||
tracing::warn!(path = %path, "rejected: invalid API key");
|
||||
return unauthorized("invalid api key");
|
||||
}
|
||||
}
|
||||
|
||||
let auth_header = request
|
||||
.headers()
|
||||
.get("authorization")
|
||||
.and_then(|v| v.to_str().ok());
|
||||
|
||||
match auth_header {
|
||||
Some(header) if header.starts_with("Bearer ") => {
|
||||
let token = &header[7..];
|
||||
if !constant_time_eq(token, expected_key.as_str()) {
|
||||
tracing::warn!(path = %path, "rejected: invalid API key");
|
||||
return unauthorized("invalid api key");
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return unauthorized(
|
||||
"missing or malformed Authorization header, expected: Bearer <api_key>",
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
return unauthorized(
|
||||
"missing or malformed Authorization header, expected: Bearer <api_key>",
|
||||
);
|
||||
}
|
||||
}
|
||||
// When no api_key is configured, or key matches, grant admin
|
||||
|
||||
// API key matches, grant admin
|
||||
request.extensions_mut().insert(UserRole::Admin);
|
||||
request.extensions_mut().insert("admin".to_string());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,6 +98,24 @@ async fn main() -> Result<()> {
|
|||
.validate()
|
||||
.map_err(|e| anyhow::anyhow!("invalid configuration: {e}"))?;
|
||||
|
||||
// Warn about authentication configuration
|
||||
if config.server.authentication_disabled {
|
||||
tracing::warn!(
|
||||
"⚠️ AUTHENTICATION IS DISABLED - All requests will be allowed without authentication!"
|
||||
);
|
||||
tracing::warn!("⚠️ This is INSECURE and should only be used for development.");
|
||||
} else {
|
||||
let has_api_key = config
|
||||
.server
|
||||
.api_key
|
||||
.as_ref()
|
||||
.map_or(false, |k| !k.is_empty());
|
||||
let has_accounts = !config.accounts.users.is_empty();
|
||||
if !has_api_key && !has_accounts {
|
||||
tracing::error!("⚠️ No authentication method configured!");
|
||||
}
|
||||
}
|
||||
|
||||
// Apply CLI overrides
|
||||
if let Some(host) = cli.host {
|
||||
config.server.host = host;
|
||||
|
|
@ -466,7 +484,6 @@ async fn main() -> Result<()> {
|
|||
config: config_arc.clone(),
|
||||
config_path: Some(config_path),
|
||||
scan_progress: pinakes_core::scan::ScanProgress::new(),
|
||||
sessions: Arc::new(RwLock::new(std::collections::HashMap::new())),
|
||||
job_queue,
|
||||
cache,
|
||||
scheduler,
|
||||
|
|
@ -476,14 +493,22 @@ async fn main() -> Result<()> {
|
|||
|
||||
// Periodic session cleanup (every 15 minutes)
|
||||
{
|
||||
let sessions = state.sessions.clone();
|
||||
let storage_clone = storage.clone();
|
||||
let cancel = shutdown_token.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(std::time::Duration::from_secs(15 * 60));
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = interval.tick() => {
|
||||
pinakes_server::state::cleanup_expired_sessions(&sessions).await;
|
||||
match storage_clone.delete_expired_sessions().await {
|
||||
Ok(count) if count > 0 => {
|
||||
tracing::info!(count = count, "cleaned up expired sessions");
|
||||
}
|
||||
Ok(_) => {} // No sessions to clean up
|
||||
Err(e) => {
|
||||
tracing::error!(error = %e, "failed to cleanup expired sessions");
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = cancel.cancelled() => {
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -83,17 +83,21 @@ pub async fn login(
|
|||
let role = user.role;
|
||||
let username = user.username.clone();
|
||||
|
||||
// Store session
|
||||
{
|
||||
let mut sessions = state.sessions.write().await;
|
||||
sessions.insert(
|
||||
token.clone(),
|
||||
crate::state::SessionInfo {
|
||||
username: username.clone(),
|
||||
role,
|
||||
created_at: chrono::Utc::now(),
|
||||
},
|
||||
);
|
||||
// Create session in database
|
||||
let now = chrono::Utc::now();
|
||||
let session_data = pinakes_core::storage::SessionData {
|
||||
session_token: token.clone(),
|
||||
user_id: None, // Could be set if we had user IDs
|
||||
username: username.clone(),
|
||||
role: role.to_string(),
|
||||
created_at: now,
|
||||
expires_at: now + chrono::Duration::hours(24), // 24 hour sessions
|
||||
last_accessed: now,
|
||||
};
|
||||
|
||||
if let Err(e) = state.storage.create_session(&session_data).await {
|
||||
tracing::error!(error = %e, "failed to create session in database");
|
||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
tracing::info!(username = %username, role = %role, "login successful");
|
||||
|
|
@ -116,13 +120,17 @@ pub async fn login(
|
|||
|
||||
pub async fn logout(State(state): State<AppState>, headers: HeaderMap) -> StatusCode {
|
||||
if let Some(token) = extract_bearer_token(&headers) {
|
||||
let sessions = state.sessions.read().await;
|
||||
let username = sessions.get(token).map(|s| s.username.clone());
|
||||
drop(sessions);
|
||||
// Get username before deleting session
|
||||
let username = match state.storage.get_session(token).await {
|
||||
Ok(Some(session)) => Some(session.username),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let mut sessions = state.sessions.write().await;
|
||||
sessions.remove(token);
|
||||
drop(sessions);
|
||||
// Delete session from database
|
||||
if let Err(e) = state.storage.delete_session(token).await {
|
||||
tracing::error!(error = %e, "failed to delete session from database");
|
||||
return StatusCode::INTERNAL_SERVER_ERROR;
|
||||
}
|
||||
|
||||
// Record logout in audit log
|
||||
if let Some(user) = username {
|
||||
|
|
@ -153,12 +161,16 @@ pub async fn me(
|
|||
drop(config);
|
||||
|
||||
let token = extract_bearer_token(&headers).ok_or(StatusCode::UNAUTHORIZED)?;
|
||||
let sessions = state.sessions.read().await;
|
||||
let session = sessions.get(token).ok_or(StatusCode::UNAUTHORIZED)?;
|
||||
let session = state
|
||||
.storage
|
||||
.get_session(token)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||
.ok_or(StatusCode::UNAUTHORIZED)?;
|
||||
|
||||
Ok(Json(UserInfoResponse {
|
||||
username: session.username.clone(),
|
||||
role: session.role.to_string(),
|
||||
role: session.role.clone(),
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
@ -168,3 +180,89 @@ fn extract_bearer_token(headers: &HeaderMap) -> Option<&str> {
|
|||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|s| s.strip_prefix("Bearer "))
|
||||
}
|
||||
|
||||
/// Revoke all sessions for the current user
|
||||
pub async fn revoke_all_sessions(State(state): State<AppState>, headers: HeaderMap) -> StatusCode {
|
||||
let token = match extract_bearer_token(&headers) {
|
||||
Some(t) => t,
|
||||
None => return StatusCode::UNAUTHORIZED,
|
||||
};
|
||||
|
||||
// Get current session to find username
|
||||
let session = match state.storage.get_session(token).await {
|
||||
Ok(Some(s)) => s,
|
||||
Ok(None) => return StatusCode::UNAUTHORIZED,
|
||||
Err(e) => {
|
||||
tracing::error!(error = %e, "failed to get session");
|
||||
return StatusCode::INTERNAL_SERVER_ERROR;
|
||||
}
|
||||
};
|
||||
|
||||
let username = session.username.clone();
|
||||
|
||||
// Delete all sessions for this user
|
||||
match state.storage.delete_user_sessions(&username).await {
|
||||
Ok(count) => {
|
||||
tracing::info!(username = %username, count = count, "revoked all user sessions");
|
||||
|
||||
// Record in audit log
|
||||
let _ = pinakes_core::audit::record_action(
|
||||
&state.storage,
|
||||
None,
|
||||
pinakes_core::model::AuditAction::Logout,
|
||||
Some(format!("revoked all sessions for username: {}", username)),
|
||||
)
|
||||
.await;
|
||||
|
||||
StatusCode::OK
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(error = %e, "failed to revoke sessions");
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// List all active sessions (admin only)
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct SessionListResponse {
|
||||
pub sessions: Vec<SessionInfo>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct SessionInfo {
|
||||
pub username: String,
|
||||
pub role: String,
|
||||
pub created_at: String,
|
||||
pub last_accessed: String,
|
||||
pub expires_at: String,
|
||||
}
|
||||
|
||||
pub async fn list_active_sessions(
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<SessionListResponse>, StatusCode> {
|
||||
// Get all active sessions
|
||||
let sessions = state
|
||||
.storage
|
||||
.list_active_sessions(None)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!(error = %e, "failed to list active sessions");
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
let session_infos = sessions
|
||||
.into_iter()
|
||||
.map(|s| SessionInfo {
|
||||
username: s.username,
|
||||
role: s.role,
|
||||
created_at: s.created_at.to_rfc3339(),
|
||||
last_accessed: s.last_accessed.to_rfc3339(),
|
||||
expires_at: s.expires_at.to_rfc3339(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(SessionListResponse {
|
||||
sessions: session_infos,
|
||||
}))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use pinakes_core::cache::CacheLayer;
|
||||
use pinakes_core::config::{Config, UserRole};
|
||||
use pinakes_core::config::Config;
|
||||
use pinakes_core::jobs::JobQueue;
|
||||
use pinakes_core::plugin::PluginManager;
|
||||
use pinakes_core::scan::ScanProgress;
|
||||
|
|
@ -13,31 +12,8 @@ use pinakes_core::scheduler::TaskScheduler;
|
|||
use pinakes_core::storage::DynStorageBackend;
|
||||
use pinakes_core::transcode::TranscodeService;
|
||||
|
||||
/// Default session TTL: 24 hours.
|
||||
pub const SESSION_TTL_SECS: i64 = 24 * 60 * 60;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SessionInfo {
|
||||
pub username: String,
|
||||
pub role: UserRole,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
impl SessionInfo {
|
||||
/// Returns true if this session has exceeded its TTL.
|
||||
pub fn is_expired(&self) -> bool {
|
||||
let age = chrono::Utc::now() - self.created_at;
|
||||
age.num_seconds() > SESSION_TTL_SECS
|
||||
}
|
||||
}
|
||||
|
||||
pub type SessionStore = Arc<RwLock<HashMap<String, SessionInfo>>>;
|
||||
|
||||
/// Remove all expired sessions from the store.
|
||||
pub async fn cleanup_expired_sessions(sessions: &SessionStore) {
|
||||
let mut store = sessions.write().await;
|
||||
store.retain(|_, info| !info.is_expired());
|
||||
}
|
||||
// Note: Sessions are now stored in the database via StorageBackend
|
||||
// See storage::SessionData and related methods
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
|
|
@ -45,7 +21,6 @@ pub struct AppState {
|
|||
pub config: Arc<RwLock<Config>>,
|
||||
pub config_path: Option<PathBuf>,
|
||||
pub scan_progress: ScanProgress,
|
||||
pub sessions: SessionStore,
|
||||
pub job_queue: Arc<JobQueue>,
|
||||
pub cache: Arc<CacheLayer>,
|
||||
pub scheduler: Arc<TaskScheduler>,
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ fn default_config() -> Config {
|
|||
port: 3000,
|
||||
api_key: None,
|
||||
tls: TlsConfig::default(),
|
||||
authentication_disabled: true,
|
||||
},
|
||||
ui: UiConfig::default(),
|
||||
accounts: AccountsConfig::default(),
|
||||
|
|
@ -149,7 +150,6 @@ async fn setup_app() -> axum::Router {
|
|||
config,
|
||||
config_path: None,
|
||||
scan_progress: pinakes_core::scan::ScanProgress::new(),
|
||||
sessions: Arc::new(RwLock::new(std::collections::HashMap::new())),
|
||||
job_queue,
|
||||
cache: Arc::new(CacheLayer::new(60)),
|
||||
scheduler: Arc::new(scheduler),
|
||||
|
|
@ -187,6 +187,7 @@ async fn setup_app_with_auth() -> (axum::Router, String, String, String) {
|
|||
}
|
||||
|
||||
let mut config = default_config();
|
||||
config.server.authentication_disabled = false; // Enable authentication for these tests
|
||||
config.accounts.enabled = true;
|
||||
config.accounts.users = vec![
|
||||
UserAccount {
|
||||
|
|
@ -220,7 +221,6 @@ async fn setup_app_with_auth() -> (axum::Router, String, String, String) {
|
|||
config,
|
||||
config_path: None,
|
||||
scan_progress: pinakes_core::scan::ScanProgress::new(),
|
||||
sessions: Arc::new(RwLock::new(std::collections::HashMap::new())),
|
||||
job_queue,
|
||||
cache: Arc::new(CacheLayer::new(60)),
|
||||
scheduler: Arc::new(scheduler),
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ async fn setup_app_with_plugins() -> (axum::Router, Arc<PluginManager>, tempfile
|
|||
port: 3000,
|
||||
api_key: None,
|
||||
tls: TlsConfig::default(),
|
||||
authentication_disabled: true,
|
||||
},
|
||||
ui: UiConfig::default(),
|
||||
accounts: AccountsConfig::default(),
|
||||
|
|
@ -106,7 +107,6 @@ async fn setup_app_with_plugins() -> (axum::Router, Arc<PluginManager>, tempfile
|
|||
config,
|
||||
config_path: None,
|
||||
scan_progress: pinakes_core::scan::ScanProgress::new(),
|
||||
sessions: Arc::new(RwLock::new(std::collections::HashMap::new())),
|
||||
job_queue,
|
||||
cache: Arc::new(CacheLayer::new(60)),
|
||||
scheduler: Arc::new(scheduler),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue