chore: cleanup unused backend code after migration to server functions
Some checks failed
Build MIPS Binary / build (push) Has been cancelled
Some checks failed
Build MIPS Binary / build (push) Has been cancelled
This commit is contained in:
@@ -1,154 +1,2 @@
|
||||
use crate::AppState;
|
||||
use axum::{
|
||||
extract::{State, Json},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite};
|
||||
use time::Duration;
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct LoginRequest {
|
||||
username: String,
|
||||
password: String,
|
||||
#[serde(default)]
|
||||
remember_me: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct UserResponse {
|
||||
username: String,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/auth/login",
|
||||
request_body = LoginRequest,
|
||||
responses(
|
||||
(status = 200, description = "Login successful"),
|
||||
(status = 401, description = "Invalid credentials"),
|
||||
(status = 500, description = "Internal server error")
|
||||
)
|
||||
)]
|
||||
pub async fn login_handler(
|
||||
State(state): State<AppState>,
|
||||
jar: CookieJar,
|
||||
Json(payload): Json<LoginRequest>,
|
||||
) -> impl IntoResponse {
|
||||
tracing::info!("Login attempt for user: {}", payload.username);
|
||||
|
||||
let user = match state.db.get_user_by_username(&payload.username).await {
|
||||
Ok(Some(u)) => u,
|
||||
Ok(None) => {
|
||||
tracing::warn!("Login failed: User not found for {}", payload.username);
|
||||
return (StatusCode::UNAUTHORIZED, "Invalid credentials").into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("DB error during login for {}: {}", payload.username, e);
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, "Database error").into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let (user_id, password_hash) = user;
|
||||
|
||||
match bcrypt::verify(&payload.password, &password_hash) {
|
||||
Ok(true) => {
|
||||
tracing::info!("Password verified for user: {}", payload.username);
|
||||
|
||||
// Create session
|
||||
let token: String = (0..32).map(|_| {
|
||||
use rand::{distributions::Alphanumeric, Rng};
|
||||
rand::thread_rng().sample(Alphanumeric) as char
|
||||
}).collect();
|
||||
|
||||
// Expiration: 30 days if remember_me is true, else 1 day
|
||||
let expires_in = if payload.remember_me {
|
||||
60 * 60 * 24 * 30
|
||||
} else {
|
||||
60 * 60 * 24
|
||||
};
|
||||
|
||||
let expires_at = time::OffsetDateTime::now_utc().unix_timestamp() + expires_in;
|
||||
|
||||
if let Err(e) = state.db.create_session(user_id, &token, expires_at).await {
|
||||
tracing::error!("Failed to create session for {}: {}", payload.username, e);
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, "Failed to create session").into_response();
|
||||
}
|
||||
|
||||
let mut cookie = Cookie::build(("auth_token", token))
|
||||
.path("/")
|
||||
.http_only(true)
|
||||
.same_site(SameSite::Lax)
|
||||
.build();
|
||||
|
||||
cookie.set_max_age(Duration::seconds(expires_in));
|
||||
|
||||
tracing::info!("Session created and cookie set for user: {}", payload.username);
|
||||
(StatusCode::OK, jar.add(cookie), Json(UserResponse { username: payload.username })).into_response()
|
||||
}
|
||||
Ok(false) => {
|
||||
tracing::warn!("Login failed: Invalid password for {}", payload.username);
|
||||
(StatusCode::UNAUTHORIZED, "Invalid credentials").into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Bcrypt error for {}: {}", payload.username, e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "Auth error").into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/auth/logout",
|
||||
responses(
|
||||
(status = 200, description = "Logged out")
|
||||
)
|
||||
)]
|
||||
pub async fn logout_handler(
|
||||
State(state): State<AppState>,
|
||||
jar: CookieJar,
|
||||
) -> impl IntoResponse {
|
||||
if let Some(token) = jar.get("auth_token") {
|
||||
let _ = state.db.delete_session(token.value()).await;
|
||||
}
|
||||
|
||||
let cookie = Cookie::build(("auth_token", ""))
|
||||
.path("/")
|
||||
.http_only(true)
|
||||
.max_age(Duration::seconds(-1)) // Expire immediately
|
||||
.build();
|
||||
|
||||
(StatusCode::OK, jar.add(cookie), "Logged out").into_response()
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/auth/check",
|
||||
responses(
|
||||
(status = 200, description = "Authenticated", body = UserResponse),
|
||||
(status = 401, description = "Not authenticated")
|
||||
)
|
||||
)]
|
||||
pub async fn check_auth_handler(
|
||||
State(state): State<AppState>,
|
||||
jar: CookieJar,
|
||||
) -> impl IntoResponse {
|
||||
if let Some(token) = jar.get("auth_token") {
|
||||
match state.db.get_session_user(token.value()).await {
|
||||
Ok(Some(user_id)) => {
|
||||
// Fetch username
|
||||
// We need a helper in db.rs to get username by id, or we can use a direct query here if we don't want to change db.rs interface yet.
|
||||
// But better to add `get_username_by_id` to db.rs
|
||||
// For now let's query directly or via a new db method.
|
||||
if let Ok(Some(username)) = state.db.get_username_by_id(user_id).await {
|
||||
return (StatusCode::OK, Json(UserResponse { username })).into_response();
|
||||
}
|
||||
},
|
||||
_ => {} // Invalid session
|
||||
}
|
||||
}
|
||||
|
||||
StatusCode::UNAUTHORIZED.into_response()
|
||||
}
|
||||
// This file is intentionally empty as authentication is now handled by Server Functions.
|
||||
// See shared/src/server_fns/auth.rs
|
||||
|
||||
@@ -1,125 +1,2 @@
|
||||
use crate::AppState;
|
||||
use axum::{
|
||||
extract::{State, Json},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite};
|
||||
use time::Duration;
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct SetupRequest {
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct SetupStatusResponse {
|
||||
completed: bool,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/setup/status",
|
||||
responses(
|
||||
(status = 200, description = "Setup status", body = SetupStatusResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn get_setup_status_handler(State(state): State<AppState>) -> impl IntoResponse {
|
||||
let completed = match state.db.has_users().await {
|
||||
Ok(has) => has,
|
||||
Err(e) => {
|
||||
tracing::error!("DB error checking users: {}", e);
|
||||
false
|
||||
}
|
||||
};
|
||||
Json(SetupStatusResponse { completed }).into_response()
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/setup",
|
||||
request_body = SetupRequest,
|
||||
responses(
|
||||
(status = 200, description = "Setup completed and logged in"),
|
||||
(status = 400, description = "Invalid request"),
|
||||
(status = 403, description = "Setup already completed"),
|
||||
(status = 500, description = "Internal server error")
|
||||
)
|
||||
)]
|
||||
pub async fn setup_handler(
|
||||
State(state): State<AppState>,
|
||||
jar: CookieJar,
|
||||
Json(payload): Json<SetupRequest>,
|
||||
) -> impl IntoResponse {
|
||||
// 1. Check if setup is already completed (i.e., users exist)
|
||||
match state.db.has_users().await {
|
||||
Ok(true) => return (StatusCode::FORBIDDEN, "Setup already completed").into_response(),
|
||||
Err(e) => {
|
||||
tracing::error!("DB error checking users: {}", e);
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, "Database error").into_response();
|
||||
}
|
||||
Ok(false) => {} // Proceed
|
||||
}
|
||||
|
||||
// 2. Validate input
|
||||
if payload.username.len() < 3 || payload.password.len() < 6 {
|
||||
return (StatusCode::BAD_REQUEST, "Username must be at least 3 chars, password at least 6").into_response();
|
||||
}
|
||||
|
||||
// 3. Create User
|
||||
// Lower cost for faster login on low-power devices (MIPS routers etc.)
|
||||
let password_hash = match bcrypt::hash(&payload.password, 6) {
|
||||
Ok(h) => h,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to hash password: {}", e);
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, "Failed to process password").into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = state.db.create_user(&payload.username, &password_hash).await {
|
||||
tracing::error!("Failed to create user: {}", e);
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, "Failed to create user").into_response();
|
||||
}
|
||||
|
||||
// 4. Auto-Login (Create Session)
|
||||
// Get the created user's ID
|
||||
let user = match state.db.get_user_by_username(&payload.username).await {
|
||||
Ok(Some(u)) => u,
|
||||
Ok(None) => return (StatusCode::INTERNAL_SERVER_ERROR, "User created but not found").into_response(),
|
||||
Err(e) => {
|
||||
tracing::error!("DB error fetching new user: {}", e);
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, "Database error").into_response();
|
||||
}
|
||||
};
|
||||
let (user_id, _) = user;
|
||||
|
||||
// Create session token
|
||||
let token: String = (0..32).map(|_| {
|
||||
use rand::{distributions::Alphanumeric, Rng};
|
||||
rand::thread_rng().sample(Alphanumeric) as char
|
||||
}).collect();
|
||||
|
||||
// Default expiration: 1 day (since it's not "remember me")
|
||||
let expires_in = 60 * 60 * 24;
|
||||
let expires_at = time::OffsetDateTime::now_utc().unix_timestamp() + expires_in;
|
||||
|
||||
if let Err(e) = state.db.create_session(user_id, &token, expires_at).await {
|
||||
tracing::error!("Failed to create session for new user: {}", e);
|
||||
// Even if session fails, setup is technically complete, but login failed.
|
||||
// We return OK but user will have to login manually.
|
||||
return (StatusCode::OK, "Setup completed, please login").into_response();
|
||||
}
|
||||
|
||||
let mut cookie = Cookie::build(("auth_token", token))
|
||||
.path("/")
|
||||
.http_only(true)
|
||||
.same_site(SameSite::Lax)
|
||||
.build();
|
||||
|
||||
cookie.set_max_age(Duration::seconds(expires_in));
|
||||
|
||||
(StatusCode::OK, jar.add(cookie), "Setup completed and logged in").into_response()
|
||||
}
|
||||
// This file is intentionally empty as setup is now handled by Server Functions.
|
||||
// See shared/src/server_fns/auth.rs
|
||||
|
||||
@@ -25,7 +25,6 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::{broadcast, watch};
|
||||
use tower::ServiceBuilder;
|
||||
use tower_governor::GovernorLayer;
|
||||
use tower_http::{
|
||||
compression::{CompressionLayer, CompressionLevel},
|
||||
cors::CorsLayer,
|
||||
@@ -48,7 +47,7 @@ pub struct AppState {
|
||||
}
|
||||
|
||||
async fn auth_middleware(
|
||||
state: axum::extract::State<AppState>,
|
||||
_state: axum::extract::State<AppState>,
|
||||
jar: CookieJar,
|
||||
request: Request<Body>,
|
||||
next: Next,
|
||||
@@ -113,13 +112,6 @@ struct Args {
|
||||
#[cfg(feature = "swagger")]
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
paths(
|
||||
handlers::auth::login_handler,
|
||||
handlers::auth::logout_handler,
|
||||
handlers::auth::check_auth_handler,
|
||||
handlers::setup::setup_handler,
|
||||
handlers::setup::get_setup_status_handler
|
||||
),
|
||||
components(
|
||||
schemas(
|
||||
shared::AddTorrentRequest,
|
||||
@@ -132,10 +124,6 @@ struct Args {
|
||||
shared::SetFilePriorityRequest,
|
||||
shared::SetLabelRequest,
|
||||
shared::GlobalLimitRequest,
|
||||
handlers::auth::LoginRequest,
|
||||
handlers::setup::SetupRequest,
|
||||
handlers::setup::SetupStatusResponse,
|
||||
handlers::auth::UserResponse
|
||||
)
|
||||
),
|
||||
tags(
|
||||
@@ -144,6 +132,7 @@ struct Args {
|
||||
)]
|
||||
struct ApiDoc;
|
||||
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Load .env file
|
||||
@@ -423,6 +412,11 @@ async fn main() {
|
||||
#[cfg(feature = "swagger")]
|
||||
let app = app.merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi()));
|
||||
|
||||
// Setup & Auth Routes (cookie-based, stay as REST)
|
||||
let scgi_path_for_ctx = args.socket.clone();
|
||||
let db_for_ctx = db.clone();
|
||||
let app = app
|
||||
.route("/api/events", get(sse::sse_handler))
|
||||
// Setup & Auth Routes (cookie-based, stay as REST)
|
||||
let scgi_path_for_ctx = args.socket.clone();
|
||||
let db_for_ctx = db.clone();
|
||||
|
||||
@@ -1,16 +1,3 @@
|
||||
use governor::clock::QuantaInstant;
|
||||
use governor::middleware::NoOpMiddleware;
|
||||
use tower_governor::governor::GovernorConfig;
|
||||
use tower_governor::governor::GovernorConfigBuilder;
|
||||
use tower_governor::key_extractor::SmartIpKeyExtractor;
|
||||
|
||||
pub fn get_login_rate_limit_config() -> GovernorConfig<SmartIpKeyExtractor, NoOpMiddleware<QuantaInstant>> {
|
||||
// 5 yanlış denemeden sonra bloklanır.
|
||||
// Her yeni hak için 60 saniye (1 dakika) bekleme süresi.
|
||||
GovernorConfigBuilder::default()
|
||||
.key_extractor(SmartIpKeyExtractor)
|
||||
.per_second(60)
|
||||
.burst_size(5)
|
||||
.finish()
|
||||
.unwrap()
|
||||
}
|
||||
// This file can be removed or repurposed if rate limiting is needed for other endpoints.
|
||||
// Login rate limiting is now handled within the server function or needs to be reimplemented
|
||||
// as a middleware for the server function endpoint.
|
||||
|
||||
@@ -4,7 +4,7 @@ use shared::xmlrpc::{
|
||||
use crate::AppState;
|
||||
use axum::extract::State;
|
||||
use axum::response::sse::{Event, Sse};
|
||||
use futures::stream::{self, Stream};
|
||||
use futures::stream::{self};
|
||||
use shared::{AppEvent, GlobalStats, Torrent, TorrentStatus};
|
||||
use std::convert::Infallible;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
Reference in New Issue
Block a user