Compare commits

..

1 Commits

Author SHA1 Message Date
spinline
9b18b97c49 Fetch and display actual username in sidebar profile section
All checks were successful
Build MIPS Binary / build (push) Successful in 4m8s
2026-02-07 17:17:16 +03:00
5 changed files with 270 additions and 138 deletions

View File

@@ -68,6 +68,14 @@ impl Db {
Ok(row.map(|r| (r.get(0), r.get(1)))) Ok(row.map(|r| (r.get(0), r.get(1))))
} }
pub async fn get_username_by_id(&self, id: i64) -> Result<Option<String>> {
let row = sqlx::query("SELECT username FROM users WHERE id = ?")
.bind(id)
.fetch_optional(&self.pool)
.await?;
Ok(row.map(|r| r.get(0)))
}
pub async fn has_users(&self) -> Result<bool> { pub async fn has_users(&self) -> Result<bool> {
let row: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM users") let row: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM users")
.fetch_one(&self.pool) .fetch_one(&self.pool)

View File

@@ -15,7 +15,6 @@ pub struct LoginRequest {
password: String, password: String,
} }
#[allow(dead_code)]
#[derive(Serialize, ToSchema)] #[derive(Serialize, ToSchema)]
pub struct UserResponse { pub struct UserResponse {
username: String, username: String,
@@ -79,7 +78,7 @@ pub async fn login_handler(
.build(); .build();
tracing::info!("Session created and cookie set for user: {}", payload.username); tracing::info!("Session created and cookie set for user: {}", payload.username);
(StatusCode::OK, jar.add(cookie), "Login successful").into_response() (StatusCode::OK, jar.add(cookie), Json(UserResponse { username: payload.username })).into_response()
} }
Ok(false) => { Ok(false) => {
tracing::warn!("Login failed: Invalid password for {}", payload.username); tracing::warn!("Login failed: Invalid password for {}", payload.username);
@@ -120,7 +119,7 @@ pub async fn logout_handler(
get, get,
path = "/api/auth/check", path = "/api/auth/check",
responses( responses(
(status = 200, description = "Authenticated"), (status = 200, description = "Authenticated", body = UserResponse),
(status = 401, description = "Not authenticated") (status = 401, description = "Not authenticated")
) )
)] )]
@@ -130,7 +129,15 @@ pub async fn check_auth_handler(
) -> impl IntoResponse { ) -> impl IntoResponse {
if let Some(token) = jar.get("auth_token") { if let Some(token) = jar.get("auth_token") {
match state.db.get_session_user(token.value()).await { match state.db.get_session_user(token.value()).await {
Ok(Some(_)) => return StatusCode::OK.into_response(), Ok(Some(user_id)) => {
// Fetch username
// We need a helper in db.rs to get username by id, or we can use a direct query here if we don't want to change db.rs interface yet.
// But better to add `get_username_by_id` to db.rs
// For now let's query directly or via a new db method.
if let Ok(Some(username)) = state.db.get_username_by_id(user_id).await {
return (StatusCode::OK, Json(UserResponse { username })).into_response();
}
},
_ => {} // Invalid session _ => {} // Invalid session
} }
} }

View File

@@ -12,6 +12,11 @@ struct SetupStatus {
completed: bool, completed: bool,
} }
#[derive(Deserialize)]
struct UserResponse {
username: String,
}
#[component] #[component]
pub fn App() -> impl IntoView { pub fn App() -> impl IntoView {
crate::store::provide_torrent_store(); crate::store::provide_torrent_store();
@@ -55,6 +60,14 @@ pub fn App() -> impl IntoView {
Ok(resp) => { Ok(resp) => {
if resp.status() == 200 { if resp.status() == 200 {
logging::log!("Authenticated!"); logging::log!("Authenticated!");
// Parse user info
if let Ok(user_info) = resp.json::<UserResponse>().await {
if let Some(store) = use_context::<crate::store::TorrentStore>() {
store.user.set(Some(user_info.username));
}
}
set_is_authenticated.set(true); set_is_authenticated.set(true);
} else { } else {
logging::log!("Not authenticated, redirecting to /login"); logging::log!("Not authenticated, redirecting to /login");
@@ -71,7 +84,6 @@ pub fn App() -> impl IntoView {
set_is_loading.set(false); set_is_loading.set(false);
}); });
}); });
// Initialize push notifications (Only if authenticated) // Initialize push notifications (Only if authenticated)
create_effect(move |_| { create_effect(move |_| {
if is_authenticated.get() { if is_authenticated.get() {

View File

@@ -86,88 +86,190 @@ pub fn Sidebar() -> impl IntoView {
}); });
}; };
let username = move || {
store.user.get().unwrap_or_else(|| "User".to_string())
};
let first_letter = move || {
username().chars().next().unwrap_or('?').to_uppercase().to_string()
};
view! { view! {
<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="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 flex-1 overflow-y-auto"> <ul class="menu w-full rounded-box gap-1">
<div class="p-2 flex-1 overflow-y-auto">
<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)>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" /> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg> </svg>
"All" "All"
<span class="badge badge-sm badge-ghost ml-auto">{total_count}</span> <span class="badge badge-sm badge-ghost ml-auto">{total_count}</span>
</button> </button>
</li> </li>
<li> <li>
<button class={move || format!("cursor-pointer {}", filter_class(crate::store::FilterStatus::Downloading))} on:click=move |_| set_filter(crate::store::FilterStatus::Downloading)> <button class={move || format!("cursor-pointer {}", filter_class(crate::store::FilterStatus::Downloading))} on:click=move |_| set_filter(crate::store::FilterStatus::Downloading)>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" /> <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg> </svg>
"Downloading" "Downloading"
<span class="badge badge-sm badge-ghost ml-auto">{downloading_count}</span> <span class="badge badge-sm badge-ghost ml-auto">{downloading_count}</span>
</button> </button>
</li> </li>
<li> <li>
<button class={move || format!("cursor-pointer {}", filter_class(crate::store::FilterStatus::Seeding))} on:click=move |_| set_filter(crate::store::FilterStatus::Seeding)> <button class={move || format!("cursor-pointer {}", filter_class(crate::store::FilterStatus::Seeding))} on:click=move |_| set_filter(crate::store::FilterStatus::Seeding)>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" /> <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
</svg> </svg>
"Seeding" "Seeding"
<span class="badge badge-sm badge-ghost ml-auto">{seeding_count}</span> <span class="badge badge-sm badge-ghost ml-auto">{seeding_count}</span>
</button> </button>
</li> </li>
<li> <li>
<button class={move || format!("cursor-pointer {}", filter_class(crate::store::FilterStatus::Completed))} on:click=move |_| set_filter(crate::store::FilterStatus::Completed)> <button class={move || format!("cursor-pointer {}", filter_class(crate::store::FilterStatus::Completed))} on:click=move |_| set_filter(crate::store::FilterStatus::Completed)>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
"Completed" "Completed"
<span class="badge badge-sm badge-ghost ml-auto">{completed_count}</span> <span class="badge badge-sm badge-ghost ml-auto">{completed_count}</span>
</button> </button>
</li> </li>
<li> <li>
<button class={move || format!("cursor-pointer {}", filter_class(crate::store::FilterStatus::Paused))} on:click=move |_| set_filter(crate::store::FilterStatus::Paused)> <button class={move || format!("cursor-pointer {}", filter_class(crate::store::FilterStatus::Paused))} on:click=move |_| set_filter(crate::store::FilterStatus::Paused)>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25v13.5m-7.5-13.5v13.5" /> <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25v13.5m-7.5-13.5v13.5" />
</svg> </svg>
"Paused" "Paused"
<span class="badge badge-sm badge-ghost ml-auto">{paused_count}</span> <span class="badge badge-sm badge-ghost ml-auto">{paused_count}</span>
</button> </button>
</li> </li>
<li> <li>
<button class={move || format!("cursor-pointer {}", filter_class(crate::store::FilterStatus::Inactive))} on:click=move |_| set_filter(crate::store::FilterStatus::Inactive)> <button class={move || format!("cursor-pointer {}", filter_class(crate::store::FilterStatus::Inactive))} on:click=move |_| set_filter(crate::store::FilterStatus::Inactive)>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" /> <path stroke-linecap="round" stroke-linejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
</svg> </svg>
"Inactive" "Inactive"
<span class="badge badge-sm badge-ghost ml-auto">{inactive_count}</span> <span class="badge badge-sm badge-ghost ml-auto">{inactive_count}</span>
</button> </button>
</li> </li>
</ul> </ul>
</div> </div>
<div class="p-4 border-t border-base-300 bg-base-200/50"> <div class="p-4 border-t border-base-300 bg-base-200/50">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<div class="avatar"> <div class="avatar">
<div class="w-8 rounded-full bg-neutral text-neutral-content ring ring-primary ring-offset-base-100 ring-offset-1"> <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>
<span class="text-sm font-bold flex items-center justify-center h-full">{first_letter}</span>
</div> </div>
</div> </div>
<div class="flex-1 overflow-hidden"> <div class="flex-1 overflow-hidden">
<div class="font-bold text-sm truncate">"Admin User"</div>
<div class="font-bold text-sm truncate">{username}</div>
<div class="text-[10px] text-base-content/60 truncate">"Online"</div> <div class="text-[10px] text-base-content/60 truncate">"Online"</div>
</div> </div>
<button <button
class="btn btn-ghost btn-xs btn-square text-error hover:bg-error/10" class="btn btn-ghost btn-xs btn-square text-error hover:bg-error/10"
title="Logout" title="Logout"
on:click=handle_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"> <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" /> <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> </svg>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
}} }}

View File

@@ -120,6 +120,7 @@ pub struct TorrentStore {
pub search_query: RwSignal<String>, pub search_query: RwSignal<String>,
pub global_stats: RwSignal<GlobalStats>, pub global_stats: RwSignal<GlobalStats>,
pub notifications: RwSignal<Vec<NotificationItem>>, pub notifications: RwSignal<Vec<NotificationItem>>,
pub user: RwSignal<Option<String>>,
} }
pub fn provide_torrent_store() { pub fn provide_torrent_store() {
@@ -128,6 +129,7 @@ pub fn provide_torrent_store() {
let search_query = create_rw_signal(String::new()); let search_query = create_rw_signal(String::new());
let global_stats = create_rw_signal(GlobalStats::default()); let global_stats = create_rw_signal(GlobalStats::default());
let notifications = create_rw_signal(Vec::<NotificationItem>::new()); let notifications = create_rw_signal(Vec::<NotificationItem>::new());
let user = create_rw_signal(Option::<String>::None);
let store = TorrentStore { let store = TorrentStore {
torrents, torrents,
@@ -135,6 +137,7 @@ pub fn provide_torrent_store() {
search_query, search_query,
global_stats, global_stats,
notifications, notifications,
user,
}; };
provide_context(store); provide_context(store);