Compare commits

...

3 Commits

Author SHA1 Message Date
8858bfa3c6 ci: add backend test workflow
Some checks failed
Backend CI / test (push) Has been cancelled
2026-06-06 20:30:05 +03:00
ec1c458bb7 feat(backend): rate limit auth routes with tower_governor 2026-06-06 17:50:36 +03:00
b5e76b0368 build: load secrets from .env instead of hardcoding 2026-06-06 16:25:29 +03:00
5 changed files with 284 additions and 21 deletions

38
.github/workflows/backend.yml vendored Normal file
View File

@@ -0,0 +1,38 @@
name: Backend CI
on:
push:
branches: [main]
paths: ["backend/**", ".github/workflows/backend.yml"]
pull_request:
branches: [main]
paths: ["backend/**", ".github/workflows/backend.yml"]
jobs:
test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: backend
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: pass
POSTGRES_DB: loft_test
ports: ["5432:5432"]
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 5s --health-timeout 5s --health-retries 5
env:
DATABASE_URL: postgres://postgres:pass@localhost:5432/loft_test
STORAGE_PATH: /tmp/loft-files-test
SQLX_OFFLINE: "true"
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo install sqlx-cli --no-default-features --features postgres,rustls
- run: sqlx migrate run
- run: mkdir -p /tmp/loft-files-test
- run: cargo test

218
backend/Cargo.lock generated
View File

