Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
221 changes: 152 additions & 69 deletions backend/Cargo.lock

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions backend/modules/api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ edition = "2021"

[dependencies]
dotenv = "0.15.0"
env_logger = "0.11.8"
log = "0.4"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
tracing-actix-web = "0.7"
actix-web = "4"
actix = "0.13"
actix-web-actors = "4"
Expand Down
5 changes: 3 additions & 2 deletions backend/modules/api/src/ai.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use dto::{
responses::ValidationErrorResponse,
};
use serde_json::json;
use tracing::error;
use validator::Validate;

use service::engine_service::EngineService;
Expand Down Expand Up @@ -47,7 +48,7 @@ pub async fn get_ai_suggestion(payload: Json<AiSuggestionRequest>) -> HttpRespon
computation_time_ms: elapsed,
}),
Err(e) => {
log::error!("Engine error in get_ai_suggestion: {}", e);
error!("Engine error in get_ai_suggestion: {}", e);
HttpResponse::InternalServerError().json(json!({
"error": "internal server error"
}))
Expand Down Expand Up @@ -104,7 +105,7 @@ pub async fn analyze_position(payload: Json<PositionAnalysisRequest>) -> HttpRes
})
}
Err(e) => {
log::error!("Engine error in analyze_position: {}", e);
error!("Engine error in analyze_position: {}", e);
HttpResponse::InternalServerError().json(json!({
"error": "internal server error"
}))
Expand Down
9 changes: 5 additions & 4 deletions backend/modules/api/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use actix_web::{
post, web, HttpRequest, HttpResponse,
};
use std::env;
use tracing::{error, warn};
use uuid::Uuid;
use validator::Validate;

Expand Down Expand Up @@ -129,7 +130,7 @@ pub async fn login(
{
Ok(t) => t,
Err(e) => {
log::error!("Failed to generate refresh token: {}", e);
error!("Failed to generate refresh token: {}", e);
return HttpResponse::InternalServerError().json(ErrorResponse {
message: "Failed to generate refresh token".to_string(),
code: "TOKEN_ERROR".to_string(),
Expand Down Expand Up @@ -241,7 +242,7 @@ pub async fn refresh(
{
Ok(fid) => fid,
Err(TokenServiceError::TokenReuseDetected) => {
log::warn!("Token reuse detected for player {}", claims.user_id);
warn!("Token reuse detected for player {}", claims.user_id);
return HttpResponse::Unauthorized().json(ErrorResponse {
message: "Token reuse detected. Account locked for security.".to_string(),
code: "TOKEN_THEFT_DETECTED".to_string(),
Expand Down Expand Up @@ -289,7 +290,7 @@ pub async fn refresh(
{
Ok(t) => t,
Err(e) => {
log::error!("Failed to generate new refresh token: {}", e);
error!("Failed to generate new refresh token: {}", e);
return HttpResponse::InternalServerError().json(ErrorResponse {
message: "Failed to generate new refresh token".to_string(),
code: "TOKEN_ERROR".to_string(),
Expand Down Expand Up @@ -377,7 +378,7 @@ pub async fn logout(

// Revoke all tokens for this player
if let Err(e) = TokenService::revoke_player_tokens(db.get_ref(), user_id).await {
log::error!("Failed to revoke tokens: {}", e);
error!("Failed to revoke tokens: {}", e);
return HttpResponse::InternalServerError().json(ErrorResponse {
message: "Failed to logout".to_string(),
code: "LOGOUT_ERROR".to_string(),
Expand Down
29 changes: 27 additions & 2 deletions backend/modules/api/src/games.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,32 @@ pub async fn list_games(
let limit = query.limit.unwrap_or(10);
let cursor = query.cursor.clone();

match GameService::list_games(db.get_ref(), cursor, limit, query.player_id, status_enum).await {
Ok((games, next_cursor)) => {
// Compute offset from page (if page is provided and cursor is not)
let offset: Option<u64> = if cursor.is_none() {
query.page.map(|p| {
let page = if p < 1 {
tracing::warn!("Invalid page value {} — clamping to 1", p);
1
} else {
p as u64
};
(page - 1) * limit
})
} else {
None
};

match GameService::list_games(
db.get_ref(),
cursor,
offset,
limit,
query.player_id,
status_enum,
)
.await
{
Ok((games, next_cursor, total_count)) => {
let game_dtos: Vec<serde_json::Value> = games
.into_iter()
.map(|g| {
Expand All @@ -230,6 +254,7 @@ pub async fn list_games(
"message": "Games found",
"data": {
"games": game_dtos,
"total_count": total_count,
"next_cursor": next_cursor,
"limit": limit,
}
Expand Down
5 changes: 3 additions & 2 deletions backend/modules/api/src/rate_limiter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use std::{
rc::Rc,
task::{Context, Poll},
};
use tracing::warn;

/// Redis-backed rate limiter middleware for actix-web.
///
Expand Down Expand Up @@ -114,7 +115,7 @@ where
let mut conn = match pool.get().await {
Ok(c) => c,
Err(e) => {
log::warn!(
warn!(
"Redis rate limiter connection failed: {}. Allowing request.",
e
);
Expand All @@ -130,7 +131,7 @@ where
{
Ok(c) => c,
Err(e) => {
log::warn!("Redis INCR failed: {}. Allowing request.", e);
warn!("Redis INCR failed: {}. Allowing request.", e);
return service.call(req).await.map(ServiceResponse::map_into_boxed_body);
}
};
Expand Down
60 changes: 38 additions & 22 deletions backend/modules/api/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ use challenge::api::configure_puzzle_routes;
use challenge::puzzle_validation::PuzzleValidationService;
use dotenv::dotenv;
use matchmaking::redis::{create_redis_pool, test_redis_connection};
use matchmaking::service::MatchmakingService;
use migration::{Migrator, MigratorTrait};
use matchmaking::MatchmakingService;
use migration::Migrator;
use migration::MigratorTrait;
use security::jwt::{JwtAuthMiddleware, JwtService};
use tracing::{info, warn, error};
use tracing_actix_web::TracingLogger;
use sea_orm::Database;
use security::JwtAuthMiddleware;
use security::JwtService;
use st_core::endpoint::configure as configure_nft_routes;
use std::env;
use std::sync::Arc;
use utoipa::OpenApi;
Expand All @@ -49,8 +50,24 @@ pub async fn main() -> std::io::Result<()> {
// Load environment variables from .env file
dotenv().ok();

// Initialize logger
env_logger::init();
// Initialize structured logger with JSON output in release builds
{
use tracing_subscriber::EnvFilter;

let env_filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("info"));

let subscriber = tracing_subscriber::fmt()
.with_env_filter(env_filter);

#[cfg(debug_assertions)]
let subscriber = subscriber.pretty();

#[cfg(not(debug_assertions))]
let subscriber = subscriber.json();

subscriber.init();
}

// Load configuration from environment — critical secrets have no fallbacks (BE-27)
let server_addr = env::var("SERVER_ADDR").unwrap_or_else(|_| "127.0.0.1:8080".to_string());
Expand All @@ -66,18 +83,19 @@ pub async fn main() -> std::io::Result<()> {
"REDIS_URL must be set. Refusing to start with a hardcoded fallback.",
);

eprintln!("Initializing KnightVerse Backend Server");
eprintln!("Server address: {}", server_addr);
info!("Initializing KnightVerse Backend Server");
info!("Server address: {}", server_addr);

// Connect to database
let db = match Database::connect(&database_url).await {
Ok(conn) => {
eprintln!("Database connection successful");
info!("Database connection successful");
conn
}
Err(e) => {
eprintln!("Failed to connect to database: {}", e);
return Err(std::io::Error::other(
error!("Failed to connect to database: {}", e);
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
"Database connection failed",
));
}
Expand All @@ -102,12 +120,12 @@ pub async fn main() -> std::io::Result<()> {
let config = AppConfig::from_env();

// Initialize Matchmaking
eprintln!("Connecting to Redis for matchmaking at {}", redis_url);
info!("Connecting to Redis for matchmaking at {}", redis_url);
let redis_pool = create_redis_pool(&redis_url).expect("Failed to create Redis pool");

// Optional: test connection
if let Err(e) = test_redis_connection(&redis_pool).await {
eprintln!("Warning: Redis connection test failed: {}", e);
warn!("Warning: Redis connection test failed: {}", e);
}

let rate_limiter_pool = redis_pool.clone();
Expand All @@ -116,7 +134,7 @@ pub async fn main() -> std::io::Result<()> {
// Initialize Puzzle Validation Service
let puzzle_service = Arc::new(PuzzleValidationService::new(jwt_secret.clone()));

eprintln!("Starting HTTP server on {}", server_addr);
info!("Starting HTTP server on {}", server_addr);

// Define the app factory closure
let app_factory = move || {
Expand Down Expand Up @@ -179,10 +197,8 @@ pub async fn main() -> std::io::Result<()> {
);

App::new()
.wrap(actix_web::middleware::DefaultHeaders::new().add((
"Strict-Transport-Security",
"max-age=31536000; includeSubDomains",
)))
.wrap(TracingLogger::default())
.wrap(actix_web::middleware::DefaultHeaders::new().add(("Strict-Transport-Security", "max-age=31536000; includeSubDomains")))
// Global middleware
.wrap(cors)
// App data
Expand Down Expand Up @@ -241,8 +257,8 @@ pub async fn main() -> std::io::Result<()> {
.service(get_ai_suggestion)
.service(analyze_position),
)
// NFT routes
.service(web::scope("/api/v1").configure(configure_nft_routes))
// NFT routes (placeholder — not yet implemented)
// .service(web::scope("/api/v1").configure(configure_nft_routes))
// Swagger UI integration
.service(
SwaggerUi::new("/api/docs/{_:.*}")
Expand All @@ -266,7 +282,7 @@ pub async fn main() -> std::io::Result<()> {

if let Ok(workers_str) = env::var("WORKERS") {
if let Ok(workers) = workers_str.parse::<usize>() {
println!("Setting worker count to {}", workers);
info!("Setting worker count to {}", workers);
http_server = http_server.workers(workers);
}
}
Expand Down
9 changes: 5 additions & 4 deletions backend/modules/api/src/ws.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::collections::{HashMap, HashSet};
use std::env;
use tracing::{error, info, warn};
use uuid::Uuid;

// For Redis Pub/Sub
Expand Down Expand Up @@ -158,7 +159,7 @@ impl WsSession {
ctx.run_interval(Self::HEARTBEAT_INTERVAL, |act, ctx| {
let elapsed = std::time::Instant::now().duration_since(act.hb);
if elapsed > Self::CLIENT_TIMEOUT {
log::warn!(
warn!(
"WebSocket timeout for game {}: no pong in {}s, terminating connection",
act.game_id,
elapsed.as_secs()
Expand Down Expand Up @@ -187,7 +188,7 @@ impl Actor for WsSession {
}

fn stopped(&mut self, ctx: &mut Self::Context) {
log::info!("WebSocket disconnected for game: {}", self.game_id);
info!("WebSocket disconnected for game: {}", self.game_id);

// Send reconnection token to client for seamless reconnection
if let Ok(reconnect_token) = self.generate_reconnect_token() {
Expand All @@ -198,9 +199,9 @@ impl Actor for WsSession {

// Try to send the reconnection token
ctx.address().do_send(reconnect_msg);
log::info!("Sent reconnection token for user: {}", self.username);
info!("Sent reconnection token for user: {}", self.username);
} else {
log::error!(
error!(
"Failed to generate reconnection token for user: {}",
self.username
);
Expand Down
2 changes: 1 addition & 1 deletion backend/modules/matchmaking/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ uuid = { version = "1", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
deadpool-redis = "0.14"
redis = { version = "0.24", features = ["tokio-comp", "json"] }
log = "0.4"
tracing = "0.1"
9 changes: 5 additions & 4 deletions backend/modules/matchmaking/routes.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use actix_web::{web, HttpResponse, Responder};
use chrono::Utc;
use serde::{Deserialize, Serialize};
use tracing::error;
use uuid::Uuid;

use super::models::*;
Expand Down Expand Up @@ -76,7 +77,7 @@ async fn join_queue(
match service.join_queue(match_request).await {
Ok(response) => HttpResponse::Ok().json(response),
Err(e) => {
log::error!("Failed to join queue: {}", e);
error!("Failed to join queue: {}", e);
HttpResponse::ServiceUnavailable().json(ErrorResponse {
status: "error".to_string(),
error: "internal_error".to_string(),
Expand All @@ -101,7 +102,7 @@ async fn get_status(
queue_status: None,
}),
Err(e) => {
log::error!("Failed to get queue status: {}", e);
error!("Failed to get queue status: {}", e);
HttpResponse::ServiceUnavailable().json(ErrorResponse {
status: "error".to_string(),
error: "internal_error".to_string(),
Expand All @@ -122,7 +123,7 @@ async fn cancel_request(
"status": "Request not found"
})),
Err(e) => {
log::error!("Failed to cancel request: {}", e);
error!("Failed to cancel request: {}", e);
HttpResponse::ServiceUnavailable().json(ErrorResponse {
status: "error".to_string(),
error: "internal_error".to_string(),
Expand Down Expand Up @@ -150,7 +151,7 @@ async fn accept_invite(
"status": "Invite not found"
})),
Err(e) => {
log::error!("Failed to accept invite: {}", e);
error!("Failed to accept invite: {}", e);
HttpResponse::ServiceUnavailable().json(ErrorResponse {
status: "error".to_string(),
error: "internal_error".to_string(),
Expand Down
2 changes: 1 addition & 1 deletion backend/modules/service/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ chrono = { version = "0.4", features = ["serde"] }
base64 = "0.22"
tokio = { version = "1", features = ["full", "sync"] }
serde_json = "1"
log = "0.4"
tracing = "0.1"

dto = { path = "../dto"}
db = {path = "../db"}
Expand Down
Loading