Compare commits
22 Commits
release-20
...
release-20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48221b039e | ||
|
|
60354b66d1 | ||
|
|
0433406288 | ||
|
|
5d8cdd7760 | ||
|
|
145436eefc | ||
|
|
10c95c5ff3 | ||
|
|
329654cc4e | ||
|
|
22b592a652 | ||
|
|
817dc49db2 | ||
|
|
b2a60d3d1e | ||
|
|
520903fa3f | ||
|
|
c45f2f50e9 | ||
|
|
791eabe9bd | ||
|
|
12f93dd640 | ||
|
|
7306db8c2f | ||
|
|
ce0ecd62af | ||
|
|
f2379b67d8 | ||
|
|
755f35c94c | ||
|
|
175cac953e | ||
|
|
2c812fc4f6 | ||
|
|
08df851970 | ||
|
|
35faa6bfda |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -6,3 +6,5 @@ result.xml
|
|||||||
frontend/dist
|
frontend/dist
|
||||||
backend.log
|
backend.log
|
||||||
.runner
|
.runner
|
||||||
|
.env
|
||||||
|
backend/.env
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
# Database
|
|
||||||
DATABASE_URL=sqlite:vibetorrent.db
|
|
||||||
|
|
||||||
# VAPID Keys for Push Notifications
|
|
||||||
# Generate new keys for production using: cargo run --bin web-push --features web-push -- generate-vapid-keys
|
|
||||||
VAPID_PUBLIC_KEY=BEdPj6XQR7MGzM28Nev9wokF5upHoydNDahouJbQ9ZdBJpEFAN1iNfANSEvY0ItasNY5zcvvqN_tjUt64Rfd0gU
|
|
||||||
VAPID_PRIVATE_KEY=aUcCYJ7kUd9UClCaWwad0IVgbYJ6svwl19MjSX7GH10
|
|
||||||
VAPID_EMAIL=mailto:admin@vibetorrent.app
|
|
||||||
@@ -3,3 +3,12 @@ RTORRENT_SOCKET=/tmp/rtorrent.sock
|
|||||||
|
|
||||||
# Backend Listen Port
|
# Backend Listen Port
|
||||||
PORT=3000
|
PORT=3000
|
||||||
|
|
||||||
|
# Database URL
|
||||||
|
DATABASE_URL=sqlite:vibetorrent.db
|
||||||
|
|
||||||
|
# VAPID Keys for Push Notifications
|
||||||
|
# Generate new keys for production using: npx web-push generate-vapid-keys
|
||||||
|
VAPID_PUBLIC_KEY=YOUR_PUBLIC_VAPID_KEY
|
||||||
|
VAPID_PRIVATE_KEY=YOUR_PRIVATE_VAPID_KEY
|
||||||
|
VAPID_EMAIL=mailto:your-email@example.com
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
use sqlx::{sqlite::SqlitePoolOptions, Pool, Sqlite, Row};
|
use sqlx::{sqlite::SqlitePoolOptions, Pool, Sqlite, Row, sqlite::SqliteConnectOptions};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Db {
|
pub struct Db {
|
||||||
@@ -9,10 +10,16 @@ pub struct Db {
|
|||||||
|
|
||||||
impl Db {
|
impl Db {
|
||||||
pub async fn new(db_url: &str) -> Result<Self> {
|
pub async fn new(db_url: &str) -> Result<Self> {
|
||||||
|
let options = SqliteConnectOptions::from_str(db_url)?
|
||||||
|
.create_if_missing(true)
|
||||||
|
.busy_timeout(Duration::from_secs(10)) // Bekleme süresini 10 saniyeye çıkardık
|
||||||
|
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
|
||||||
|
.synchronous(sqlx::sqlite::SqliteSynchronous::Normal);
|
||||||
|
|
||||||
let pool = SqlitePoolOptions::new()
|
let pool = SqlitePoolOptions::new()
|
||||||
.max_connections(5)
|
.max_connections(5)
|
||||||
.acquire_timeout(Duration::from_secs(3))
|
.acquire_timeout(Duration::from_secs(10))
|
||||||
.connect(db_url)
|
.connect_with(options)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let db = Self { pool };
|
let db = Self { pool };
|
||||||
@@ -21,21 +28,6 @@ impl Db {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn run_migrations(&self) -> Result<()> {
|
async fn run_migrations(&self) -> Result<()> {
|
||||||
// WAL mode - enables concurrent reads while writing
|
|
||||||
sqlx::query("PRAGMA journal_mode=WAL")
|
|
||||||
.execute(&self.pool)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// NORMAL synchronous - faster than FULL, still safe enough
|
|
||||||
sqlx::query("PRAGMA synchronous=NORMAL")
|
|
||||||
.execute(&self.pool)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// 5 second busy timeout - reduces "database locked" errors
|
|
||||||
sqlx::query("PRAGMA busy_timeout=5000")
|
|
||||||
.execute(&self.pool)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
sqlx::migrate!("./migrations").run(&self.pool).await?;
|
sqlx::migrate!("./migrations").run(&self.pool).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
use shared::{AppEvent, NotificationLevel, SystemNotification, Torrent, TorrentUpdate};
|
use shared::{AppEvent, NotificationLevel, SystemNotification, Torrent, TorrentUpdate};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -8,24 +9,32 @@ pub enum DiffResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn diff_torrents(old: &[Torrent], new: &[Torrent]) -> DiffResult {
|
pub fn diff_torrents(old: &[Torrent], new: &[Torrent]) -> DiffResult {
|
||||||
// 1. Structural Check (Length or Order changed)
|
// 1. Structural Check: Eğer torrent sayısı değişmişse (yeni eklenen veya silinen),
|
||||||
|
// şimdilik basitlik adına FullUpdate gönderiyoruz.
|
||||||
if old.len() != new.len() {
|
if old.len() != new.len() {
|
||||||
return DiffResult::FullUpdate;
|
return DiffResult::FullUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (i, t) in new.iter().enumerate() {
|
// 2. Hash Set Karşılaştırması:
|
||||||
if old[i].hash != t.hash {
|
// Sıralama değişmiş olabilir ama torrentler aynı mı?
|
||||||
|
let old_map: HashMap<&str, &Torrent> = old.iter().map(|t| (t.hash.as_str(), t)).collect();
|
||||||
|
|
||||||
|
// Eğer yeni listedeki bir hash eski listede yoksa, yapı değişmiş demektir.
|
||||||
|
for new_t in new {
|
||||||
|
if !old_map.contains_key(new_t.hash.as_str()) {
|
||||||
return DiffResult::FullUpdate;
|
return DiffResult::FullUpdate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Field Updates
|
// 3. Alan Güncellemeleri (Partial Updates)
|
||||||
|
// Buraya geldiğimizde biliyoruz ki old ve new listelerindeki torrentler (hash olarak) aynı,
|
||||||
|
// sadece sıraları farklı olabilir veya içindeki veriler güncellenmiş olabilir.
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
|
|
||||||
for (i, new_t) in new.iter().enumerate() {
|
for new_t in new {
|
||||||
let old_t = &old[i];
|
// old_map'ten ilgili torrente hash ile ulaşalım (sıradan bağımsız)
|
||||||
|
let old_t = old_map.get(new_t.hash.as_str()).unwrap();
|
||||||
|
|
||||||
// Initialize with all None
|
|
||||||
let mut update = TorrentUpdate {
|
let mut update = TorrentUpdate {
|
||||||
hash: new_t.hash.clone(),
|
hash: new_t.hash.clone(),
|
||||||
name: None,
|
name: None,
|
||||||
@@ -42,7 +51,7 @@ pub fn diff_torrents(old: &[Torrent], new: &[Torrent]) -> DiffResult {
|
|||||||
|
|
||||||
let mut has_changes = false;
|
let mut has_changes = false;
|
||||||
|
|
||||||
// Compare fields
|
// Alanları karşılaştır
|
||||||
if old_t.name != new_t.name {
|
if old_t.name != new_t.name {
|
||||||
update.name = Some(new_t.name.clone());
|
update.name = Some(new_t.name.clone());
|
||||||
has_changes = true;
|
has_changes = true;
|
||||||
@@ -63,7 +72,7 @@ pub fn diff_torrents(old: &[Torrent], new: &[Torrent]) -> DiffResult {
|
|||||||
update.percent_complete = Some(new_t.percent_complete);
|
update.percent_complete = Some(new_t.percent_complete);
|
||||||
has_changes = true;
|
has_changes = true;
|
||||||
|
|
||||||
// Check for torrent completion: reached 100%
|
// Torrent tamamlanma kontrolü
|
||||||
if old_t.percent_complete < 100.0 && new_t.percent_complete >= 100.0 {
|
if old_t.percent_complete < 100.0 && new_t.percent_complete >= 100.0 {
|
||||||
tracing::info!("Torrent completed: {} ({})", new_t.name, new_t.hash);
|
tracing::info!("Torrent completed: {} ({})", new_t.name, new_t.hash);
|
||||||
events.push(AppEvent::Notification(SystemNotification {
|
events.push(AppEvent::Notification(SystemNotification {
|
||||||
@@ -83,8 +92,7 @@ pub fn diff_torrents(old: &[Torrent], new: &[Torrent]) -> DiffResult {
|
|||||||
if old_t.status != new_t.status {
|
if old_t.status != new_t.status {
|
||||||
update.status = Some(new_t.status.clone());
|
update.status = Some(new_t.status.clone());
|
||||||
has_changes = true;
|
has_changes = true;
|
||||||
|
|
||||||
// Log status changes for debugging
|
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"Torrent status changed: {} ({}) {:?} -> {:?}",
|
"Torrent status changed: {} ({}) {:?} -> {:?}",
|
||||||
new_t.name, new_t.hash, old_t.status, new_t.status
|
new_t.name, new_t.hash, old_t.status, new_t.status
|
||||||
@@ -110,4 +118,4 @@ pub fn diff_torrents(old: &[Torrent], new: &[Torrent]) -> DiffResult {
|
|||||||
tracing::debug!("Generated {} partial updates", events.len());
|
tracing::debug!("Generated {} partial updates", events.len());
|
||||||
DiffResult::Partial(events)
|
DiffResult::Partial(events)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -690,8 +690,10 @@ pub async fn handle_timeout_error(err: BoxError) -> (StatusCode, &'static str) {
|
|||||||
(status = 200, description = "VAPID public key", body = String)
|
(status = 200, description = "VAPID public key", body = String)
|
||||||
)
|
)
|
||||||
)]
|
)]
|
||||||
pub async fn get_push_public_key_handler() -> impl IntoResponse {
|
pub async fn get_push_public_key_handler(
|
||||||
let public_key = push::get_vapid_public_key();
|
State(state): State<AppState>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let public_key = state.push_store.get_public_key();
|
||||||
(StatusCode::OK, Json(serde_json::json!({ "publicKey": public_key }))).into_response()
|
(StatusCode::OK, Json(serde_json::json!({ "publicKey": public_key }))).into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -255,9 +255,7 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update in DB (using a direct query since db.rs doesn't have update_password yet)
|
// Update in DB
|
||||||
// We should add `update_password` to db.rs for cleaner code, but for now direct query is fine or we can extend Db.
|
|
||||||
// Let's extend Db.rs first to be clean.
|
|
||||||
if let Err(e) = db.update_password(user_id, &password_hash).await {
|
if let Err(e) = db.update_password(user_id, &password_hash).await {
|
||||||
tracing::error!("Failed to update password in DB: {}", e);
|
tracing::error!("Failed to update password in DB: {}", e);
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
@@ -518,9 +516,9 @@ async fn main() {
|
|||||||
.layer(TraceLayer::new_for_http())
|
.layer(TraceLayer::new_for_http())
|
||||||
.layer(
|
.layer(
|
||||||
CompressionLayer::new()
|
CompressionLayer::new()
|
||||||
.br(false)
|
.br(true)
|
||||||
.gzip(true)
|
.gzip(true)
|
||||||
.quality(CompressionLevel::Fastest),
|
.quality(CompressionLevel::Best),
|
||||||
)
|
)
|
||||||
.layer(
|
.layer(
|
||||||
ServiceBuilder::new()
|
ServiceBuilder::new()
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use utoipa::ToSchema;
|
|||||||
use web_push::{
|
use web_push::{
|
||||||
HyperWebPushClient, SubscriptionInfo, VapidSignatureBuilder, WebPushClient, WebPushMessageBuilder,
|
HyperWebPushClient, SubscriptionInfo, VapidSignatureBuilder, WebPushClient, WebPushMessageBuilder,
|
||||||
};
|
};
|
||||||
|
use futures::StreamExt;
|
||||||
|
|
||||||
use crate::db::Db;
|
use crate::db::Db;
|
||||||
|
|
||||||
@@ -20,17 +21,34 @@ pub struct PushKeys {
|
|||||||
pub auth: String,
|
pub auth: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct VapidConfig {
|
||||||
|
pub private_key: String,
|
||||||
|
pub public_key: String,
|
||||||
|
pub email: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct PushSubscriptionStore {
|
pub struct PushSubscriptionStore {
|
||||||
db: Option<Db>,
|
db: Option<Db>,
|
||||||
subscriptions: Arc<RwLock<Vec<PushSubscription>>>,
|
subscriptions: Arc<RwLock<Vec<PushSubscription>>>,
|
||||||
|
vapid_config: VapidConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PushSubscriptionStore {
|
impl PushSubscriptionStore {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
let private_key = std::env::var("VAPID_PRIVATE_KEY").expect("VAPID_PRIVATE_KEY must be set in .env");
|
||||||
|
let public_key = std::env::var("VAPID_PUBLIC_KEY").expect("VAPID_PUBLIC_KEY must be set in .env");
|
||||||
|
let email = std::env::var("VAPID_EMAIL").expect("VAPID_EMAIL must be set in .env");
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
db: None,
|
db: None,
|
||||||
subscriptions: Arc::new(RwLock::new(Vec::new())),
|
subscriptions: Arc::new(RwLock::new(Vec::new())),
|
||||||
|
vapid_config: VapidConfig {
|
||||||
|
private_key,
|
||||||
|
public_key,
|
||||||
|
email,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,9 +65,18 @@ impl PushSubscriptionStore {
|
|||||||
}
|
}
|
||||||
tracing::info!("Loaded {} push subscriptions from database", subscriptions_vec.len());
|
tracing::info!("Loaded {} push subscriptions from database", subscriptions_vec.len());
|
||||||
|
|
||||||
|
let private_key = std::env::var("VAPID_PRIVATE_KEY").expect("VAPID_PRIVATE_KEY must be set in .env");
|
||||||
|
let public_key = std::env::var("VAPID_PUBLIC_KEY").expect("VAPID_PUBLIC_KEY must be set in .env");
|
||||||
|
let email = std::env::var("VAPID_EMAIL").expect("VAPID_EMAIL must be set in .env");
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
db: Some(db.clone()),
|
db: Some(db.clone()),
|
||||||
subscriptions: Arc::new(RwLock::new(subscriptions_vec)),
|
subscriptions: Arc::new(RwLock::new(subscriptions_vec)),
|
||||||
|
vapid_config: VapidConfig {
|
||||||
|
private_key,
|
||||||
|
public_key,
|
||||||
|
email,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,6 +118,10 @@ impl PushSubscriptionStore {
|
|||||||
pub async fn get_all_subscriptions(&self) -> Vec<PushSubscription> {
|
pub async fn get_all_subscriptions(&self) -> Vec<PushSubscription> {
|
||||||
self.subscriptions.read().await.clone()
|
self.subscriptions.read().await.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_public_key(&self) -> &str {
|
||||||
|
&self.vapid_config.public_key
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send push notification to all subscribed clients
|
/// Send push notification to all subscribed clients
|
||||||
@@ -116,50 +147,68 @@ pub async fn send_push_notification(
|
|||||||
"tag": "vibetorrent"
|
"tag": "vibetorrent"
|
||||||
});
|
});
|
||||||
|
|
||||||
let client = HyperWebPushClient::new();
|
let client = Arc::new(HyperWebPushClient::new());
|
||||||
|
let vapid_config = store.vapid_config.clone();
|
||||||
|
let payload_str = payload.to_string();
|
||||||
|
|
||||||
let vapid_private_key = std::env::var("VAPID_PRIVATE_KEY").expect("VAPID_PRIVATE_KEY must be set in .env");
|
// Send notifications concurrently
|
||||||
let vapid_email = std::env::var("VAPID_EMAIL").expect("VAPID_EMAIL must be set in .env");
|
futures::stream::iter(subscriptions)
|
||||||
|
.for_each_concurrent(10, |subscription| {
|
||||||
|
let client = client.clone();
|
||||||
|
let vapid_config = vapid_config.clone();
|
||||||
|
let payload_str = payload_str.clone();
|
||||||
|
|
||||||
for subscription in subscriptions {
|
async move {
|
||||||
let subscription_info = SubscriptionInfo {
|
let subscription_info = SubscriptionInfo {
|
||||||
endpoint: subscription.endpoint.clone(),
|
endpoint: subscription.endpoint.clone(),
|
||||||
keys: web_push::SubscriptionKeys {
|
keys: web_push::SubscriptionKeys {
|
||||||
p256dh: subscription.keys.p256dh.clone(),
|
p256dh: subscription.keys.p256dh.clone(),
|
||||||
auth: subscription.keys.auth.clone(),
|
auth: subscription.keys.auth.clone(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut sig_builder = VapidSignatureBuilder::from_base64(
|
let sig_res = VapidSignatureBuilder::from_base64(
|
||||||
&vapid_private_key,
|
&vapid_config.private_key,
|
||||||
web_push::URL_SAFE_NO_PAD,
|
web_push::URL_SAFE_NO_PAD,
|
||||||
&subscription_info,
|
&subscription_info,
|
||||||
)?;
|
);
|
||||||
|
|
||||||
sig_builder.add_claim("sub", vapid_email.as_str());
|
match sig_res {
|
||||||
sig_builder.add_claim("aud", subscription.endpoint.as_str());
|
Ok(mut sig_builder) => {
|
||||||
let signature = sig_builder.build()?;
|
sig_builder.add_claim("sub", vapid_config.email.as_str());
|
||||||
|
sig_builder.add_claim("aud", subscription.endpoint.as_str());
|
||||||
|
|
||||||
|
match sig_builder.build() {
|
||||||
|
Ok(signature) => {
|
||||||
|
let mut builder = WebPushMessageBuilder::new(&subscription_info);
|
||||||
|
builder.set_vapid_signature(signature);
|
||||||
|
builder.set_payload(web_push::ContentEncoding::Aes128Gcm, payload_str.as_bytes());
|
||||||
|
|
||||||
let mut builder = WebPushMessageBuilder::new(&subscription_info);
|
match builder.build() {
|
||||||
builder.set_vapid_signature(signature);
|
Ok(msg) => {
|
||||||
|
match client.send(msg).await {
|
||||||
let payload_str = payload.to_string();
|
Ok(_) => {
|
||||||
builder.set_payload(web_push::ContentEncoding::Aes128Gcm, payload_str.as_bytes());
|
tracing::debug!("Push notification sent to: {}", subscription.endpoint);
|
||||||
|
}
|
||||||
match client.send(builder.build()?).await {
|
Err(e) => {
|
||||||
Ok(_) => {
|
tracing::error!("Failed to send push notification to {}: {}", subscription.endpoint, e);
|
||||||
tracing::debug!("Push notification sent to: {}", subscription.endpoint);
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => tracing::error!("Failed to build push message: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => tracing::error!("Failed to build VAPID signature: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => tracing::error!("Failed to create VAPID signature builder: {}", e),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
})
|
||||||
tracing::error!("Failed to send push notification: {}", e);
|
.await;
|
||||||
// TODO: Remove invalid subscriptions
|
|
||||||
}
|
Ok(())
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_vapid_public_key() -> String {
|
|
||||||
std::env::var("VAPID_PUBLIC_KEY").expect("VAPID_PUBLIC_KEY must be set in .env")
|
|
||||||
}
|
|
||||||
@@ -5,13 +5,12 @@ use tower_governor::governor::GovernorConfigBuilder;
|
|||||||
use tower_governor::key_extractor::SmartIpKeyExtractor;
|
use tower_governor::key_extractor::SmartIpKeyExtractor;
|
||||||
|
|
||||||
pub fn get_login_rate_limit_config() -> GovernorConfig<SmartIpKeyExtractor, NoOpMiddleware<QuantaInstant>> {
|
pub fn get_login_rate_limit_config() -> GovernorConfig<SmartIpKeyExtractor, NoOpMiddleware<QuantaInstant>> {
|
||||||
// 20 saniyede bir yeni hak verilir (dakikada 3 istek).
|
// 5 yanlış denemeden sonra bloklanır.
|
||||||
// Başlangıçta 3 isteklik bir patlama (burst) hakkı tanınır.
|
// Her yeni hak için 60 saniye (1 dakika) bekleme süresi.
|
||||||
// Kullanıcı 3 kere hızlıca deneyebilir, 4. deneme için 20 saniye beklemesi gerekir.
|
|
||||||
GovernorConfigBuilder::default()
|
GovernorConfigBuilder::default()
|
||||||
.key_extractor(SmartIpKeyExtractor)
|
.key_extractor(SmartIpKeyExtractor)
|
||||||
.per_second(20)
|
.per_second(60)
|
||||||
.burst_size(3)
|
.burst_size(5)
|
||||||
.finish()
|
.finish()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ RUN apt-get update && apt-get install -y \
|
|||||||
jq \
|
jq \
|
||||||
# Needed for some crate compilations
|
# Needed for some crate compilations
|
||||||
protobuf-compiler \
|
protobuf-compiler \
|
||||||
|
# Install binaryen to have wasm-opt available system-wide
|
||||||
|
binaryen \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# 2. Install Node.js v20 (Manual install to support multi-arch cleanly)
|
# 2. Install Node.js v20 (Manual install to support multi-arch cleanly)
|
||||||
@@ -70,7 +72,7 @@ RUN . "$HOME/.cargo/env" && \
|
|||||||
ARCH=$(dpkg --print-architecture) && \
|
ARCH=$(dpkg --print-architecture) && \
|
||||||
if [ "$ARCH" = "amd64" ]; then TRUNK_ARCH="x86_64-unknown-linux-gnu"; \
|
if [ "$ARCH" = "amd64" ]; then TRUNK_ARCH="x86_64-unknown-linux-gnu"; \
|
||||||
elif [ "$ARCH" = "arm64" ]; then TRUNK_ARCH="aarch64-unknown-linux-gnu"; fi && \
|
elif [ "$ARCH" = "arm64" ]; then TRUNK_ARCH="aarch64-unknown-linux-gnu"; fi && \
|
||||||
wget -qO- "https://github.com/trunk-rs/trunk/releases/download/v0.21.5/trunk-$TRUNK_ARCH.tar.gz" | tar -xzf - -C /root/.cargo/bin/ && \
|
wget -qO- "https://github.com/trunk-rs/trunk/releases/download/v0.21.14/trunk-$TRUNK_ARCH.tar.gz" | tar -xzf - -C /root/.cargo/bin/ && \
|
||||||
chmod +x /root/.cargo/bin/trunk && \
|
chmod +x /root/.cargo/bin/trunk && \
|
||||||
# Install wasm-bindgen-cli (Compiling from source to avoid glibc issues, doing it ONCE here)
|
# Install wasm-bindgen-cli (Compiling from source to avoid glibc issues, doing it ONCE here)
|
||||||
cargo install wasm-bindgen-cli --version 0.2.108
|
cargo install wasm-bindgen-cli --version 0.2.108
|
||||||
|
|||||||
@@ -52,3 +52,15 @@ web-sys = { version = "0.3", features = [
|
|||||||
shared = { path = "../shared" }
|
shared = { path = "../shared" }
|
||||||
tailwind_fuse = "0.3.2"
|
tailwind_fuse = "0.3.2"
|
||||||
js-sys = "0.3.85"
|
js-sys = "0.3.85"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
# En yüksek seviyede boyut optimizasyonu
|
||||||
|
opt-level = "z"
|
||||||
|
# Tüm kütüphaneler arasında link-time optimizasyonu yapar (Çok etkilidir)
|
||||||
|
lto = true
|
||||||
|
# Kod üretim birimini 1'e düşürerek daha iyi optimizasyon sağlar
|
||||||
|
codegen-units = 1
|
||||||
|
# Hata durumunda stack unwinding yerine doğrudan durur (Kod boyutunu düşürür)
|
||||||
|
panic = "abort"
|
||||||
|
# Sembolleri ve debug bilgilerini siler
|
||||||
|
strip = true
|
||||||
|
|||||||
@@ -86,12 +86,15 @@
|
|||||||
id="app-loading"
|
id="app-loading"
|
||||||
style="
|
style="
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
font-family: sans-serif;
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
id="app-loading-spinner"
|
||||||
style="
|
style="
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
@@ -102,6 +105,32 @@
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
"
|
"
|
||||||
></div>
|
></div>
|
||||||
|
<div
|
||||||
|
id="app-loading-error"
|
||||||
|
style="display: none; text-align: center; margin-top: 20px; padding: 0 20px"
|
||||||
|
>
|
||||||
|
<p style="color: #ef4444; font-weight: bold; margin-bottom: 8px">
|
||||||
|
Uygulama yüklenemedi
|
||||||
|
</p>
|
||||||
|
<p style="font-size: 14px; opacity: 0.7">
|
||||||
|
Bağlantınız yavaş olabilir veya bir sistem hatası oluşmuş olabilir.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onclick="location.reload()"
|
||||||
|
style="
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: #3b82f6;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Sayfayı Yenile
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<style>
|
<style>
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
@@ -114,6 +143,34 @@
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// App loading timeout handler
|
||||||
|
(function () {
|
||||||
|
var timeout = setTimeout(function () {
|
||||||
|
if (!document.body.classList.contains("app-loaded")) {
|
||||||
|
var spinner = document.getElementById("app-loading-spinner");
|
||||||
|
var error = document.getElementById("app-loading-error");
|
||||||
|
if (spinner) spinner.style.display = "none";
|
||||||
|
if (error) error.style.display = "block";
|
||||||
|
}
|
||||||
|
}, 15000); // 15 seconds timeout
|
||||||
|
|
||||||
|
// Clean up timeout if app loads
|
||||||
|
var observer = new MutationObserver(function (mutations) {
|
||||||
|
mutations.forEach(function (mutation) {
|
||||||
|
if (
|
||||||
|
mutation.attributeName === "class" &&
|
||||||
|
document.body.classList.contains("app-loaded")
|
||||||
|
) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
observer.observe(document.body, { attributes: true });
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
<!-- Service Worker Registration & PWA Setup -->
|
<!-- Service Worker Registration & PWA Setup -->
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ pub fn Login() -> impl IntoView {
|
|||||||
logging::log!("Login successful, redirecting...");
|
logging::log!("Login successful, redirecting...");
|
||||||
// Force a full reload to re-run auth checks in App.rs
|
// Force a full reload to re-run auth checks in App.rs
|
||||||
let _ = window().location().set_href("/");
|
let _ = window().location().set_href("/");
|
||||||
|
} else if resp.status() == 429 {
|
||||||
|
set_error.set(Some("Çok fazla başarısız deneme yaptınız. Lütfen bir süre bekleyip tekrar deneyin.".to_string()));
|
||||||
} else {
|
} else {
|
||||||
let text = resp.text().await.unwrap_or_default();
|
let text = resp.text().await.unwrap_or_default();
|
||||||
logging::error!("Login failed: {}", text);
|
logging::error!("Login failed: {}", text);
|
||||||
|
|||||||
@@ -143,6 +143,12 @@ pub fn provide_torrent_store() {
|
|||||||
|
|
||||||
// Initialize SSE connection with auto-reconnect
|
// Initialize SSE connection with auto-reconnect
|
||||||
create_effect(move |_| {
|
create_effect(move |_| {
|
||||||
|
// Sadece kullanıcı giriş yapmışsa bağlantıyı başlat
|
||||||
|
if user.get().is_none() {
|
||||||
|
logging::log!("SSE: User not authenticated, skipping connection.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
let mut backoff_ms: u32 = 1000; // Start with 1 second
|
let mut backoff_ms: u32 = 1000; // Start with 1 second
|
||||||
let max_backoff_ms: u32 = 30000; // Max 30 seconds
|
let max_backoff_ms: u32 = 30000; // Max 30 seconds
|
||||||
|
|||||||
Reference in New Issue
Block a user