Skip to content

Commit

Permalink
frontend: add applications shell (#657)
Browse files Browse the repository at this point in the history
* add applications shell

* add login redirect

* add authorization header

* add user info handler

* add user info shell

* refactor root and shell

* prevent client is none
  • Loading branch information
vnghia authored Jan 12, 2025
1 parent d51c299 commit 1e59b87
Show file tree
Hide file tree
Showing 19 changed files with 496 additions and 43 deletions.
37 changes: 37 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions nghe-api/src/user/info.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use nghe_proc_macro::api_derive;

use super::Role;

#[api_derive(fake = true)]
#[endpoint(path = "userInfo", internal = true)]
pub struct Request;

#[api_derive]
#[derive(Clone)]
pub struct Response {
pub username: String,
pub email: String,
pub role: Role,
}
1 change: 1 addition & 0 deletions nghe-api/src/user/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod create;
pub mod info;
mod role;
pub mod setup;

Expand Down
1 change: 1 addition & 0 deletions nghe-api/src/user/role.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use nghe_proc_macro::api_derive;

#[api_derive(fake = true)]
#[derive(Clone, Copy)]
pub struct Role {
pub admin: bool,
pub stream: bool,
Expand Down
17 changes: 14 additions & 3 deletions nghe-backend/src/orm/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,27 @@ pub struct UsernameAuthentication<'a> {
pub password: Cow<'a, [u8]>,
}

#[derive(Debug, Queryable, Selectable, Insertable)]
#[derive(Debug, Queryable, Selectable, Insertable, o2o)]
#[diesel(table_name = users, check_for_backend(super::Type))]
pub struct Data<'a> {
#[owned_into(nghe_api::user::info::Response)]
pub struct Info<'a> {
#[into(~.into_owned())]
pub username: Cow<'a, str>,
pub password: Cow<'a, [u8]>,
#[into(~.into_owned())]
pub email: Cow<'a, str>,
#[diesel(embed)]
#[into(~.into())]
pub role: Role,
}

#[derive(Debug, Queryable, Selectable, Insertable)]
#[diesel(table_name = users, check_for_backend(super::Type))]
pub struct Data<'a> {
#[diesel(embed)]
pub info: Info<'a>,
pub password: Cow<'a, [u8]>,
}

#[derive(Debug, Queryable, Selectable, Identifiable)]
#[diesel(table_name = users, check_for_backend(super::Type))]
pub struct User<'a> {
Expand Down
4 changes: 1 addition & 3 deletions nghe-backend/src/route/user/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,8 @@ pub async fn handler(database: &Database, request: Request) -> Result<Response,

let user_id = diesel::insert_into(users::table)
.values(users::Data {
username: username.into(),
info: users::Info { username: username.into(), email: email.into(), role: role.into() },
password: password.into(),
email: email.into(),
role: role.into(),
})
.returning(users::id)
.get_result(&mut database.get().await?)
Expand Down
37 changes: 37 additions & 0 deletions nghe-backend/src/route/user/info.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use diesel::{ExpressionMethods, QueryDsl, SelectableHelper};
use diesel_async::RunQueryDsl;
pub use nghe_api::user::info::{Request, Response};
use nghe_proc_macro::handler;
use uuid::Uuid;

use crate::Error;
use crate::database::Database;
use crate::orm::users;

#[handler(internal = true)]
pub async fn handler(database: &Database, user_id: Uuid) -> Result<Response, Error> {
users::table
.filter(users::id.eq(user_id))
.select(users::Info::as_select())
.first(&mut database.get().await?)
.await
.map(users::Info::into)
.map_err(Error::from)
}

#[cfg(test)]
#[coverage(off)]
mod tests {
use rstest::rstest;

use super::*;
use crate::test::{Mock, mock};

#[rstest]
#[tokio::test]
async fn test_handler(#[future(awt)] mock: Mock) {
let user = mock.user(0).await;
let user_info = handler(mock.database(), user.id()).await.unwrap();
assert_eq!(user.username(), user_info.username);
}
}
2 changes: 2 additions & 0 deletions nghe-backend/src/route/user/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
pub mod create;
mod info;
mod setup;

nghe_proc_macro::build_router! {
modules = [
create(internal = true),
info(internal = true),
setup(internal = true),
],
}
4 changes: 2 additions & 2 deletions nghe-backend/src/test/mock_impl/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ impl<'a> Mock<'a> {
self.user.id
}

