Implement authentication system with SQLite: Add login/setup pages, auth middleware, and database integration
Some checks failed
Build MIPS Binary / build (push) Failing after 3m42s
Some checks failed
Build MIPS Binary / build (push) Failing after 3m42s
This commit is contained in:
@@ -3,92 +3,149 @@ use crate::components::layout::statusbar::StatusBar;
|
||||
use crate::components::layout::toolbar::Toolbar;
|
||||
use crate::components::toast::ToastContainer;
|
||||
use crate::components::torrent::table::TorrentTable;
|
||||
use crate::components::auth::login::Login;
|
||||
use crate::components::auth::setup::Setup;
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SetupStatus {
|
||||
completed: bool,
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
crate::store::provide_torrent_store();
|
||||
|
||||
// Initialize push notifications after user grants permission
|
||||
// Auth State
|
||||
let (is_loading, set_is_loading) = create_signal(true);
|
||||
let (is_authenticated, set_is_authenticated) = create_signal(false);
|
||||
|
||||
// Check Auth & Setup Status on load
|
||||
create_effect(move |_| {
|
||||
spawn_local(async {
|
||||
// Wait a bit for service worker to be ready
|
||||
gloo_timers::future::TimeoutFuture::new(2000).await;
|
||||
|
||||
// Check if running on iOS and not standalone
|
||||
if let Some(ios_message) = crate::utils::platform::get_ios_notification_info() {
|
||||
log::warn!("iOS detected: {}", ios_message);
|
||||
|
||||
// Show toast to inform user
|
||||
if let Some(store) = use_context::<crate::store::TorrentStore>() {
|
||||
crate::store::show_toast_with_signal(
|
||||
store.notifications,
|
||||
shared::NotificationLevel::Info,
|
||||
ios_message,
|
||||
);
|
||||
spawn_local(async move {
|
||||
// 1. Check Setup Status
|
||||
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 {
|
||||
if !status.completed {
|
||||
// Redirect to setup if not completed
|
||||
let navigate = use_navigate();
|
||||
navigate("/setup", Default::default());
|
||||
set_is_loading.set(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if push notifications are supported
|
||||
if !crate::utils::platform::supports_push_notifications() {
|
||||
log::warn!("Push notifications not supported on this platform");
|
||||
return;
|
||||
}
|
||||
|
||||
// Safari requires user gesture for notification permission
|
||||
// Don't auto-request on Safari - user should click a button
|
||||
if crate::utils::platform::is_safari() {
|
||||
log::info!("Safari detected - notification permission requires user interaction");
|
||||
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(),
|
||||
);
|
||||
|
||||
// 2. Check Auth Status
|
||||
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
|
||||
// ... (Push notification logic moved here or kept global but guarded)
|
||||
} else {
|
||||
let navigate = use_navigate();
|
||||
// If we are already on login or setup, don't redirect loop
|
||||
let pathname = window().location().pathname().unwrap_or_default();
|
||||
if pathname != "/login" && pathname != "/setup" {
|
||||
navigate("/login", Default::default());
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// For non-Safari browsers (Chrome, Firefox, Edge), attempt auto-subscribe
|
||||
log::info!("Attempting to subscribe to push notifications...");
|
||||
crate::store::subscribe_to_push_notifications().await;
|
||||
set_is_loading.set(false);
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize push notifications after user grants permission (Only if authenticated)
|
||||
create_effect(move |_| {
|
||||
if is_authenticated.get() {
|
||||
spawn_local(async {
|
||||
// Wait a bit for service worker to be ready
|
||||
gloo_timers::future::TimeoutFuture::new(2000).await;
|
||||
|
||||
// Check if running on iOS and not standalone
|
||||
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;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
view! {
|
||||
// Main app wrapper - ensures proper stacking context
|
||||
<div class="relative w-full h-screen" style="height: 100dvh;">
|
||||
// Drawer layout
|
||||
<div class="drawer lg:drawer-open h-full w-full">
|
||||
<input id="my-drawer" type="checkbox" class="drawer-toggle" />
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/login" view=move || view! { <Login /> } />
|
||||
<Route path="/setup" view=move || view! { <Setup /> } />
|
||||
|
||||
<div class="drawer-content flex flex-col h-full overflow-hidden bg-base-100 text-base-content text-sm select-none">
|
||||
<Toolbar />
|
||||
<Route path="/*" view=move || {
|
||||
view! {
|
||||
<Show when=move || !is_loading.get() fallback=|| view! {
|
||||
<div class="flex items-center justify-center h-screen bg-base-100">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
}>
|
||||
<Show when=move || is_authenticated.get() fallback=|| view! { <Login /> }>
|
||||
// Protected Layout
|
||||
<div class="drawer lg:drawer-open h-full w-full">
|
||||
<input id="my-drawer" type="checkbox" class="drawer-toggle" />
|
||||
|
||||
<main class="flex-1 flex flex-col min-w-0 bg-base-100 overflow-hidden pb-8">
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" view=move || view! { <TorrentTable /> } />
|
||||
<Route path="/settings" view=move || view! { <div class="p-4">"Settings Page (Coming Soon)"</div> } />
|
||||
</Routes>
|
||||
</Router>
|
||||
</main>
|
||||
<div class="drawer-content flex flex-col h-full overflow-hidden bg-base-100 text-base-content text-sm select-none">
|
||||
<Toolbar />
|
||||
|
||||
// StatusBar is rendered via fixed positioning, just mount it here
|
||||
<StatusBar />
|
||||
</div>
|
||||
<main class="flex-1 flex flex-col min-w-0 bg-base-100 overflow-hidden pb-8">
|
||||
<Routes>
|
||||
<Route path="/" view=move || view! { <TorrentTable /> } />
|
||||
<Route path="/settings" view=move || view! { <div class="p-4">"Settings Page (Coming Soon)"</div> } />
|
||||
</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>
|
||||
}
|
||||
}/>
|
||||
</Routes>
|
||||
</Router>
|
||||
|
||||
<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>
|
||||
|
||||
// Toast container - fixed positioning relative to viewport
|
||||
<ToastContainer />
|
||||
</div>
|
||||
}
|
||||
|
||||
110
frontend/src/components/auth/login.rs
Normal file
110
frontend/src/components/auth/login.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct LoginRequest {
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Login() -> impl IntoView {
|
||||
let (username, set_username) = create_signal(String::new());
|
||||
let (password, set_password) = create_signal(String::new());
|
||||
let (error, set_error) = create_signal(Option::<String>::None);
|
||||
let (loading, set_loading) = create_signal(false);
|
||||
|
||||
let handle_login = move |ev: web_sys::SubmitEvent| {
|
||||
ev.prevent_default();
|
||||
set_loading.set(true);
|
||||
set_error.set(None);
|
||||
|
||||
spawn_local(async move {
|
||||
let req = LoginRequest {
|
||||
username: username.get(),
|
||||
password: password.get(),
|
||||
};
|
||||
|
||||
let client = gloo_net::http::Request::post("/api/auth/login")
|
||||
.json(&req)
|
||||
.expect("Failed to create request");
|
||||
|
||||
match client.send().await {
|
||||
Ok(resp) => {
|
||||
if resp.ok() {
|
||||
// Redirect to home on success
|
||||
let navigate = use_navigate();
|
||||
navigate("/", Default::default());
|
||||
} else {
|
||||
set_error.set(Some("Kullanıcı adı veya şifre hatalı".to_string()));
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
set_error.set(Some("Bağlantı hatası".to_string()));
|
||||
}
|
||||
}
|
||||
set_loading.set(false);
|
||||
});
|
||||
};
|
||||
|
||||
view! {
|
||||
<div class="flex items-center justify-center min-h-screen bg-base-200">
|
||||
<div class="card w-full max-w-sm shadow-xl bg-base-100">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title justify-center mb-4">"VibeTorrent Giriş"</h2>
|
||||
|
||||
<form on:submit=handle_login>
|
||||
<div class="form-control w-full">
|
||||
<label class="label">
|
||||
<span class="label-text">"Kullanıcı Adı"</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Kullanıcı adınız"
|
||||
class="input input-bordered w-full"
|
||||
prop:value=username
|
||||
on:input=move |ev| set_username.set(event_target_value(&ev))
|
||||
disabled=move || loading.get()
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control w-full mt-4">
|
||||
<label class="label">
|
||||
<span class="label-text">"Şifre"</span>
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="******"
|
||||
class="input input-bordered w-full"
|
||||
prop:value=password
|
||||
on:input=move |ev| set_password.set(event_target_value(&ev))
|
||||
disabled=move || loading.get()
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Show when=move || error.get().is_some()>
|
||||
<div class="alert alert-error mt-4 text-sm py-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
<span>{move || error.get()}</span>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="card-actions justify-end mt-6">
|
||||
<button
|
||||
class="btn btn-primary w-full"
|
||||
type="submit"
|
||||
disabled=move || loading.get()
|
||||
>
|
||||
<Show when=move || loading.get() fallback=|| "Giriş Yap">
|
||||
<span class="loading loading-spinner"></span>
|
||||
"Giriş Yapılıyor..."
|
||||
</Show>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
2
frontend/src/components/auth/mod.rs
Normal file
2
frontend/src/components/auth/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod login;
|
||||
pub mod setup;
|
||||
145
frontend/src/components/auth/setup.rs
Normal file
145
frontend/src/components/auth/setup.rs
Normal file
@@ -0,0 +1,145 @@
|
||||
use leptos::*;
|
||||
use leptos_router::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SetupRequest {
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn Setup() -> impl IntoView {
|
||||
let (username, set_username) = create_signal(String::new());
|
||||
let (password, set_password) = create_signal(String::new());
|
||||
let (confirm_password, set_confirm_password) = create_signal(String::new());
|
||||
let (error, set_error) = create_signal(Option::<String>::None);
|
||||
let (loading, set_loading) = create_signal(false);
|
||||
|
||||
let handle_setup = move |ev: web_sys::SubmitEvent| {
|
||||
ev.prevent_default();
|
||||
set_loading.set(true);
|
||||
set_error.set(None);
|
||||
|
||||
let pass = password.get();
|
||||
let confirm = confirm_password.get();
|
||||
|
||||
if pass != confirm {
|
||||
set_error.set(Some("Şifreler eşleşmiyor".to_string()));
|
||||
set_loading.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if pass.len() < 6 {
|
||||
set_error.set(Some("Şifre en az 6 karakter olmalıdır".to_string()));
|
||||
set_loading.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
spawn_local(async move {
|
||||
let req = SetupRequest {
|
||||
username: username.get(),
|
||||
password: pass,
|
||||
};
|
||||
|
||||
let client = gloo_net::http::Request::post("/api/setup")
|
||||
.json(&req)
|
||||
.expect("Failed to create request");
|
||||
|
||||
match client.send().await {
|
||||
Ok(resp) => {
|
||||
if resp.ok() {
|
||||
// Redirect to login after setup
|
||||
let navigate = use_navigate();
|
||||
navigate("/login", Default::default());
|
||||
} else {
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
set_error.set(Some(format!("Hata: {}", text)));
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
set_error.set(Some("Bağlantı hatası".to_string()));
|
||||
}
|
||||
}
|
||||
set_loading.set(false);
|
||||
});
|
||||
};
|
||||
|
||||
view! {
|
||||
<div class="flex items-center justify-center min-h-screen bg-base-200">
|
||||
<div class="card w-full max-w-md shadow-xl bg-base-100">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title justify-center mb-2">"VibeTorrent Kurulumu"</h2>
|
||||
<p class="text-center text-sm opacity-70 mb-4">"Yönetici hesabınızı oluşturun"</p>
|
||||
|
||||
<form on:submit=handle_setup>
|
||||
<div class="form-control w-full">
|
||||
<label class="label">
|
||||
<span class="label-text">"Kullanıcı Adı"</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="admin"
|
||||
class="input input-bordered w-full"
|
||||
prop:value=username
|
||||
on:input=move |ev| set_username.set(event_target_value(&ev))
|
||||
disabled=move || loading.get()
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control w-full mt-4">
|
||||
<label class="label">
|
||||
<span class="label-text">"Şifre"</span>
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="******"
|
||||
class="input input-bordered w-full"
|
||||
prop:value=password
|
||||
on:input=move |ev| set_password.set(event_target_value(&ev))
|
||||
disabled=move || loading.get()
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control w-full mt-4">
|
||||
<label class="label">
|
||||
<span class="label-text">"Şifre Tekrar"</span>
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="******"
|
||||
class="input input-bordered w-full"
|
||||
prop:value=confirm_password
|
||||
on:input=move |ev| set_confirm_password.set(event_target_value(&ev))
|
||||
disabled=move || loading.get()
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Show when=move || error.get().is_some()>
|
||||
<div class="alert alert-error mt-4 text-sm py-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
<span>{move || error.get()}</span>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="card-actions justify-end mt-6">
|
||||
<button
|
||||
class="btn btn-primary w-full"
|
||||
type="submit"
|
||||
disabled=move || loading.get()
|
||||
>
|
||||
<Show when=move || loading.get() fallback=|| "Kurulumu Tamamla">
|
||||
<span class="loading loading-spinner"></span>
|
||||
"İşleniyor..."
|
||||
</Show>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -3,3 +3,4 @@ pub mod layout;
|
||||
pub mod modal;
|
||||
pub mod toast;
|
||||
pub mod torrent;
|
||||
pub mod auth;
|
||||
|
||||
Reference in New Issue
Block a user