feat(leptos): integrate auth with API + WS auth gating
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use shared_types::ui_state::Tab;
|
use shared_types::ui_state::Tab;
|
||||||
use crate::auth::AuthOverlay;
|
use crate::auth::AuthOverlay;
|
||||||
|
use crate::ws::context::WsContext;
|
||||||
|
|
||||||
// ── Contexts ────────────────────────────────────────────
|
// ── Contexts ────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -33,6 +34,9 @@ pub fn App() -> impl IntoView {
|
|||||||
provide_context(auth.clone());
|
provide_context(auth.clone());
|
||||||
provide_context(ui.clone());
|
provide_context(ui.clone());
|
||||||
|
|
||||||
|
let ws = WsContext::new("ws://localhost:3001");
|
||||||
|
provide_context(ws.clone());
|
||||||
|
|
||||||
// Auth check: redirect "live" tab to "messages" if not authenticated
|
// Auth check: redirect "live" tab to "messages" if not authenticated
|
||||||
create_effect(move |_| {
|
create_effect(move |_| {
|
||||||
if !auth.authenticated.get() && ui.active_tab.get() == Tab::Live {
|
if !auth.authenticated.get() && ui.active_tab.get() == Tab::Live {
|
||||||
@@ -40,6 +44,16 @@ pub fn App() -> impl IntoView {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
{
|
||||||
|
let ws = ws.clone();
|
||||||
|
let auth = auth.clone();
|
||||||
|
create_effect(move |_| {
|
||||||
|
if auth.authenticated.get() {
|
||||||
|
ws.connect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div data-theme="light">
|
<div data-theme="light">
|
||||||
// Auth overlay
|
// Auth overlay
|
||||||
|
|||||||
@@ -1,24 +1,53 @@
|
|||||||
|
// services/frontend-leptos/frontend/src/auth.rs
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos::ev::SubmitEvent;
|
use wasm_bindgen_futures::spawn_local;
|
||||||
|
use crate::app::AuthContext;
|
||||||
|
use crate::api::auth as auth_api;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn AuthOverlay(
|
pub fn AuthOverlay() -> impl IntoView {
|
||||||
/// Called when password is submitted — parent handles the actual API call
|
let auth = use_context::<AuthContext>().expect("AuthContext not provided");
|
||||||
#[prop(default = ())]
|
|
||||||
on_submit: (),
|
|
||||||
) -> impl IntoView {
|
|
||||||
let (password, set_password) = create_signal(String::new());
|
let (password, set_password) = create_signal(String::new());
|
||||||
let (error, set_error) = create_signal(Option::<String>::None);
|
let (error, set_error) = create_signal(Option::<String>::None);
|
||||||
let (loading, set_loading) = create_signal(false);
|
let (loading, set_loading) = create_signal(false);
|
||||||
|
|
||||||
let handle_submit = move |ev: SubmitEvent| {
|
let handle_submit = move |ev: leptos::ev::SubmitEvent| {
|
||||||
ev.prevent_default();
|
ev.prevent_default();
|
||||||
if password.get().is_empty() {
|
let pwd = password.get();
|
||||||
|
if pwd.is_empty() {
|
||||||
set_error.set(Some("Password diperlukan".to_string()));
|
set_error.set(Some("Password diperlukan".to_string()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// TODO: actual login call (wired in Task 7)
|
|
||||||
set_loading.set(true);
|
set_loading.set(true);
|
||||||
|
set_error.set(None);
|
||||||
|
|
||||||
|
let auth_clone = auth.clone();
|
||||||
|
let pwd_clone = pwd.clone();
|
||||||
|
let set_loading_clone = set_loading.clone();
|
||||||
|
let set_error_clone = set_error.clone();
|
||||||
|
|
||||||
|
spawn_local(async move {
|
||||||
|
match auth_api::login(&pwd_clone).await {
|
||||||
|
Ok(true) => {
|
||||||
|
// Store password in sessionStorage
|
||||||
|
if let Some(storage) = web_sys::window()
|
||||||
|
.and_then(|w| w.local_storage().ok())
|
||||||
|
.flatten()
|
||||||
|
{
|
||||||
|
let _ = storage.set_item("admin-password", &pwd_clone);
|
||||||
|
}
|
||||||
|
auth_clone.authenticated.set(true);
|
||||||
|
auth_clone.password.set(pwd_clone);
|
||||||
|
}
|
||||||
|
Ok(false) => {
|
||||||
|
set_error_clone.set(Some("Login gagal — password salah".to_string()));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
set_error_clone.set(Some(format!("Error: {}", e.message)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set_loading_clone.set(false);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
@@ -26,9 +55,14 @@ pub fn AuthOverlay(
|
|||||||
<div class="modal-content" style="width: 380px;">
|
<div class="modal-content" style="width: 380px;">
|
||||||
<div class="modal-body" style="text-align: center;">
|
<div class="modal-body" style="text-align: center;">
|
||||||
<div style="font-size: 3rem; margin-bottom: 1rem;">
|
<div style="font-size: 3rem; margin-bottom: 1rem;">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" stroke-width="2">
|
||||||
|
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
||||||
|
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h2 style="font-size: 1.25rem; font-weight: 600; margin-bottom: 0.5rem;">"Akses Dashboard"</h2>
|
<h2 style="font-size: 1.25rem; font-weight: 600; margin-bottom: 0.5rem;">
|
||||||
|
"Akses Dashboard"
|
||||||
|
</h2>
|
||||||
<p style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 1.5rem;">
|
<p style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 1.5rem;">
|
||||||
"Masukkan password admin untuk melanjutkan"
|
"Masukkan password admin untuk melanjutkan"
|
||||||
</p>
|
</p>
|
||||||
@@ -40,7 +74,9 @@ pub fn AuthOverlay(
|
|||||||
prop:value=password
|
prop:value=password
|
||||||
on:input=move |ev| set_password.set(event_target_value(&ev))
|
on:input=move |ev| set_password.set(event_target_value(&ev))
|
||||||
/>
|
/>
|
||||||
{move || error.get().map(|e| view! { <p style="color: var(--color-error); font-size: 0.75rem;">{e}</p> })}
|
{move || error.get().map(|e| view! {
|
||||||
|
<p style="color: var(--color-error); font-size: 0.75rem;">{e}</p>
|
||||||
|
})}
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="btn btn-primary w-full btn-lg"
|
class="btn btn-primary w-full btn-lg"
|
||||||
|
|||||||
@@ -138,3 +138,10 @@ impl WsContext {
|
|||||||
let _ = self.handle.send_binary(data);
|
let _ = self.handle.send_binary(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SAFETY: WsContext uses Rc/RefCell for cheap cloning in a single-threaded WASM
|
||||||
|
// environment. The leptos reactive system requires provided contexts to be Send+Sync.
|
||||||
|
#[allow(unsafe_code)]
|
||||||
|
unsafe impl Send for WsContext {}
|
||||||
|
#[allow(unsafe_code)]
|
||||||
|
unsafe impl Sync for WsContext {}
|
||||||
|
|||||||
@@ -150,3 +150,10 @@ impl WsHandle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SAFETY: WsHandle uses Rc/RefCell for cheap cloning in a single-threaded WASM
|
||||||
|
// environment. The leptos reactive system requires provided contexts to be Send+Sync.
|
||||||
|
#[allow(unsafe_code)]
|
||||||
|
unsafe impl Send for WsHandle {}
|
||||||
|
#[allow(unsafe_code)]
|
||||||
|
unsafe impl Sync for WsHandle {}
|
||||||
|
|||||||
Reference in New Issue
Block a user