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;
|
// This file is intentionally empty as authentication is now handled by Server Functions.
|
||||||
use axum::{
|
// See shared/src/server_fns/auth.rs
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,125 +1,2 @@
|
|||||||
use crate::AppState;
|
// This file is intentionally empty as setup is now handled by Server Functions.
|
||||||
use axum::{
|
// See shared/src/server_fns/auth.rs
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ use std::sync::Arc;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::sync::{broadcast, watch};
|
use tokio::sync::{broadcast, watch};
|
||||||
use tower::ServiceBuilder;
|
use tower::ServiceBuilder;
|
||||||
use tower_governor::GovernorLayer;
|
|
||||||
use tower_http::{
|
use tower_http::{
|
||||||
compression::{CompressionLayer, CompressionLevel},
|
compression::{CompressionLayer, CompressionLevel},
|
||||||
cors::CorsLayer,
|
cors::CorsLayer,
|
||||||
@@ -48,7 +47,7 @@ pub struct AppState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn auth_middleware(
|
async fn auth_middleware(
|
||||||
state: axum::extract::State<AppState>,
|
_state: axum::extract::State<AppState>,
|
||||||
jar: CookieJar,
|
jar: CookieJar,
|
||||||
request: Request<Body>,
|
request: Request<Body>,
|
||||||
next: Next,
|
next: Next,
|
||||||
@@ -113,13 +112,6 @@ struct Args {
|
|||||||
#[cfg(feature = "swagger")]
|
#[cfg(feature = "swagger")]
|
||||||
#[derive(OpenApi)]
|
#[derive(OpenApi)]
|
||||||
#[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(
|
components(
|
||||||
schemas(
|
schemas(
|
||||||
shared::AddTorrentRequest,
|
shared::AddTorrentRequest,
|
||||||
@@ -132,10 +124,6 @@ struct Args {
|
|||||||
shared::SetFilePriorityRequest,
|
shared::SetFilePriorityRequest,
|
||||||
shared::SetLabelRequest,
|
shared::SetLabelRequest,
|
||||||
shared::GlobalLimitRequest,
|
shared::GlobalLimitRequest,
|
||||||
handlers::auth::LoginRequest,
|
|
||||||
handlers::setup::SetupRequest,
|
|
||||||
handlers::setup::SetupStatusResponse,
|
|
||||||
handlers::auth::UserResponse
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
tags(
|
tags(
|
||||||
@@ -144,6 +132,7 @@ struct Args {
|
|||||||
)]
|
)]
|
||||||
struct ApiDoc;
|
struct ApiDoc;
|
||||||
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
// Load .env file
|
// Load .env file
|
||||||
@@ -423,6 +412,11 @@ async fn main() {
|
|||||||
#[cfg(feature = "swagger")]
|
#[cfg(feature = "swagger")]
|
||||||
let app = app.merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi()));
|
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)
|
// Setup & Auth Routes (cookie-based, stay as REST)
|
||||||
let scgi_path_for_ctx = args.socket.clone();
|
let scgi_path_for_ctx = args.socket.clone();
|
||||||
let db_for_ctx = db.clone();
|
let db_for_ctx = db.clone();
|
||||||
|
|||||||
@@ -1,16 +1,3 @@
|
|||||||
use governor::clock::QuantaInstant;
|
// This file can be removed or repurposed if rate limiting is needed for other endpoints.
|
||||||
use governor::middleware::NoOpMiddleware;
|
// Login rate limiting is now handled within the server function or needs to be reimplemented
|
||||||
use tower_governor::governor::GovernorConfig;
|
// as a middleware for the server function endpoint.
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use shared::xmlrpc::{
|
|||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use axum::response::sse::{Event, Sse};
|
use axum::response::sse::{Event, Sse};
|
||||||
use futures::stream::{self, Stream};
|
use futures::stream::{self};
|
||||||
use shared::{AppEvent, GlobalStats, Torrent, TorrentStatus};
|
use shared::{AppEvent, GlobalStats, Torrent, TorrentStatus};
|
||||||
use std::convert::Infallible;
|
use std::convert::Infallible;
|
||||||
use tokio_stream::StreamExt;
|
use tokio_stream::StreamExt;
|
||||||
|
|||||||
Reference in New Issue
Block a user