Added Auth Context

implement login client code (todo: server login)
Added aboa icon
This commit is contained in:
2025-05-03 23:52:51 -04:00
parent e223612a08
commit d0cc8be566
21 changed files with 135 additions and 48 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -1,8 +1,8 @@
$mainBGColor: #584577; $mainBGColor: #584577;
$featureColor: #CE2D4F; $featureColor: #ce2d4f;
$accentColor: #f0eaf8; $accentColor: #f0eaf8;
$mainTextColor: #eee; $mainTextColor: #eee;
$brightTextColor: #fff; $brightTextColor: #fff;
$invertTextColor: #222; $invertTextColor: #222;
$invertBrightTextColor: #000; $invertBrightTextColor: #000;

View File

@@ -26,7 +26,8 @@ body {
#content { #content {
grid-area: Content; grid-area: Content;
margin-left: $navBarSize; padding: 10px;
/* margin-left: $navBarSize; */
} }
.mediaGrid { .mediaGrid {
@@ -55,6 +56,7 @@ body {
text-align: center; text-align: center;
width: 100%; width: 100%;
display: block; display: block;
overflow: hidden;
} }
.details { .details {
display: flex; display: flex;

View File

@@ -1,4 +1,4 @@
$navBarSize: 40px; $navBarSize: 64px;
@mixin mobile { @mixin mobile {
@media (max-width: 700px) { @media (max-width: 700px) {
@@ -40,4 +40,4 @@ $navBarSize: 40px;
@container (max-width: 1200px) { @container (max-width: 1200px) {
@content; @content;
} }
} }

View File

@@ -1,6 +1,5 @@
@import 'mixins'; @import "mixins";
@import 'colors'; @import "colors";
nav { nav {
display: grid; display: grid;
@@ -9,14 +8,20 @@ nav {
background-color: $featureColor; background-color: $featureColor;
height: 100dvh; height: 100dvh;
position: fixed; position: fixed;
width: $navBarSize;
>* { > * {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.branding { .branding {
grid-area: Branding; grid-area: Branding;
img {
width: $navBarSize;
object-fit: contain;
}
} }
.mainNav { .mainNav {
@@ -30,5 +35,4 @@ nav {
.utils { .utils {
grid-area: Utils; grid-area: Utils;
} }
}
}

View File

@@ -19,6 +19,7 @@ pub fn Button(props: ButtonProps) -> Element {
rsx! { rsx! {
button { button {
onclick: move |event| { onclick: move |event| {
event.prevent_default();
if let Some(h) = props.onclick { if let Some(h) = props.onclick {
h.call(event); h.call(event);
} }

View File

@@ -3,11 +3,12 @@ use dioxus::prelude::*;
#[derive(PartialEq, Clone, Props)] #[derive(PartialEq, Clone, Props)]
pub struct InputProps { pub struct InputProps {
pub r#type: Option<String>, pub r#type: Option<String>,
pub value: Option<String>, pub value: Option<Signal<String>>,
pub label: Option<String>, pub label: Option<String>,
pub placeholder: Option<String>, pub placeholder: Option<String>,
pub name: String, pub name: String,
pub oninput: Option<EventHandler<FormEvent>>, pub oninput: Option<EventHandler<FormEvent>>,
pub required: Option<bool>,
} }
#[component] #[component]
@@ -21,7 +22,8 @@ pub fn Input(props: InputProps) -> Element {
type : props.r#type.unwrap_or("text".into()), type : props.r#type.unwrap_or("text".into()),
value: props.value, value: props.value,
name: props.name, name: props.name,
placeholder:ph placeholder:ph,
required: props.required
} }
} }
} }

View File

@@ -3,6 +3,7 @@ use tonic::{IntoRequest, Request};
use crate::{ use crate::{
components::MediaItem, components::MediaItem,
contexts::AuthContext,
rpc::{aoba::PageFilter, get_rpc_client}, rpc::{aoba::PageFilter, get_rpc_client},
}; };
@@ -34,11 +35,17 @@ impl Into<PageFilter> for MediaGridProps {
#[component] #[component]
pub fn MediaGrid(props: MediaGridProps) -> Element { pub fn MediaGrid(props: MediaGridProps) -> Element {
let media_result = use_resource(use_reactive!(|(props,)| async move { let jwt = use_context::<AuthContext>().jwt;
let media_result = use_resource(use_reactive!(|(props, jwt)| async move {
let mut client = get_rpc_client(); let mut client = get_rpc_client();
let mut req = Request::new(props.into()); let mut req = Request::new(props.into());
let token = if jwt.cloned().is_some() {
jwt.unwrap()
} else {
"".into()
};
req.metadata_mut() req.metadata_mut()
.insert("authorization", "Bearer <toto: get token>".parse().unwrap()); .insert("authorization", format!("Bearer {token}").parse().unwrap());
let result = client.list_media(req).await; let result = client.list_media(req).await;
return result.expect("Failed to load media").into_inner(); return result.expect("Failed to load media").into_inner();
})); }));

View File

@@ -3,6 +3,7 @@ use dioxus::prelude::*;
use crate::Route; use crate::Route;
const NAV_CSS: Asset = asset!("/assets/style/nav.scss"); const NAV_CSS: Asset = asset!("/assets/style/nav.scss");
const NAV_ICON: Asset = asset!("/assets/favicon.ico");
#[component] #[component]
pub fn Navbar() -> Element { pub fn Navbar() -> Element {
@@ -30,7 +31,10 @@ pub fn MainNaviagation() -> Element {
#[component] #[component]
pub fn Branding() -> Element { pub fn Branding() -> Element {
rsx! { rsx! {
div { class: "branding" } div { class: "branding",
"Aoba"
img {src: NAV_ICON}
}
} }
} }

View File

@@ -1,15 +1,15 @@
use dioxus::prelude::*; use dioxus::prelude::*;
#[component] #[component]
pub fn Search(query: Option<String>, oninput: EventHandler<FormEvent>) -> Element { pub fn Search(query: Signal<String>) -> Element {
rsx! { rsx! {
div{ div{
class: "searchBar", class: "searchBar",
input { input {
type: "search", type: "search",
placeholder: "Search Files", placeholder: "Search Files",
value: query.unwrap_or("".into()), value: query,
oninput: move |event| oninput.call(event) oninput: move |event| query.set(event.value())
} }
} }
} }

View File

@@ -0,0 +1,6 @@
use dioxus::signals::Signal;
#[derive(Clone, Copy, Default)]
pub struct AuthContext {
pub jwt: Signal<Option<String>>,
}

View File

@@ -0,0 +1,2 @@
mod auth_context;
pub use auth_context::*;

View File

@@ -1,11 +1,17 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use crate::{components::Navbar, Route}; use crate::{components::Navbar, contexts::AuthContext, layouts::BasicLayout, views::Login, Route};
#[component] #[component]
pub fn MainLayout() -> Element { pub fn MainLayout() -> Element {
rsx! { let auth_context = use_context::<AuthContext>();
// if auth_context.jwt.cloned().is_none() {
// return rsx! { Login { } };
// }
return rsx! {
Navbar {} Navbar {}
Outlet::<Route> {} Outlet::<Route> {}
} };
} }

View File

@@ -1,10 +1,12 @@
pub mod components; pub mod components;
pub mod contexts;
mod layouts; mod layouts;
pub mod models; pub mod models;
pub mod route; pub mod route;
pub mod rpc; pub mod rpc;
pub mod views; pub mod views;
use contexts::AuthContext;
use dioxus::prelude::*; use dioxus::prelude::*;
use route::Route; use route::Route;
@@ -17,6 +19,7 @@ fn main() {
#[component] #[component]
fn App() -> Element { fn App() -> Element {
let _auth_state = use_context_provider(|| AuthContext::default());
rsx! { rsx! {
document::Link { rel: "icon", href: FAVICON } document::Link { rel: "icon", href: FAVICON }
document::Link { rel: "preconnect", href: "https://fonts.googleapis.com" } document::Link { rel: "preconnect", href: "https://fonts.googleapis.com" }
@@ -26,6 +29,6 @@ fn App() -> Element {
rel: "stylesheet", rel: "stylesheet",
href: "https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap", href: "https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap",
} }
Router::<Route> {} Router::<Route> { }
} }
} }

View File

@@ -1,6 +1,6 @@
use crate::{ use crate::{
layouts::{BasicLayout, MainLayout}, layouts::MainLayout,
views::{Home, Login, Settings}, views::{Home, Settings},
}; };
use dioxus::prelude::*; use dioxus::prelude::*;
@@ -12,8 +12,5 @@ pub enum Route {
Home {}, Home {},
#[route("/settings")] #[route("/settings")]
Settings {}, Settings {},
#[end_layout] // #[end_layout]
#[layout(BasicLayout)]
#[route("/login")]
Login {},
} }

View File

@@ -1,32 +1,42 @@
use std::sync::RwLock; use std::sync::RwLock;
use aoba::aoba_rpc_client::AobaRpcClient; use aoba::{aoba_rpc_client::AobaRpcClient, auth_rpc_client::AuthRpcClient};
use tonic_web_wasm_client::Client; use tonic_web_wasm_client::Client;
pub mod aoba { pub mod aoba {
tonic::include_proto!("aoba"); tonic::include_proto!("aoba");
tonic::include_proto!("aoba.auth");
} }
const HOST: &'static str = "http://localhost:5164";
static RPC_CLIENT: RpcConnection = RpcConnection { static RPC_CLIENT: RpcConnection = RpcConnection {
client: RwLock::new(None), aoba: RwLock::new(None),
auth: RwLock::new(None),
}; };
#[derive(Default)] #[derive(Default)]
pub struct RpcConnection { pub struct RpcConnection {
client: RwLock<Option<AobaRpcClient<Client>>>, aoba: RwLock<Option<AobaRpcClient<Client>>>,
auth: RwLock<Option<AuthRpcClient<Client>>>,
} }
impl RpcConnection { impl RpcConnection {
pub fn get_client(&self) -> AobaRpcClient<Client> { pub fn get_client(&self) -> AobaRpcClient<Client> {
self.ensure_client(); self.ensure_client();
return self.client.read().unwrap().clone().unwrap(); return self.aoba.read().unwrap().clone().unwrap();
}
pub fn get_auth_client(&self) -> AuthRpcClient<Client> {
self.ensure_client();
return self.auth.read().unwrap().clone().unwrap();
} }
fn ensure_client(&self) { fn ensure_client(&self) {
if self.client.read().unwrap().is_none() { if self.aoba.read().unwrap().is_none() {
let wasm_client = Client::new("http://localhost:5164".into()); let wasm_client = Client::new(HOST.into());
let c = AobaRpcClient::new(wasm_client); *self.aoba.write().unwrap() = Some(AobaRpcClient::new(wasm_client.clone()));
*self.client.write().unwrap() = Some(c); *self.auth.write().unwrap() = Some(AuthRpcClient::new(wasm_client.clone()));
} }
} }
} }
@@ -34,3 +44,7 @@ impl RpcConnection {
pub fn get_rpc_client() -> AobaRpcClient<Client> { pub fn get_rpc_client() -> AobaRpcClient<Client> {
return RPC_CLIENT.get_client(); return RPC_CLIENT.get_client();
} }
pub fn get_auth_rpc_client() -> AuthRpcClient<Client> {
return RPC_CLIENT.get_auth_client();
}

View File

@@ -3,16 +3,13 @@ use dioxus::prelude::*;
#[component] #[component]
pub fn Home() -> Element { pub fn Home() -> Element {
let mut query = use_signal(|| "".to_string()); let query = use_signal(|| "".to_string());
rsx! { rsx! {
div { div {
id: "content", id: "content",
Search { Search {
query: query.cloned(), query: query
oninput: move |event:FormEvent| {
query.set(event.value())
}
}, },
MediaGrid { query: query.cloned() } MediaGrid { query: query.cloned() }
} }

View File

@@ -1,16 +1,58 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use tonic::IntoRequest;
use crate::components::basic::{Button, Input}; use crate::{
components::basic::{Button, Input},
contexts::AuthContext,
rpc::{aoba::Credentials, get_auth_rpc_client},
};
#[component] #[component]
pub fn Login() -> Element { pub fn Login() -> Element {
let username = use_signal(|| "".to_string());
let password = use_signal(|| "".to_string());
let mut auth_context = use_context::<AuthContext>();
let onclick = move |_| {
spawn(async move {
let mut auth = get_auth_rpc_client();
let result = auth
.login(
Credentials {
user: username.cloned(),
password: password.cloned(),
}
.into_request(),
)
.await;
match result {
Ok(res) => {
match res.into_inner().result.unwrap() {
crate::rpc::aoba::login_response::Result::Jwt(jwt) => {
auth_context.jwt.set(Some(jwt.token));
}
crate::rpc::aoba::login_response::Result::Error(_login_error) => {
auth_context.jwt.set(None);
}
};
}
Err(_err) => {
auth_context.jwt.set(None);
}
}
});
};
rsx! { rsx! {
div{ div{
id: "centralModal", id: "centralModal",
form{ form{
Input { type : "text", name: "username", label: "Username" }, Input { type : "text", name: "username", label: "Username", value: username, required: true },
Input { type : "password", name: "password", label: "Password" }, Input { type : "password", name: "password", label: "Password", value: password, required: true },
Button{text: "Login!"} Button {
text: "Login!",
onclick: onclick
}
} }
} }
} }

View File

@@ -18,7 +18,7 @@ public class AobaService(IMongoDatabase db)
return await _media.Find(m => m.Id == id).FirstOrDefaultAsync(cancellationToken); return await _media.Find(m => m.Id == id).FirstOrDefaultAsync(cancellationToken);
} }
public async Task<PagedResult<Media>> FindMediaAsync(string? query, int page = 1, int pageSize = 50) public async Task<PagedResult<Media>> FindMediaAsync(string? query, int page = 1, int pageSize = 100)
{ {
var filter = string.IsNullOrWhiteSpace(query) ? "{}" : Builders<Media>.Filter.Text(query); var filter = string.IsNullOrWhiteSpace(query) ? "{}" : Builders<Media>.Filter.Text(query);
var find = _media.Find(filter); var find = _media.Find(filter);

View File

@@ -18,7 +18,7 @@ public class AobaRpcService(AobaService aobaService) : AobaRpc.AobaRpcBase
public override async Task<ListResponse> ListMedia(PageFilter request, ServerCallContext context) public override async Task<ListResponse> ListMedia(PageFilter request, ServerCallContext context)
{ {
var result = await aobaService.FindMediaAsync(request.Query, request.HasPage ? request.Page : 1, request.HasPageSize ? request.PageSize : 50); var result = await aobaService.FindMediaAsync(request.Query, request.HasPage ? request.Page : 1, request.HasPageSize ? request.PageSize : 100);
return result.ToResponse(); return result.ToResponse();
} }