From 75c8202b9ce4afd039ce5243014d4102abbd7054 Mon Sep 17 00:00:00 2001 From: Finnik <73883383+golkity@users.noreply.github.com> Date: Fri, 22 May 2026 10:22:24 +0300 Subject: [PATCH 01/23] Feat(core & env): init Dockerfile and dockerignore for bff service and init cargo envirment and using slim docker --- backend/service/bff_service/.dockerignore | 6 ++++++ backend/service/bff_service/Cargo.toml | 7 +++++++ backend/service/bff_service/Dockerfile | 14 ++++++++++++++ 3 files changed, 27 insertions(+) create mode 100644 backend/service/bff_service/.dockerignore create mode 100644 backend/service/bff_service/Cargo.toml create mode 100644 backend/service/bff_service/Dockerfile diff --git a/backend/service/bff_service/.dockerignore b/backend/service/bff_service/.dockerignore new file mode 100644 index 0000000..584000c --- /dev/null +++ b/backend/service/bff_service/.dockerignore @@ -0,0 +1,6 @@ +target/ +**/*.rs.bk +Cargo.lock +.git/ +.env +Dockerfile diff --git a/backend/service/bff_service/Cargo.toml b/backend/service/bff_service/Cargo.toml new file mode 100644 index 0000000..a35e4e1 --- /dev/null +++ b/backend/service/bff_service/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "bff-service" +version = "0.1.0" +edition = "2021" + +[dependencies] +rust-core = { path = "../../libs/rust-core" } diff --git a/backend/service/bff_service/Dockerfile b/backend/service/bff_service/Dockerfile new file mode 100644 index 0000000..e65591a --- /dev/null +++ b/backend/service/bff_service/Dockerfile @@ -0,0 +1,14 @@ +FROM rust:1.85-slim-bookworm AS builder +RUN apt-get update && apt-get install -y pkg-config libssl-dev cmake build-essential +WORKDIR /app + +COPY . . +RUN cargo build --release -p bff-service + +FROM gcr.io/distroless/cc-debian12 +WORKDIR /app +COPY --from=builder /app/target/release/bff-service /app/bff-service +ENV RUST_LOG=info +ENV PORT=8000 +EXPOSE 8000 +CMD ["/app/bff-service"] From ea1763c657e2877b723f2fe148b2bc5766a7d107 Mon Sep 17 00:00:00 2001 From: Finnik <73883383+golkity@users.noreply.github.com> Date: Fri, 22 May 2026 14:29:46 +0300 Subject: [PATCH 02/23] Feat(auth.rs): impl login handler and init crate with models --- .../bff_service/src/api/handlers/auth.rs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 backend/service/bff_service/src/api/handlers/auth.rs diff --git a/backend/service/bff_service/src/api/handlers/auth.rs b/backend/service/bff_service/src/api/handlers/auth.rs new file mode 100644 index 0000000..dcc6ecc --- /dev/null +++ b/backend/service/bff_service/src/api/handlers/auth.rs @@ -0,0 +1,34 @@ +use axum::{extract::State, Json}; +use std::sync::Arc; + +use crate::{ + models::{ + requests::{LoginRequest, RegisterRequest}, + responses::{LoginResponse, RegisterResponse}, + }, + utils::errors::AppError, + AppState, +}; + +pub async fn login_handler( + State(state): State>, + Json(payload): Json, +) -> Result, AppError> { + if payload.email.is_empty() || payload.password.is_empty() { + return Err(AppError::BadRequest( + "Email and password are required".to_string() + )); + } + + let mut auth_client = + state.auth_grpc_client.clone(); + + let grpc_res = + auth_client.login(payload.email, payload.password).await?; + + Ok(Json(LoginResponse{ + access_token: grpc_res.access_token, + refresh_token: grpc_res.refresh_token, + expires_in: grpc_res.expires_in, + })) +} From fe681470443f5d7a766a780f75e6cc781afd0102 Mon Sep 17 00:00:00 2001 From: Finnik <73883383+golkity@users.noreply.github.com> Date: Fri, 22 May 2026 14:44:43 +0300 Subject: [PATCH 03/23] Feat(auth.rs): impl register_handler --- .../bff_service/src/api/handlers/auth.rs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/backend/service/bff_service/src/api/handlers/auth.rs b/backend/service/bff_service/src/api/handlers/auth.rs index dcc6ecc..9a1003d 100644 --- a/backend/service/bff_service/src/api/handlers/auth.rs +++ b/backend/service/bff_service/src/api/handlers/auth.rs @@ -10,6 +10,7 @@ use crate::{ AppState, }; + pub async fn login_handler( State(state): State>, Json(payload): Json, @@ -32,3 +33,24 @@ pub async fn login_handler( expires_in: grpc_res.expires_in, })) } + + +pub async fn register_handler( + State(state): State>, + Json(payload): Json, +) -> Result, AppError> { + if payload.username .is_empty() { + return Err(AppError::BadRequest( + "Username is required".to_string() + )) + } + + let mut auth_client = state.auth_grpc_client.clone(); + let grpc_res = auth_client.register(payload.username, + payload.email, payload.password).await?; + + Ok(Json(RegisterResponse { + id: grpc_res.id, + message: grpc_res.message, + })) +} From 86eedccd5b5261ec99a9ba4eca1d34cf5c26cf2c Mon Sep 17 00:00:00 2001 From: Finnik <73883383+golkity@users.noreply.github.com> Date: Fri, 22 May 2026 15:33:53 +0300 Subject: [PATCH 04/23] Feat(stream.rs):impl stream handler and impl function GET live streams --- .../bff_service/src/api/handlers/stream.rs | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 backend/service/bff_service/src/api/handlers/stream.rs diff --git a/backend/service/bff_service/src/api/handlers/stream.rs b/backend/service/bff_service/src/api/handlers/stream.rs new file mode 100644 index 0000000..6b4c36e --- /dev/null +++ b/backend/service/bff_service/src/api/handlers/stream.rs @@ -0,0 +1,42 @@ +use axum::{extract::{ + State, Query +}, Json}; +use std::sync::Arc; + + +use crate::{ + models::{ + requests ::GetStreamsQeury, + responses::{StreamCatalogItem, StreamCatalogResponse} + } + utils::errors::AppError, + AppState, +}; + + +pub async fn get_live_streams_handler( + State(state): State>, + Query(query): Query, +) -> Result, AppError> { + let limit = query.limit.unwrap_or(20); + let cursor = query.cursor.unwrap_or_default(); + + let mut stream_client = + state.stream_meta_grpc_client.clone(); + + let grpc_res = + stream_client.get_live_streams(limit, + cursor).await?; + + let streams = grpc_res.streams.into_iter().map(|s| StreamCatalogItem { + stream_id: s.stream_id, + title: s.title, + category: s.category, + viewers_count: s.viewers_count, + }).collect(); + + Ok(Json(StreamCatalogResponse { + data: streams, + next_cursor: grpc_res.next_cursor, + })) +} From 338b1b33907f2d7dad19d5e18bcca34fde6a6a58 Mon Sep 17 00:00:00 2001 From: Finnik <73883383+golkity@users.noreply.github.com> Date: Fri, 22 May 2026 15:41:29 +0300 Subject: [PATCH 05/23] Feat(mod):init module handlers --- backend/service/bff_service/src/api/handlers/mod.rs | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 backend/service/bff_service/src/api/handlers/mod.rs diff --git a/backend/service/bff_service/src/api/handlers/mod.rs b/backend/service/bff_service/src/api/handlers/mod.rs new file mode 100644 index 0000000..1d16316 --- /dev/null +++ b/backend/service/bff_service/src/api/handlers/mod.rs @@ -0,0 +1,2 @@ +pub mod auth; +pub mod stream; From 2e3f4bd3325c35c4aa277918bb0f17689a76127a Mon Sep 17 00:00:00 2001 From: Finnik <73883383+golkity@users.noreply.github.com> Date: Fri, 22 May 2026 16:02:22 +0300 Subject: [PATCH 06/23] Feat(routes): impl route strcut and impl handlers with module and init some route --- backend/service/bff_service/src/api/routes/auth.rs | 12 ++++++++++++ backend/service/bff_service/src/api/routes/mod.rs | 2 ++ backend/service/bff_service/src/api/routes/stream.rs | 7 +++++++ 3 files changed, 21 insertions(+) create mode 100644 backend/service/bff_service/src/api/routes/auth.rs create mode 100644 backend/service/bff_service/src/api/routes/mod.rs create mode 100644 backend/service/bff_service/src/api/routes/stream.rs diff --git a/backend/service/bff_service/src/api/routes/auth.rs b/backend/service/bff_service/src/api/routes/auth.rs new file mode 100644 index 0000000..2473489 --- /dev/null +++ b/backend/service/bff_service/src/api/routes/auth.rs @@ -0,0 +1,12 @@ +use crate::{api::handlers::auth, middleware::rate_limiter, AppState}; +use axum::{routing::post, Router}; +use std::sync::Arc; + +pub fn router() -> Router> { + Router::new() + .route("/login", post(auth::login_handler)) + .route("/register", post(auth::register_handler)) + .route_layer(axum::middleware::from_fn_with_state( + //pupupupuppupupu + )) +} diff --git a/backend/service/bff_service/src/api/routes/mod.rs b/backend/service/bff_service/src/api/routes/mod.rs new file mode 100644 index 0000000..1d16316 --- /dev/null +++ b/backend/service/bff_service/src/api/routes/mod.rs @@ -0,0 +1,2 @@ +pub mod auth; +pub mod stream; diff --git a/backend/service/bff_service/src/api/routes/stream.rs b/backend/service/bff_service/src/api/routes/stream.rs new file mode 100644 index 0000000..9f6496a --- /dev/null +++ b/backend/service/bff_service/src/api/routes/stream.rs @@ -0,0 +1,7 @@ +use crate::{api::handlers::stream, AppState}; +use axum::{routing::get, Router}; +use std::sync::Arc; + +pub fn router() -> Router> { + Router::new().route("/live", get(stream::get_live_streams_handler)) +} From 33c82094adde3c163781d1edc0192afa1b06a10b Mon Sep 17 00:00:00 2001 From: Finnik <73883383+golkity@users.noreply.github.com> Date: Fri, 22 May 2026 16:07:22 +0300 Subject: [PATCH 07/23] Feat(module): init mod --- backend/service/bff_service/src/api/mod.rs | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 backend/service/bff_service/src/api/mod.rs diff --git a/backend/service/bff_service/src/api/mod.rs b/backend/service/bff_service/src/api/mod.rs new file mode 100644 index 0000000..c0c696a --- /dev/null +++ b/backend/service/bff_service/src/api/mod.rs @@ -0,0 +1,2 @@ +pub mod handlers; +pub mod routes; From 495b22263030f10487fe66c55309e00bfbefdc6c Mon Sep 17 00:00:00 2001 From: Finnik <73883383+golkity@users.noreply.github.com> Date: Fri, 22 May 2026 16:24:26 +0300 Subject: [PATCH 08/23] Feat(grpc): init grpc client and imp module for auth client --- .../src/grpc_clients/auth_client.rs | 30 +++++++++++++++++++ .../bff_service/src/grpc_clients/mod.rs | 11 +++++++ .../src/grpc_clients/stream_meta_client.rs | 23 ++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 backend/service/bff_service/src/grpc_clients/auth_client.rs create mode 100644 backend/service/bff_service/src/grpc_clients/mod.rs create mode 100644 backend/service/bff_service/src/grpc_clients/stream_meta_client.rs diff --git a/backend/service/bff_service/src/grpc_clients/auth_client.rs b/backend/service/bff_service/src/grpc_clients/auth_client.rs new file mode 100644 index 0000000..772c1a3 --- /dev/null +++ b/backend/service/bff_service/src/grpc_clients/auth_client.rs @@ -0,0 +1,30 @@ +use crate::utils::errors::AppError; +use super::pb::auth::{auth_service_client::AuthServiceClient, + LoginRequest, LoginResponse, RegisterRequest, RegisterResponse}; +use tonic::transport::Channel; + +#[derive(Clone)] +pub struct AuthGrpcClient { + client: AuthServiceClient, +} + +impl AuthGrpcClient { + pub async fn connect(url: String) -> Result> { + let client = AuthServiceClient::connect(url).await?; + Ok(Self { client }) + } + + pub async fn login(&mut self, email: String, + password: String) -> Result { + let request = tonic::Request::new(LoginRequest { email, password }); + let response = self.client.login(request).await?; + Ok(response.into_inner()) + } + + pub async fn register(&mut self, username: String, email: String, + password: String) -> Result { + let request = tonic::Request::new(RegisterRequest { username, email, password }); + let response = self.client.register(request).await?; + Ok(response.into_inner()) + } +} diff --git a/backend/service/bff_service/src/grpc_clients/mod.rs b/backend/service/bff_service/src/grpc_clients/mod.rs new file mode 100644 index 0000000..81097bd --- /dev/null +++ b/backend/service/bff_service/src/grpc_clients/mod.rs @@ -0,0 +1,11 @@ +pub mod auth_client; +pub mod stream_meta_client; + +pub mod pb { + pub mod auth { + tonic::include_proto!("stremo.auth.v1"); + } + pub mod stream_meta { + tonic::include_proto!("stremo.stream_meta.v1"); + } +} diff --git a/backend/service/bff_service/src/grpc_clients/stream_meta_client.rs b/backend/service/bff_service/src/grpc_clients/stream_meta_client.rs new file mode 100644 index 0000000..b76fc99 --- /dev/null +++ b/backend/service/bff_service/src/grpc_clients/stream_meta_client.rs @@ -0,0 +1,23 @@ +use crate::utils::errors::AppError; +use super::pb::stream_meta::{stream_meta_service_client::StreamMetaServiceClient, + GetLiveStreamsRequest, GetLiveStreamsResponse}; +use tonic::transport::Channel; + +#[derive(Clone)] +pub struct StreamMetaGrpcClient { + client: StreamMetaServiceClient, +} + +impl StreamMetaGrpcClient { + pub async fn connect(url: String) -> Result> { + let client = StreamMetaServiceClient::connect(url).await?; + Ok(Self { client }) + } + + pub async fn get_live_streams(&mut self, limit: i32, + cursor: String) -> Result { + let request = tonic::Request::new(GetLiveStreamsRequest { limit, cursor }); + let response = self.client.get_live_streams(request).await?; + Ok(response.into_inner()) + } +} From 0dcddcdfbfb33759ba9254942c3de9f559beeb50 Mon Sep 17 00:00:00 2001 From: Finnik <73883383+golkity@users.noreply.github.com> Date: Sat, 23 May 2026 14:28:37 +0300 Subject: [PATCH 09/23] Feat(models): init module file --- backend/service/bff_service/src/models/mod.rs | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 backend/service/bff_service/src/models/mod.rs diff --git a/backend/service/bff_service/src/models/mod.rs b/backend/service/bff_service/src/models/mod.rs new file mode 100644 index 0000000..116da0f --- /dev/null +++ b/backend/service/bff_service/src/models/mod.rs @@ -0,0 +1,2 @@ +pub mod requests; +pub mod responses; From f6375e852ae00788c27b74ae5f70f4691096f75a Mon Sep 17 00:00:00 2001 From: Finnik <73883383+golkity@users.noreply.github.com> Date: Sat, 23 May 2026 14:29:46 +0300 Subject: [PATCH 10/23] Feat(Response & Request): impl pub struct for request and response --- .../bff_service/src/models/requests.rs | 13 +++++++++ .../bff_service/src/models/responses.rs | 28 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 backend/service/bff_service/src/models/requests.rs create mode 100644 backend/service/bff_service/src/models/responses.rs diff --git a/backend/service/bff_service/src/models/requests.rs b/backend/service/bff_service/src/models/requests.rs new file mode 100644 index 0000000..3942383 --- /dev/null +++ b/backend/service/bff_service/src/models/requests.rs @@ -0,0 +1,13 @@ +use serde::Deserialize; + +#[derive(Deserialize)] +pub struct RegisterRequest { + pub email: String, + pub password: String, +} + +#[derive(Deserialize)] +pub struct GetStreamsQuery { + pub limit: Option, + pub cursor: Option, +} diff --git a/backend/service/bff_service/src/models/responses.rs b/backend/service/bff_service/src/models/responses.rs new file mode 100644 index 0000000..bbfb18f --- /dev/null +++ b/backend/service/bff_service/src/models/responses.rs @@ -0,0 +1,28 @@ +use serde::Serialize; + +#[derive(Serialize)] +pub struct LoginResponse { + pub access_token: String, + pub refresh_token: String, + pub exprise_in: u32, +} + +#[derive(Serialize)] +pub struct RegisterResponse { + pub id: String, + pub message: String, +} + +#[derive(Serialize)] +pub struct StreamCatalogItem { + pub stream_id: String, + pub title: String, + pub category: String, + pub viewers_count: i32, +} + +#[derive(Serialize)] +pub struct StreamCatalogResponse { + pub data: Vec, + pub next_cursor: String, +} From fc98af5a878ee4c9096dc350cb19ed40bed1194e Mon Sep 17 00:00:00 2001 From: Finnik <73883383+golkity@users.noreply.github.com> Date: Sat, 23 May 2026 14:30:45 +0300 Subject: [PATCH 11/23] Feat(utils): init module for utils errors and state --- backend/service/bff_service/src/utils/mod.rs | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 backend/service/bff_service/src/utils/mod.rs diff --git a/backend/service/bff_service/src/utils/mod.rs b/backend/service/bff_service/src/utils/mod.rs new file mode 100644 index 0000000..a209a6f --- /dev/null +++ b/backend/service/bff_service/src/utils/mod.rs @@ -0,0 +1,2 @@ +pub mod errors; +pub mod state; From 8d850069974aa4f52e1be78c6d780dba335b02ec Mon Sep 17 00:00:00 2001 From: Finnik <73883383+golkity@users.noreply.github.com> Date: Sat, 23 May 2026 14:31:46 +0300 Subject: [PATCH 12/23] Feat(utils): impl state pub struct is AppState --- backend/service/bff_service/src/utils/state.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 backend/service/bff_service/src/utils/state.rs diff --git a/backend/service/bff_service/src/utils/state.rs b/backend/service/bff_service/src/utils/state.rs new file mode 100644 index 0000000..62b8810 --- /dev/null +++ b/backend/service/bff_service/src/utils/state.rs @@ -0,0 +1,10 @@ +use crate::{config, grpc_clients}; +use bb8::Pool; +use bb8_redis::RedisConnectionManager; + +pub struct AppState { + pub config: config::Config, + pub redis_pool: Pool, + pub auth_grpc_client: grpc_clients::auth_client::AuthGrpcClient, + pub stream_meta_grpc_client: grpc_clients::stream_meta_client::StreamMetaGrpcClient, +} From 85127ed72f432e96a3f52b1a661f71f264a388a5 Mon Sep 17 00:00:00 2001 From: Finnik <73883383+golkity@users.noreply.github.com> Date: Sat, 23 May 2026 14:32:37 +0300 Subject: [PATCH 13/23] Feat(utils): impl custom errors for this service --- .../service/bff_service/src/utils/errors.rs | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 backend/service/bff_service/src/utils/errors.rs diff --git a/backend/service/bff_service/src/utils/errors.rs b/backend/service/bff_service/src/utils/errors.rs new file mode 100644 index 0000000..5590ce9 --- /dev/null +++ b/backend/service/bff_service/src/utils/errors.rs @@ -0,0 +1,80 @@ +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, + Json, +}; +use serde_json::json; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum AppError { + #[error("Internal Server Error")] + InternalError(anyhow::Error), + + #[error("Invalid Credentials")] + InvalidCredentials, + + #[error("Unauthorized: {0}")] + Unauthorized(String), + + #[error("Bad Request: {0}")] + BadRequest(String), + + #[error("gRPC Error: {0}")] + GrpcError(String), +} + +impl From for AppError { + fn from(err: anyhow::Error) -> Self { + AppError::InternalError(err) + } +} + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + let (status, error_code, error_message) = match self { + AppError::InternalError(ref e) => { + tracing::error!("Internal Server Error: {:?}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + "INTERNAL_ERROR", + "Что-то пошло не так на сервере".to_string(), + ) + } + AppError::InvalidCredentials => ( + StatusCode::UNAUTHORIZED, + "INVALID_CREDENTIALS", + "Неверный email или пароль".to_string(), + ), + AppError::Unauthorized(ref msg) => { + (StatusCode::UNAUTHORIZED, "UNAUTHORIZED", msg.to_string()) + } + AppError::BadRequest(ref msg) => { + (StatusCode::BAD_REQUEST, "BAD_REQUEST", msg.to_string()) + } + AppError::GrpcError(ref msg) => { + tracing::error!("gRPC call failed: {}", msg); + ( + StatusCode::BAD_GATEWAY, + "UPSTREAM_ERROR", + "Внутренний сервис недоступен".to_string(), + ) + } + }; + + let body = Json(json!({ + "error": { + "code": error_code, + "message": error_message + } + })); + + (status, body).into_response() + } +} + +impl From for AppError { + fn from(status: tonic::Status) -> Self { + AppError::GrpcError(status.message().to_string()) + } +} From 24c3553662e5c70ccbdce8c595588bcf4c9c10ec Mon Sep 17 00:00:00 2001 From: Finnik <73883383+golkity@users.noreply.github.com> Date: Sat, 23 May 2026 14:34:17 +0300 Subject: [PATCH 14/23] Feat(config): realised impl Config with pub from-env and impl Config struct --- backend/service/bff_service/src/config.rs | 29 +++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 backend/service/bff_service/src/config.rs diff --git a/backend/service/bff_service/src/config.rs b/backend/service/bff_service/src/config.rs new file mode 100644 index 0000000..e3d25a3 --- /dev/null +++ b/backend/service/bff_service/src/config.rs @@ -0,0 +1,29 @@ +#[derive(Clone, Debug)] +pub struct Config { + pub port: u16, + pub redis_url: String, + pub auth_service_grpc_url: String, + pub stream_meta_grpc_url: String, + pub jwt_secret: String, +} + +impl Config { + pub fn from_env() -> Self { + dotenvy::dotenv().ok(); + + Self { + port: std::env::var("PORT") + .unwrap_or_else(|_| "8000".to_string()) + .parse() + .expect("PORT must be a number"), + redis_url: std::env::var("REDIS_URL") + .unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string()), + auth_service_grpc_url: std::env::var("AUTH_SERVICE_GRPC_URL") + .unwrap_or_else(|_| "http://127.0.0.1:50051".to_string()), + stream_meta_grpc_url: std::env::var("STREAM_META_SERVICE_GRPC_URL") + .unwrap_or_else(|_| "http://127.0.0.1:50052".to_string()), + jwt_secret: std::env::var("JWT_SECRET") + .unwrap_or_else(|_| "super_secret_key_for_local_dev".to_string()), + } + } +} From 464965a51be2612f80c235cf692a8d46bc7ef11e Mon Sep 17 00:00:00 2001 From: Finnik <73883383+golkity@users.noreply.github.com> Date: Sat, 23 May 2026 20:20:54 +0300 Subject: [PATCH 15/23] Feat(middleware): impl middleware and i ustal boosss --- .../bff_service/src/middleware/jwt_auth.rs | 66 +++++++++++++++++ .../service/bff_service/src/middleware/mod.rs | 2 + .../src/middleware/rate_limiter.rs | 72 +++++++++++++++++++ 3 files changed, 140 insertions(+) create mode 100644 backend/service/bff_service/src/middleware/jwt_auth.rs create mode 100644 backend/service/bff_service/src/middleware/mod.rs create mode 100644 backend/service/bff_service/src/middleware/rate_limiter.rs diff --git a/backend/service/bff_service/src/middleware/jwt_auth.rs b/backend/service/bff_service/src/middleware/jwt_auth.rs new file mode 100644 index 0000000..100d452 --- /dev/null +++ b/backend/service/bff_service/src/middleware/jwt_auth.rs @@ -0,0 +1,66 @@ +use axum::{async_trait, extract::FromRequestParts, http::request::Parts}; +use jsonwebtoken::{decode, DecodingKey, Validation}; +use redis::AsyncCommands; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + +use crate::{utils::errors::AppError, utils::state::AppState}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + pub sub: String, + pub username: String, + pub exp: usize, +} + +#[async_trait] +impl FromRequestParts> for Claims { + type Rejection = AppError; + + async fn from_request_parts( + parts: &mut Parts, + state: &Arc, + ) -> Result { + let auth_header = parts + .headers + .get("Authorization") + .and_then(|h| h.to_str().ok()) + .ok_or_else(|| AppError::Unauthorized("Token is missing".to_string()))?; + + if !auth_header.starts_with("Bearer ") { + return Err(AppError::Unauthorized("Invalid token format".to_string())); + } + + let token = &auth_header[7..]; + + let token_data = decode::( + token, + &DecodingKey::from_secret(state.config.jwt_secret.as_bytes()), + &Validation::default(), + ) + .map_err(|_| AppError::Unauthorized("Invalid token".to_string()))?; + + let mut redis_conn = state.redis_pool.get().await.map_err(|e| { + tracing::error!("Redis pool error: {}", e); + AppError::InternalError(anyhow::anyhow!("DB Connection Error")) + })?; + + let blacklist_key = format!("blacklist:{}", token_data.claims.sub); + let is_blacklisted: bool = redis_conn + .exists(&blacklist_key) + .await + .map_err(|_| AppError::InternalError(anyhow::anyhow!("Redis Error")))?; + + if is_blacklisted { + tracing::warn!( + "Access attempt with revoked token: {}", + token_data.claims.sub + ); + return Err(AppError::Unauthorized( + "Session revoked. Re-login required.".to_string(), + )); + } + + Ok(token_data.claims) + } +} diff --git a/backend/service/bff_service/src/middleware/mod.rs b/backend/service/bff_service/src/middleware/mod.rs new file mode 100644 index 0000000..38b63cd --- /dev/null +++ b/backend/service/bff_service/src/middleware/mod.rs @@ -0,0 +1,2 @@ +pub mod jwt_auth; +pub mod rate_limiter; diff --git a/backend/service/bff_service/src/middleware/rate_limiter.rs b/backend/service/bff_service/src/middleware/rate_limiter.rs new file mode 100644 index 0000000..ce89738 --- /dev/null +++ b/backend/service/bff_service/src/middleware/rate_limiter.rs @@ -0,0 +1,72 @@ +use axum::{ + extract::{Request, State}, + middleware::Next, + response::Response, +}; +use tracing_subscriber::fmt::format; + +use crate::{ + models::requests, + utils::{errors::AppError, state::AppState}, +}; +use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; +use uuid::Uuid; + +pub async fn rate_limit_middleware( + State(state): State>, + req: Request, + next: Next, +) -> Result { + let ip = req + .headers() + .get("x-forwarded-for") + .and_then(|val| val.to_str().ok()) + .and_then(|s| s.split(',').next()) + .map(|s| s.trim().to_string()) + .or_else(|| { + req.headers() + .get("x-real-ip") + .and_then(|val| val.to_str().ok()) + .map(|s| s.to_string()) + }) + .unwrap_or_else(|| "unknown_ip".to_string()); + + let redis_key = format!("rate_limit:ip: {}", ip); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() as u64; + + let window_start = now - 60_000; + let request_id = Uuid::new_v4().to_string(); + + let mut redis_conn = state.redis_pool.get().await.map_err(|e| { + tracing::error!("Redis pool error: {}", e); + AppError::InternalError(anyhow::anyhow!("DB Connection Error")) + })?; + + let (_, _, count, _): ((), (), i32, ()) = redis::pipe() + .atomic() + .zrembyscore(&redis_key, 0, window_start as i64) + .zadd(&redis_key, &request_id, now as i64) + .zcard(&redis_key) + .expire(&redis_key, 60) + .query_async(&mut *redis_conn) + .await + .map_err(|e| { + tracing::error!("Redis pipeline error: {}", e); + AppError::InternalError(anyhow::anyhow!("Redis Error")) + })?; + + if count > 100 { + tracing::warn!( + "Rate limit exceeded (Sliding Window) for IP: {} (count: {})", + ip, + count + ); + return Err(AppError::BadRequest("Too Many Requests".to_string())); + } + + Ok(next.run(req).await) +} From 7589ff29dce19c3a306a53fff990df792da8ed34 Mon Sep 17 00:00:00 2001 From: Finnik <73883383+golkity@users.noreply.github.com> Date: Sat, 23 May 2026 20:22:19 +0300 Subject: [PATCH 16/23] Fix(use crate): fix use crate and impl models --- .../bff_service/src/api/handlers/auth.rs | 27 +++++------ .../bff_service/src/api/handlers/stream.rs | 47 +++++++++---------- .../bff_service/src/api/routes/auth.rs | 5 +- .../bff_service/src/api/routes/stream.rs | 2 +- 4 files changed, 36 insertions(+), 45 deletions(-) diff --git a/backend/service/bff_service/src/api/handlers/auth.rs b/backend/service/bff_service/src/api/handlers/auth.rs index 9a1003d..f753f6f 100644 --- a/backend/service/bff_service/src/api/handlers/auth.rs +++ b/backend/service/bff_service/src/api/handlers/auth.rs @@ -7,47 +7,42 @@ use crate::{ responses::{LoginResponse, RegisterResponse}, }, utils::errors::AppError, - AppState, + utils::state::AppState, }; - pub async fn login_handler( State(state): State>, Json(payload): Json, ) -> Result, AppError> { if payload.email.is_empty() || payload.password.is_empty() { return Err(AppError::BadRequest( - "Email and password are required".to_string() + "Email and password are required".to_string(), )); } - let mut auth_client = - state.auth_grpc_client.clone(); + let mut auth_client = state.auth_grpc_client.clone(); - let grpc_res = - auth_client.login(payload.email, payload.password).await?; + let grpc_res = auth_client.login(payload.email, payload.password).await?; - Ok(Json(LoginResponse{ + Ok(Json(LoginResponse { access_token: grpc_res.access_token, refresh_token: grpc_res.refresh_token, expires_in: grpc_res.expires_in, })) } - pub async fn register_handler( - State(state): State>, + State(state): State>, Json(payload): Json, ) -> Result, AppError> { - if payload.username .is_empty() { - return Err(AppError::BadRequest( - "Username is required".to_string() - )) + if payload.username.is_empty() { + return Err(AppError::BadRequest("Username is required".to_string())); } let mut auth_client = state.auth_grpc_client.clone(); - let grpc_res = auth_client.register(payload.username, - payload.email, payload.password).await?; + let grpc_res = auth_client + .register(payload.username, payload.email, payload.password) + .await?; Ok(Json(RegisterResponse { id: grpc_res.id, diff --git a/backend/service/bff_service/src/api/handlers/stream.rs b/backend/service/bff_service/src/api/handlers/stream.rs index 6b4c36e..8533072 100644 --- a/backend/service/bff_service/src/api/handlers/stream.rs +++ b/backend/service/bff_service/src/api/handlers/stream.rs @@ -1,39 +1,38 @@ -use axum::{extract::{ - State, Query -}, Json}; +use axum::{ + extract::{Query, State}, + Json, +}; use std::sync::Arc; - use crate::{ models::{ - requests ::GetStreamsQeury, - responses::{StreamCatalogItem, StreamCatalogResponse} - } + requests::GetStreamsQuery, + responses::{StreamCatalogItem, StreamCatalogResponse}, + }, utils::errors::AppError, - AppState, + utils::state::AppState, }; - pub async fn get_live_streams_handler( State(state): State>, - Query(query): Query, + Query(query): Query, ) -> Result, AppError> { - let limit = query.limit.unwrap_or(20); + let limit = query.limit.unwrap_or(20); let cursor = query.cursor.unwrap_or_default(); - let mut stream_client = - state.stream_meta_grpc_client.clone(); - - let grpc_res = - stream_client.get_live_streams(limit, - cursor).await?; - - let streams = grpc_res.streams.into_iter().map(|s| StreamCatalogItem { - stream_id: s.stream_id, - title: s.title, - category: s.category, - viewers_count: s.viewers_count, - }).collect(); + let mut stream_client = state.stream_meta_grpc_client.clone(); + let grpc_res = stream_client.get_live_streams(limit, cursor).await?; + + let streams = grpc_res + .streams + .into_iter() + .map(|s| StreamCatalogItem { + stream_id: s.stream_id, + title: s.title, + category: s.category, + viewers_count: s.viewers_count, + }) + .collect(); Ok(Json(StreamCatalogResponse { data: streams, diff --git a/backend/service/bff_service/src/api/routes/auth.rs b/backend/service/bff_service/src/api/routes/auth.rs index 2473489..dde6964 100644 --- a/backend/service/bff_service/src/api/routes/auth.rs +++ b/backend/service/bff_service/src/api/routes/auth.rs @@ -1,4 +1,4 @@ -use crate::{api::handlers::auth, middleware::rate_limiter, AppState}; +use crate::{api::handlers::auth, utils::state::AppState}; use axum::{routing::post, Router}; use std::sync::Arc; @@ -6,7 +6,4 @@ pub fn router() -> Router> { Router::new() .route("/login", post(auth::login_handler)) .route("/register", post(auth::register_handler)) - .route_layer(axum::middleware::from_fn_with_state( - //pupupupuppupupu - )) } diff --git a/backend/service/bff_service/src/api/routes/stream.rs b/backend/service/bff_service/src/api/routes/stream.rs index 9f6496a..14f61cc 100644 --- a/backend/service/bff_service/src/api/routes/stream.rs +++ b/backend/service/bff_service/src/api/routes/stream.rs @@ -1,4 +1,4 @@ -use crate::{api::handlers::stream, AppState}; +use crate::{api::handlers::stream, utils::state::AppState}; use axum::{routing::get, Router}; use std::sync::Arc; From 2815704c1ce23ce38012c41ffb06debfb87a586c Mon Sep 17 00:00:00 2001 From: Finnik <73883383+golkity@users.noreply.github.com> Date: Sat, 23 May 2026 20:24:12 +0300 Subject: [PATCH 17/23] Fix(use crate): add use AppState --- .../src/grpc_clients/auth_client.rs | 27 ++++++++++++++----- .../src/grpc_clients/stream_meta_client.rs | 13 ++++++--- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/backend/service/bff_service/src/grpc_clients/auth_client.rs b/backend/service/bff_service/src/grpc_clients/auth_client.rs index 772c1a3..d609276 100644 --- a/backend/service/bff_service/src/grpc_clients/auth_client.rs +++ b/backend/service/bff_service/src/grpc_clients/auth_client.rs @@ -1,6 +1,8 @@ +use super::pb::auth::{ + auth_service_client::AuthServiceClient, LoginRequest, LoginResponse, RegisterRequest, + RegisterResponse, +}; use crate::utils::errors::AppError; -use super::pb::auth::{auth_service_client::AuthServiceClient, - LoginRequest, LoginResponse, RegisterRequest, RegisterResponse}; use tonic::transport::Channel; #[derive(Clone)] @@ -14,16 +16,27 @@ impl AuthGrpcClient { Ok(Self { client }) } - pub async fn login(&mut self, email: String, - password: String) -> Result { + pub async fn login( + &mut self, + email: String, + password: String, + ) -> Result { let request = tonic::Request::new(LoginRequest { email, password }); let response = self.client.login(request).await?; Ok(response.into_inner()) } - pub async fn register(&mut self, username: String, email: String, - password: String) -> Result { - let request = tonic::Request::new(RegisterRequest { username, email, password }); + pub async fn register( + &mut self, + username: String, + email: String, + password: String, + ) -> Result { + let request = tonic::Request::new(RegisterRequest { + username, + email, + password, + }); let response = self.client.register(request).await?; Ok(response.into_inner()) } diff --git a/backend/service/bff_service/src/grpc_clients/stream_meta_client.rs b/backend/service/bff_service/src/grpc_clients/stream_meta_client.rs index b76fc99..75aac2e 100644 --- a/backend/service/bff_service/src/grpc_clients/stream_meta_client.rs +++ b/backend/service/bff_service/src/grpc_clients/stream_meta_client.rs @@ -1,6 +1,8 @@ +use super::pb::stream_meta::{ + stream_meta_service_client::StreamMetaServiceClient, GetLiveStreamsRequest, + GetLiveStreamsResponse, +}; use crate::utils::errors::AppError; -use super::pb::stream_meta::{stream_meta_service_client::StreamMetaServiceClient, - GetLiveStreamsRequest, GetLiveStreamsResponse}; use tonic::transport::Channel; #[derive(Clone)] @@ -14,8 +16,11 @@ impl StreamMetaGrpcClient { Ok(Self { client }) } - pub async fn get_live_streams(&mut self, limit: i32, - cursor: String) -> Result { + pub async fn get_live_streams( + &mut self, + limit: i32, + cursor: String, + ) -> Result { let request = tonic::Request::new(GetLiveStreamsRequest { limit, cursor }); let response = self.client.get_live_streams(request).await?; Ok(response.into_inner()) From 09e719ff52f190895975779d14f2e44c1dd0eaf4 Mon Sep 17 00:00:00 2001 From: Finnik <73883383+golkity@users.noreply.github.com> Date: Sat, 23 May 2026 20:26:14 +0300 Subject: [PATCH 18/23] Fix(field): add new pub strcut Login Request, add new field username on Reg Request and add field exppires_in Login Response --- backend/service/bff_service/src/models/requests.rs | 7 +++++++ backend/service/bff_service/src/models/responses.rs | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/backend/service/bff_service/src/models/requests.rs b/backend/service/bff_service/src/models/requests.rs index 3942383..b9d837d 100644 --- a/backend/service/bff_service/src/models/requests.rs +++ b/backend/service/bff_service/src/models/requests.rs @@ -1,7 +1,14 @@ use serde::Deserialize; +#[derive(Deserialize)] +pub struct LoginRequest { + pub email: String, + pub password: String, +} + #[derive(Deserialize)] pub struct RegisterRequest { + pub username: String, pub email: String, pub password: String, } diff --git a/backend/service/bff_service/src/models/responses.rs b/backend/service/bff_service/src/models/responses.rs index bbfb18f..b01769c 100644 --- a/backend/service/bff_service/src/models/responses.rs +++ b/backend/service/bff_service/src/models/responses.rs @@ -4,7 +4,7 @@ use serde::Serialize; pub struct LoginResponse { pub access_token: String, pub refresh_token: String, - pub exprise_in: u32, + pub expires_in: u32, } #[derive(Serialize)] From bdf4bccf187e1642acff0692657a253927b51407 Mon Sep 17 00:00:00 2001 From: Finnik <73883383+golkity@users.noreply.github.com> Date: Sat, 23 May 2026 20:29:03 +0300 Subject: [PATCH 19/23] Feat(main): add entery points in this bff service --- backend/service/bff_service/src/main.rs | 97 +++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 backend/service/bff_service/src/main.rs diff --git a/backend/service/bff_service/src/main.rs b/backend/service/bff_service/src/main.rs new file mode 100644 index 0000000..4dc5545 --- /dev/null +++ b/backend/service/bff_service/src/main.rs @@ -0,0 +1,97 @@ +use axum::Router; +use bb8::Pool; +use bb8_redis::RedisConnectionManager; +use std::net::SocketAddr; +use std::sync::Arc; +use tokio::signal; +use tower_http::trace::TraceLayer; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + +mod api; +mod config; +mod grpc_clients; +mod middleware; +mod models; +mod utils; + +pub use utils::state::AppState; + +#[tokio::main] +async fn main() -> Result<(), Box> { + tracing_subscriber::registry() + .with(tracing_subscriber::EnvFilter::new( + std::env::var("RUST_LOG").unwrap_or_else(|_| "info,bff_service=debug".into()), + )) + .with(tracing_subscriber::fmt::layer()) + .init(); + + tracing::info!("Starting BFF Service (API Gateway)..."); + + let config = config::Config::from_env(); + let manager = RedisConnectionManager::new(config.redis_url.clone())?; + let redis_pool = Pool::builder().build(manager).await?; + + tracing::info!("Connecting to gRPC backends..."); + let auth_grpc_client = + grpc_clients::auth_client::AuthGrpcClient::connect(config.auth_service_grpc_url.clone()) + .await + .expect("Failed to connect to Auth Service"); + + let stream_meta_grpc_client = grpc_clients::stream_meta_client::StreamMetaGrpcClient::connect( + config.stream_meta_grpc_url.clone(), + ) + .await + .expect("Failed to connect to Stream Meta Service"); + + let state = Arc::new(AppState { + config: config.clone(), + redis_pool, + auth_grpc_client, + stream_meta_grpc_client, + }); + + let app = Router::new() + .nest("/v1/auth", api::routes::auth::router()) + .nest("/v1/stream", api::routes::stream::router()) + .layer(axum::middleware::from_fn_with_state( + state.clone(), + middleware::rate_limiter::rate_limit_middleware, + )) + .layer(TraceLayer::new_for_http()) + .with_state(state); + + let addr = SocketAddr::from(([0, 0, 0, 0], config.port)); + tracing::info!("Gateway listening on http://{}", addr); + + let listener = tokio::net::TcpListener::bind(&addr).await?; + axum::serve(listener, app) + .with_graceful_shutdown(shutdown_signal()) + .await?; + + Ok(()) +} + +async fn shutdown_signal() { + let ctrl_c = async { + signal::ctrl_c() + .await + .expect("Failed to install Ctrl+C handler"); + }; + + #[cfg(unix)] + let terminate = async { + signal::unix::signal(signal::unix::SignalKind::terminate()) + .expect("Failed to install signal handler") + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => {}, + _ = terminate => {}, + } + tracing::info!("Received shutdown signal, starting graceful shutdown..."); +} From 95157a5e8206d85fd324fc14ce5e228d0e4c6ec5 Mon Sep 17 00:00:00 2001 From: Finnik <73883383+golkity@users.noreply.github.com> Date: Sat, 23 May 2026 20:29:47 +0300 Subject: [PATCH 20/23] Feat(build): add builder which codegenerate protobuf conf -> rust code --- backend/service/bff_service/build.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 backend/service/bff_service/build.rs diff --git a/backend/service/bff_service/build.rs b/backend/service/bff_service/build.rs new file mode 100644 index 0000000..31d43c9 --- /dev/null +++ b/backend/service/bff_service/build.rs @@ -0,0 +1,20 @@ +fn main() -> Result<(), Box> { + println!("cargo:rerun-if-changed=../../proto/auth.proto"); + println!("cargo:rerun-if-changed=../../proto/stream_meta.proto"); + println!("cargo:rerun-if-changed=../../proto/errors.proto"); + + tonic_build::configure() + .build_server(false) + .build_client(true) + .out_dir(std::env::var("OUT_DIR").unwrap()) + .compile( + &[ + "../../proto/auth.proto", + "../../proto/stream_meta.proto", + "../../proto/errors.proto", + ], + &["../../proto"], + )?; + + Ok(()) +} From 8d84c85a5080f860ef4509eaf8023b8cb4b595fe Mon Sep 17 00:00:00 2001 From: Finnik <73883383+golkity@users.noreply.github.com> Date: Sat, 23 May 2026 20:30:18 +0300 Subject: [PATCH 21/23] Update(lib): add new lib --- backend/service/bff_service/Cargo.toml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/backend/service/bff_service/Cargo.toml b/backend/service/bff_service/Cargo.toml index a35e4e1..264ee4e 100644 --- a/backend/service/bff_service/Cargo.toml +++ b/backend/service/bff_service/Cargo.toml @@ -5,3 +5,23 @@ edition = "2021" [dependencies] rust-core = { path = "../../libs/rust-core" } +axum = "0.7" +tokio = { version = "1.37", features = ["full"] } +tower = "0.4" +tower-http = { version = "0.5", features = ["trace", "cors"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +thiserror = "1.0" +jsonwebtoken = "9.3" +tonic = "0.11" +prost = "0.12" +redis = { version = "0.25", features = ["tokio-comp"] } +bb8 = "0.8" +bb8-redis = "0.15" +dotenvy = "0.15" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +uuid = { version = "1.8", features = ["v4", "serde"] } +anyhow = "1.0.102" +[build-dependencies] +tonic-build = "0.11" From 62ce0b6715168266db11fe3b7cfc1d4548ba0de9 Mon Sep 17 00:00:00 2001 From: Finnik <73883383+golkity@users.noreply.github.com> Date: Sat, 23 May 2026 20:42:55 +0300 Subject: [PATCH 22/23] Feat(proto gRPC): add initial protobuf schemas --- backend/proto/auth.proto | 30 ++++++++++++++++++++++++++++++ backend/proto/stream_meta.proto | 24 ++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 backend/proto/auth.proto create mode 100644 backend/proto/stream_meta.proto diff --git a/backend/proto/auth.proto b/backend/proto/auth.proto new file mode 100644 index 0000000..8337e99 --- /dev/null +++ b/backend/proto/auth.proto @@ -0,0 +1,30 @@ +syntax = "proto3"; + +package stremo.auth.v1; + +service AuthService { + rpc Login(LoginRequest) returns (LoginResponse); + rpc Register(RegisterRequest) returns (RegisterResponse); +} + +message LoginRequest { + string email = 1; + string password = 2; +} + +message LoginResponse { + string access_token = 1; + string refresh_token = 2; + uint32 expires_in = 3; +} + +message RegisterRequest { + string username = 1; + string email = 2; + string password = 3; +} + +message RegisterResponse { + string id = 1; + string message = 2; +} diff --git a/backend/proto/stream_meta.proto b/backend/proto/stream_meta.proto new file mode 100644 index 0000000..6eb9454 --- /dev/null +++ b/backend/proto/stream_meta.proto @@ -0,0 +1,24 @@ +syntax = "proto3"; + +package stremo.stream_meta.v1; + +service StreamMetaService { + rpc GetLiveStreams(GetLiveStreamsRequest) returns (GetLiveStreamsResponse); +} + +message GetLiveStreamsRequest { + int32 limit = 1; + string cursor = 2; +} + +message StreamDto { + string stream_id = 1; + string title = 2; + string category = 3; + int32 viewers_count = 4; +} + +message GetLiveStreamsResponse { + repeated StreamDto streams = 1; + string next_cursor = 2; +} From 85e886845d020c2f53c89ffa02f451414f44340b Mon Sep 17 00:00:00 2001 From: Finnik <73883383+golkity@users.noreply.github.com> Date: Sat, 23 May 2026 20:50:07 +0300 Subject: [PATCH 23/23] ci(bff): setup ci pipeline with lint, test, and security scans --- .github/workflows/bff-ci.yml | 134 +++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 .github/workflows/bff-ci.yml diff --git a/.github/workflows/bff-ci.yml b/.github/workflows/bff-ci.yml new file mode 100644 index 0000000..9be8284 --- /dev/null +++ b/.github/workflows/bff-ci.yml @@ -0,0 +1,134 @@ +name: BFF Service CI (Rust) + +on: + push: + branches: ["dev", "master"] + paths: + - "backend/service/bff_service/**" + - "backend/libs/rust-core/**" + - "backend/proto/**" + - ".github/workflows/bff-ci.yml" + pull_request: + branches: ["dev", "master"] + paths: + - "backend/service/bff_service/**" + - "backend/libs/rust-core/**" + - "backend/proto/**" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + WORKING_DIR: backend/service/bff_service + +jobs: + lint-and-format: + name: Code Quality (Fmt & Clippy) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Protoc (Required for tonic-build) + run: sudo apt-get update && sudo apt-get install -y protobuf-compiler + + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + with: + workspaces: backend + + - name: Check Formatting + working-directory: ${{ env.WORKING_DIR }} + run: cargo fmt --all -- --check + + - name: Run Clippy (Strict Mode) + working-directory: ${{ env.WORKING_DIR }} + run: cargo clippy --workspace --all-targets --all-features -- -D warnings + + security-audit: + name: Security Audit (Cargo Audit) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Install cargo-audit + uses: taiki-e/install-action@cargo-audit + + - name: Run Vulnerability Scan + working-directory: ${{ env.WORKING_DIR }} + run: cargo audit + + test-and-coverage: + name: Unit & Integration Tests + runs-on: ubuntu-latest + needs: [lint-and-format, security-audit] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Protoc + run: sudo apt-get update && sudo apt-get install -y protobuf-compiler + + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Install cargo-llvm-cov + uses: taiki-e/install-action@cargo-llvm-cov + + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + with: + workspaces: backend + + - name: Run Tests & Generate Coverage + working-directory: ${{ env.WORKING_DIR }} + run: cargo llvm-cov test --all-features --workspace --lcov --output-path lcov.info --fail-under-lines 80 + + - name: Upload Coverage to Codecov (Optional) + uses: codecov/codecov-action@v4 + with: + file: ${{ env.WORKING_DIR }}/lcov.info + fail_ci_if_error: false + token: ${{ secrets.CODECOV_TOKEN }} + + docker-build-scan: + name: Build Docker & Trivy Scan + runs-on: ubuntu-latest + needs: test-and-coverage + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker Image + uses: docker/build-push-action@v5 + with: + context: backend + file: backend/service/bff_service/Dockerfile + push: false + tags: stremo/bff-service:dev-test + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + image-ref: "stremo/bff-service:dev-test" + format: "table" + exit-code: "1" + ignore-unfixed: true + vuln-type: "os,library" + severity: "CRITICAL,HIGH"