fn username(&self) -> String {
self.user.data.username.to_string()
pub fn username(&self) -> String {
self.user.data.info.username.to_string()
}

fn password(&self) -> String {
Expand Down
7 changes: 7 additions & 0 deletions nghe-frontend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,21 @@ edition = { workspace = true }
workspace = true

[dependencies]
concat-string = { workspace = true }
uuid = { workspace = true }

anyhow = { version = "1.0.95" }
codee = { version = "0.2.0" }
console_error_panic_hook = { version = "0.1.7" }
gloo-net = { version = "0.6.0", default-features = false, features = [
"http",
"json",
] }
leptos = { version = "0.7.2", features = ["csr", "nightly"] }
leptos_router = { version = "0.7.2", features = ["nightly"] }
leptos-use = { version = "0.15.3", default-features = false, features = [
"storage",
] }
wasm-bindgen = { version = "0.2.99" }

nghe_api = { path = "../nghe-api" }
Expand Down
61 changes: 58 additions & 3 deletions nghe-frontend/src/client.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,71 @@
use anyhow::Error;
use codee::string::{FromToStringCodec, OptionCodec};
use concat_string::concat_string;
use gloo_net::http;
use leptos::prelude::*;
use leptos_router::NavigateOptions;
use leptos_router::hooks::use_navigate;
use leptos_use::storage::use_local_storage;
use nghe_api::common::{JsonEndpoint, JsonURL};
use uuid::Uuid;

pub struct Client;
#[derive(Clone)]
pub struct Client {
authorization: String,
}

impl Client {
pub async fn json<R: JsonEndpoint>(request: &R) -> Result<R::Response, Error> {
let response = http::Request::post(<R as JsonURL>::URL_JSON).json(request)?.send().await?;
const API_KEY_STORAGE_KEY: &'static str = "api-key";

pub const EXPECT_MSG: &'static str = "use_client_redirect should prevent this";

pub fn new(api_key: Uuid) -> Self {
Self { authorization: concat_string!("Bearer ", api_key.to_string()) }
}

pub fn use_api_key() -> (Signal<Option<Uuid>>, WriteSignal<Option<Uuid>>) {
let (read, write, _) = use_local_storage::<Option<Uuid>, OptionCodec<FromToStringCodec>>(
Self::API_KEY_STORAGE_KEY,
);
(read, write)
}

pub fn use_client() -> Signal<Option<Client>> {
let (read_api_key, _) = Self::use_api_key();
Signal::derive(move || read_api_key.with(|api_key| api_key.map(Client::new)))
}

pub fn use_client_redirect() -> (Signal<Option<Client>>, Effect<LocalStorage>) {
let client = Self::use_client();
let effect = Effect::new(move || {
if client.with(Option::is_none) {
use_navigate()("/login", NavigateOptions::default());
}
});
(client, effect)
}

async fn json_impl<R: JsonEndpoint>(
request: &R,
authorization: Option<&str>,
) -> Result<R::Response, Error> {
let response = http::Request::post(<R as JsonURL>::URL_JSON)
.header("Authorization", authorization.unwrap_or_default())
.json(request)?
.send()
.await?;
if response.ok() {
Ok(response.json().await?)
} else {
anyhow::bail!("{}", response.text().await?)
}
}

pub async fn json_no_auth<R: JsonEndpoint>(request: &R) -> Result<R::Response, Error> {
Self::json_impl(request, None).await
}

pub async fn json<R: JsonEndpoint>(&self, request: &R) -> Result<R::Response, Error> {
Self::json_impl(request, Some(&self.authorization)).await
}
}
24 changes: 16 additions & 8 deletions nghe-frontend/src/components/authentication/login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ use crate::client::Client;
use crate::components::form;

pub fn Login() -> impl IntoView {
let navigate = leptos_router::hooks::use_navigate();

let (read_api_key, set_api_key) = Client::use_api_key();
Effect::new(move || {
if read_api_key.with(Option::is_some) {
navigate("/", NavigateOptions::default());
}
});

let username = RwSignal::new(String::default());
let password = RwSignal::new(String::default());
let client = RwSignal::new(nghe_api::constant::SERVER_NAME.into());
Expand All @@ -15,19 +24,18 @@ pub fn Login() -> impl IntoView {
let (password_error, set_password_error) = signal(Option::default());
let (client_error, set_client_error) = signal(Option::default());

let login_action = Action::<_, _, SyncStorage>::new_unsync(|request: &Request| {
let login_action = Action::<_, _, SyncStorage>::new_unsync(move |request: &Request| {
let request = request.clone();
async move {
Client::json(&request).await.map_err(|error| error.to_string())?;
let api_key = Client::json_no_auth(&request)
.await
.map_err(|error| error.to_string())?
.api_key
.api_key;
set_api_key(Some(api_key));
Ok::<_, String>(())
}
});
Effect::new(move || {
if login_action.value().with(|result| result.as_ref().is_some_and(Result::is_ok)) {
let navigate = leptos_router::hooks::use_navigate();
navigate("/", NavigateOptions::default());
}
});

html::section().class("bg-gray-50 dark:bg-gray-900").child(
html::div()
Expand Down
2 changes: 1 addition & 1 deletion nghe-frontend/src/components/authentication/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pub fn Setup() -> impl IntoView {
let setup_action = Action::<_, _, SyncStorage>::new_unsync(|request: &Request| {
let request = request.clone();
async move {
Client::json(&request).await.map_err(|error| error.to_string())?;
Client::json_no_auth(&request).await.map_err(|error| error.to_string())?;
Ok::<_, String>(())
}
});
Expand Down
Loading

0 comments on commit 1e59b87

Please sign in to comment.