@@ -202,6 +202,7 @@ dependencies = [
"tokio",
"tower-cookies",
"tower-http",
"tower_governor",
"tracing",
"tracing-subscriber",
]
@@ -464,6 +465,20 @@ version = "0.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1"
[[package]]
name = "dashmap"
version = "6.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c"
dependencies = [
"cfg-if",
"crossbeam-utils",
"hashbrown 0.14.5",
"lock_api",
"once_cell",
"parking_lot_core",
]
[[package]]
name = "der"
version = "0.7.10"
@@ -629,7 +644,7 @@ dependencies = [
"regex",
"serde",
"serde_json",
"thiserror",
"thiserror 2.0.18",
"typetag",
"uuid",
]
@@ -674,6 +689,12 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "foldhash"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "form_urlencoded"
version = "1.2.2"
@@ -683,6 +704,16 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "forwarded-header-value"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9"
dependencies = [
"nonempty",
"thiserror 1.0.69",
]
[[package]]
name = "fs_extra"
version = "1.3.0"
@@ -771,6 +802,12 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]]
name = "futures-timer"
version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968"
[[package]]
name = "futures-util"
version = "0.3.32"
@@ -851,6 +888,35 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "governor"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9efcab3c1958580ff1f25a2a41be1668f7603d849bb63af523b208a3cc1223b8"
dependencies = [
"cfg-if",
"dashmap",
"futures-sink",
"futures-timer",
"futures-util",
"getrandom 0.3.4",
"hashbrown 0.16.1",
"nonzero_ext",
"parking_lot",
"portable-atomic",
"quanta",
"rand 0.9.4",
"smallvec",
"spinning_top",
"web-time",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "hashbrown"
version = "0.15.5"
@@ -859,7 +925,7 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash",
"foldhash 0.1.5",
]
[[package]]
@@ -867,6 +933,11 @@ name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash 0.2.0",
]
[[package]]
name = "hashlink"
@@ -1247,7 +1318,7 @@ dependencies = [
"jni-sys",
"log",
"simd_cesu8",
"thiserror",
"thiserror 2.0.18",
"walkdir",
"windows-link",
]
@@ -1495,6 +1566,18 @@ dependencies = [
"version_check",
]
[[package]]
name = "nonempty"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7"
[[package]]
name = "nonzero_ext"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21"
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
@@ -1786,6 +1869,26 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pin-project"
version = "1.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "pin-project-lite"
version = "0.2.17"
@@ -1893,6 +1996,21 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "quanta"
version = "0.12.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7"
dependencies = [
"crossbeam-utils",
"libc",
"once_cell",
"raw-cpuid",
"wasi",
"web-sys",
"winapi",
]
[[package]]
name = "quick-xml"
version = "0.38.4"
@@ -1917,7 +2035,7 @@ dependencies = [
"rustc-hash",
"rustls",
"socket2",
"thiserror",
"thiserror 2.0.18",
"tokio",
"tracing",
"web-time",
@@ -1939,7 +2057,7 @@ dependencies = [
"rustls",
"rustls-pki-types",
"slab",
"thiserror",
"thiserror 2.0.18",
"tinyvec",
"tracing",
"web-time",
@@ -2056,6 +2174,15 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba"
[[package]]
name = "raw-cpuid"
version = "11.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186"
dependencies = [
"bitflags",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
@@ -2169,7 +2296,7 @@ version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94070964579245eb2f76e62a7668fe87bd9969ed6c41256f3bf614e3323dd3cc"
dependencies = [
"thiserror",
"thiserror 2.0.18",
]
[[package]]
@@ -2218,7 +2345,7 @@ dependencies = [
"http",
"mime",
"rand 0.10.1",
"thiserror",
"thiserror 2.0.18",
]
[[package]]
@@ -2604,6 +2731,15 @@ dependencies = [
"lock_api",
]
[[package]]
name = "spinning_top"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300"
dependencies = [
"lock_api",
]
[[package]]
name = "spki"
version = "0.7.3"
@@ -2656,7 +2792,7 @@ dependencies = [
"serde_json",
"sha2",
"smallvec",
"thiserror",
"thiserror 2.0.18",
"tokio",
"tokio-stream",
"tracing",
@@ -2741,7 +2877,7 @@ dependencies = [
"smallvec",
"sqlx-core",
"stringprep",
"thiserror",
"thiserror 2.0.18",
"tracing",
"uuid",
"whoami",
@@ -2780,7 +2916,7 @@ dependencies = [
"smallvec",
"sqlx-core",
"stringprep",
"thiserror",
"thiserror 2.0.18",
"tracing",
"uuid",
"whoami",
@@ -2806,7 +2942,7 @@ dependencies = [
"serde",
"serde_urlencoded",
"sqlx-core",
"thiserror",
"thiserror 2.0.18",
"tracing",
"url",
"uuid",
@@ -2866,13 +3002,33 @@ dependencies = [
"syn",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl 1.0.69",
]
[[package]]
name = "thiserror"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
"thiserror-impl",
"thiserror-impl 2.0.18",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
@@ -3085,6 +3241,22 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tower_governor"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44de9b94d849d3c46e06a883d72d408c2de6403367b39df2b1c9d9e7b6736fe6"
dependencies = [
"axum",
"forwarded-header-value",
"governor",
"http",
"pin-project",
"thiserror 2.0.18",
"tower",
"tracing",
]
[[package]]
name = "tracing"
version = "0.1.44"
@@ -3490,6 +3662,22 @@ dependencies = [
"wasite",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.11"
@@ -3499,6 +3687,12 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.62.2"

View File

@@ -22,6 +22,7 @@ argon2 = "0.5.3"
rand = "0.10.1"
futures-util = "0.3.32"
mime_guess = "2.0.5"
tower_governor = { version = "0.8.0", default-features = false, features = ["axum", "tracing"] }
[dev-dependencies]
axum-test = "20.0.0"

View File

@@ -3,6 +3,8 @@ mod error;
mod model;
mod web;
use std::{net::SocketAddr, time::Duration};
use anyhow::Result;
use axum::{
Router,
@@ -13,6 +15,9 @@ use axum::{
};
use sqlx::PgPool;
use tower_cookies::CookieManagerLayer;
use tower_governor::{
GovernorLayer, governor::GovernorConfigBuilder, key_extractor::SmartIpKeyExtractor,
};
use tower_http::{
cors::CorsLayer,
services::{ServeDir, ServeFile},
@@ -44,6 +49,26 @@ async fn main() -> Result<()> {
dotenvy::dotenv().ok();
let governor_conf_auth = GovernorConfigBuilder::default()
.per_second(4)
.burst_size(2)
.key_extractor(SmartIpKeyExtractor)
.finish()
.unwrap();
let governor_auth_limiter = governor_conf_auth.limiter().clone();
let interval = Duration::from_secs(60);
std::thread::spawn(move || {
loop {
std::thread::sleep(interval);
info!(
"rate limiting auth storage size: {}",
governor_auth_limiter.len()
);
governor_auth_limiter.retain_recent();
}
});
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
let pool = PgPool::connect(&database_url).await.unwrap();
sqlx::migrate!().run(&pool).await?;
@@ -53,7 +78,8 @@ async fn main() -> Result<()> {
.layer(DefaultBodyLimit::disable());
let user_repository = UserRepository::new(pool)?;
let routes_auth = routes_auth(user_repository.clone());
let routes_auth =
routes_auth(user_repository.clone()).layer(GovernorLayer::new(governor_conf_auth));
let app = Router::new()
.nest("/api", routes_file)
@@ -80,7 +106,11 @@ async fn main() -> Result<()> {
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
info!("listening on {}", listener.local_addr().unwrap());
axum::serve(listener, app).await?;
axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.await?;
Ok(())
}

View File

@@ -3,11 +3,11 @@ services:
image: docker.io/library/postgres:16
restart: unless-stopped
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=loft
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
ports:
- "5432:5432"
- "127.0.0.1:5432:5432"
volumes:
- loft_db_data:/var/lib/postgresql/data
healthcheck:
@@ -26,10 +26,10 @@ services:
db:
condition: service_healthy
environment:
- DATABASE_URL=postgres://postgres:pass@db:5432/loft
- STORAGE_PATH=/app/storage
- DATABASE_URL=${DATABASE_URL}
- STORAGE_PATH=${STORAGE_PATH}
volumes:
- loft_storage:/app/storage
- loft_storage:${STORAGE_PATH}
volumes:
loft_db_data: