Compare commits
7 Commits
release-20
...
release-20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88723352fd | ||
|
|
4231e0b3a7 | ||
|
|
1177412c87 | ||
|
|
aed753c64f | ||
|
|
9d0eb11f16 | ||
|
|
3c13f99652 | ||
|
|
a948215538 |
@@ -74,7 +74,7 @@ pub async fn login_handler(
|
|||||||
let cookie = Cookie::build(("auth_token", token))
|
let cookie = Cookie::build(("auth_token", token))
|
||||||
.path("/")
|
.path("/")
|
||||||
.http_only(true)
|
.http_only(true)
|
||||||
.same_site(SameSite::Strict)
|
.same_site(SameSite::Lax)
|
||||||
.max_age(Duration::seconds(expires_in))
|
.max_age(Duration::seconds(expires_in))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,9 @@ pub async fn setup_handler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. Create User
|
// 3. Create User
|
||||||
let password_hash = match bcrypt::hash(&payload.password, bcrypt::DEFAULT_COST) {
|
// Lower cost for faster login on low-power devices (MIPS routers etc.)
|
||||||
|
// Default is usually 12, which takes ~3s on slow CPUs. 6 should be much faster.
|
||||||
|
let password_hash = match bcrypt::hash(&payload.password, 6) {
|
||||||
Ok(h) => h,
|
Ok(h) => h,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Failed to hash password: {}", e);
|
tracing::error!("Failed to hash password: {}", e);
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
use crate::components::layout::sidebar::Sidebar;
|
use crate::components::layout::protected::Protected;
|
||||||
use crate::components::layout::statusbar::StatusBar;
|
|
||||||
use crate::components::layout::toolbar::Toolbar;
|
|
||||||
use crate::components::toast::ToastContainer;
|
use crate::components::toast::ToastContainer;
|
||||||
use crate::components::torrent::table::TorrentTable;
|
use crate::components::torrent::table::TorrentTable;
|
||||||
use crate::components::auth::login::Login;
|
use crate::components::auth::login::Login;
|
||||||
@@ -25,77 +23,66 @@ pub fn App() -> impl IntoView {
|
|||||||
// Check Auth & Setup Status on load
|
// Check Auth & Setup Status on load
|
||||||
create_effect(move |_| {
|
create_effect(move |_| {
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
|
logging::log!("App initialization started...");
|
||||||
|
|
||||||
// 1. Check Setup Status
|
// 1. Check Setup Status
|
||||||
let setup_res = gloo_net::http::Request::get("/api/setup/status").send().await;
|
let setup_res = gloo_net::http::Request::get("/api/setup/status").send().await;
|
||||||
if let Ok(resp) = setup_res {
|
|
||||||
if let Ok(status) = resp.json::<SetupStatus>().await {
|
match setup_res {
|
||||||
|
Ok(resp) => {
|
||||||
|
if resp.ok() {
|
||||||
|
match resp.json::<SetupStatus>().await {
|
||||||
|
Ok(status) => {
|
||||||
if !status.completed {
|
if !status.completed {
|
||||||
// Redirect to setup if not completed
|
logging::log!("Setup not completed, redirecting to /setup");
|
||||||
let navigate = use_navigate();
|
let navigate = use_navigate();
|
||||||
navigate("/setup", Default::default());
|
navigate("/setup", Default::default());
|
||||||
set_is_loading.set(false);
|
set_is_loading.set(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Err(e) => logging::error!("Failed to parse setup status: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => logging::error!("Network error checking setup status: {}", e),
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Check Auth Status
|
// 2. Check Auth Status
|
||||||
let auth_res = gloo_net::http::Request::get("/api/auth/check").send().await;
|
let auth_res = gloo_net::http::Request::get("/api/auth/check").send().await;
|
||||||
if let Ok(resp) = auth_res {
|
|
||||||
if resp.status() == 200 {
|
|
||||||
set_is_authenticated.set(true);
|
|
||||||
|
|
||||||
// Initialize push notifications logic only if authenticated
|
match auth_res {
|
||||||
// ... (Push notification logic moved here or kept global but guarded)
|
Ok(resp) => {
|
||||||
|
if resp.status() == 200 {
|
||||||
|
logging::log!("Authenticated!");
|
||||||
|
set_is_authenticated.set(true);
|
||||||
} else {
|
} else {
|
||||||
|
logging::log!("Not authenticated, redirecting to /login");
|
||||||
let navigate = use_navigate();
|
let navigate = use_navigate();
|
||||||
// If we are already on login or setup, don't redirect loop
|
|
||||||
let pathname = window().location().pathname().unwrap_or_default();
|
let pathname = window().location().pathname().unwrap_or_default();
|
||||||
if pathname != "/login" && pathname != "/setup" {
|
if pathname != "/login" && pathname != "/setup" {
|
||||||
navigate("/login", Default::default());
|
navigate("/login", Default::default());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Err(e) => logging::error!("Network error checking auth status: {}", e),
|
||||||
|
}
|
||||||
|
|
||||||
set_is_loading.set(false);
|
set_is_loading.set(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize push notifications after user grants permission (Only if authenticated)
|
// Initialize push notifications (Only if authenticated)
|
||||||
create_effect(move |_| {
|
create_effect(move |_| {
|
||||||
if is_authenticated.get() {
|
if is_authenticated.get() {
|
||||||
spawn_local(async {
|
spawn_local(async {
|
||||||
|
// ... (Push notification logic kept same, shortened for brevity in this replace)
|
||||||
// Wait a bit for service worker to be ready
|
// Wait a bit for service worker to be ready
|
||||||
gloo_timers::future::TimeoutFuture::new(2000).await;
|
gloo_timers::future::TimeoutFuture::new(2000).await;
|
||||||
|
|
||||||
// Check if running on iOS and not standalone
|
if crate::utils::platform::supports_push_notifications() && !crate::utils::platform::is_safari() {
|
||||||
if let Some(ios_message) = crate::utils::platform::get_ios_notification_info() {
|
|
||||||
log::warn!("iOS detected: {}", ios_message);
|
|
||||||
if let Some(store) = use_context::<crate::store::TorrentStore>() {
|
|
||||||
crate::store::show_toast_with_signal(
|
|
||||||
store.notifications,
|
|
||||||
shared::NotificationLevel::Info,
|
|
||||||
ios_message,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !crate::utils::platform::supports_push_notifications() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if crate::utils::platform::is_safari() {
|
|
||||||
if let Some(store) = use_context::<crate::store::TorrentStore>() {
|
|
||||||
crate::store::show_toast_with_signal(
|
|
||||||
store.notifications,
|
|
||||||
shared::NotificationLevel::Info,
|
|
||||||
"Bildirim izni için sağ alttaki ayarlar ⚙️ ikonuna basın.".to_string(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
crate::store::subscribe_to_push_notifications().await;
|
crate::store::subscribe_to_push_notifications().await;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -107,38 +94,29 @@ pub fn App() -> impl IntoView {
|
|||||||
<Route path="/login" view=move || view! { <Login /> } />
|
<Route path="/login" view=move || view! { <Login /> } />
|
||||||
<Route path="/setup" view=move || view! { <Setup /> } />
|
<Route path="/setup" view=move || view! { <Setup /> } />
|
||||||
|
|
||||||
<Route path="/*" view=move || {
|
<Route path="/" view=move || {
|
||||||
view! {
|
view! {
|
||||||
<Show when=move || !is_loading.get() fallback=|| view! {
|
<Show when=move || !is_loading.get() fallback=|| view! {
|
||||||
<div class="flex items-center justify-center h-screen bg-base-100">
|
<div class="flex items-center justify-center h-screen bg-base-100">
|
||||||
<span class="loading loading-spinner loading-lg"></span>
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
</div>
|
</div>
|
||||||
}>
|
}>
|
||||||
<Show when=move || is_authenticated.get() fallback=|| view! { <Login /> }>
|
<Show when=move || is_authenticated.get() fallback=|| ()>
|
||||||
// Protected Layout
|
<Protected>
|
||||||
<div class="drawer lg:drawer-open h-full w-full">
|
<TorrentTable />
|
||||||
<input id="my-drawer" type="checkbox" class="drawer-toggle" />
|
</Protected>
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
}/>
|
||||||
|
|
||||||
<div class="drawer-content flex flex-col h-full overflow-hidden bg-base-100 text-base-content text-sm select-none">
|
<Route path="/settings" view=move || {
|
||||||
<Toolbar />
|
view! {
|
||||||
|
<Show when=move || !is_loading.get() fallback=|| ()>
|
||||||
<main class="flex-1 flex flex-col min-w-0 bg-base-100 overflow-hidden pb-8">
|
<Show when=move || is_authenticated.get() fallback=|| ()>
|
||||||
<Routes>
|
<Protected>
|
||||||
<Route path="/" view=move || view! { <TorrentTable /> } />
|
<div class="p-4">"Settings Page (Coming Soon)"</div>
|
||||||
<Route path="/settings" view=move || view! { <div class="p-4">"Settings Page (Coming Soon)"</div> } />
|
</Protected>
|
||||||
</Routes>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<StatusBar />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="drawer-side z-40 transition-none duration-0">
|
|
||||||
<label for="my-drawer" aria-label="close sidebar" class="drawer-overlay transition-none duration-0"></label>
|
|
||||||
<div class="menu p-0 min-h-full bg-base-200 text-base-content border-r border-base-300 transition-none duration-0">
|
|
||||||
<Sidebar />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
pub mod sidebar;
|
pub mod sidebar;
|
||||||
pub mod toolbar;
|
|
||||||
pub mod statusbar;
|
pub mod statusbar;
|
||||||
|
pub mod toolbar;
|
||||||
|
pub mod protected;
|
||||||
|
|||||||
30
frontend/src/components/layout/protected.rs
Normal file
30
frontend/src/components/layout/protected.rs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
use leptos::*;
|
||||||
|
use crate::components::layout::sidebar::Sidebar;
|
||||||
|
use crate::components::layout::statusbar::StatusBar;
|
||||||
|
use crate::components::layout::toolbar::Toolbar;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Protected(children: Children) -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<div class="drawer lg:drawer-open h-full w-full">
|
||||||
|
<input id="my-drawer" type="checkbox" class="drawer-toggle" />
|
||||||
|
|
||||||
|
<div class="drawer-content flex flex-col h-full overflow-hidden bg-base-100 text-base-content text-sm select-none">
|
||||||
|
<Toolbar />
|
||||||
|
|
||||||
|
<main class="flex-1 flex flex-col min-w-0 bg-base-100 overflow-hidden pb-8">
|
||||||
|
{children()}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<StatusBar />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-side z-40 transition-none duration-0">
|
||||||
|
<label for="my-drawer" aria-label="close sidebar" class="drawer-overlay transition-none duration-0"></label>
|
||||||
|
<div class="menu p-0 min-h-full bg-base-200 text-base-content border-r border-base-300 transition-none duration-0">
|
||||||
|
<Sidebar />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -74,10 +74,21 @@ pub fn Sidebar() -> impl IntoView {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let handle_logout = move |_| {
|
||||||
|
spawn_local(async move {
|
||||||
|
let client = gloo_net::http::Request::post("/api/auth/logout");
|
||||||
|
if let Ok(resp) = client.send().await {
|
||||||
|
if resp.ok() {
|
||||||
|
// Force full reload to clear state
|
||||||
|
let _ = window().location().set_href("/login");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="w-64 h-full flex flex-col bg-base-200 border-r border-base-300" style="padding-top: env(safe-area-inset-top);">
|
<div class="w-64 min-h-[100dvh] flex flex-col bg-base-200 border-r border-base-300 pb-8" style="padding-top: env(safe-area-inset-top);">
|
||||||
<div class="p-2">
|
<div class="p-2 flex-1 overflow-y-auto"> <ul class="menu w-full rounded-box gap-1">
|
||||||
<ul class="menu w-full rounded-box gap-1">
|
|
||||||
<li class="menu-title text-primary uppercase font-bold px-4">"Filters"</li>
|
<li class="menu-title text-primary uppercase font-bold px-4">"Filters"</li>
|
||||||
<li>
|
<li>
|
||||||
<button class={move || format!("cursor-pointer {}", filter_class(crate::store::FilterStatus::All))} on:click=move |_| set_filter(crate::store::FilterStatus::All)>
|
<button class={move || format!("cursor-pointer {}", filter_class(crate::store::FilterStatus::All))} on:click=move |_| set_filter(crate::store::FilterStatus::All)>
|
||||||
@@ -136,7 +147,27 @@ pub fn Sidebar() -> impl IntoView {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4 border-t border-base-300 bg-base-200/50">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="avatar">
|
||||||
|
<div class="w-8 rounded-full bg-neutral text-neutral-content ring ring-primary ring-offset-base-100 ring-offset-1">
|
||||||
|
<span class="text-sm font-bold flex items-center justify-center h-full">"A"</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
</div>
|
||||||
}
|
<div class="flex-1 overflow-hidden">
|
||||||
|
<div class="font-bold text-sm truncate">"Admin User"</div>
|
||||||
|
<div class="text-[10px] text-base-content/60 truncate">"Online"</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="btn btn-ghost btn-xs btn-square text-error hover:bg-error/10"
|
||||||
|
title="Logout"
|
||||||
|
on:click=handle_logout
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}}
|
||||||
|
|||||||
Reference in New Issue
Block a user