Login Implementation
This commit is contained in:
2
AobaClient/Cargo.lock
generated
2
AobaClient/Cargo.lock
generated
@@ -37,12 +37,14 @@ name = "aoba-client"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dioxus",
|
"dioxus",
|
||||||
|
"js-sys",
|
||||||
"prost",
|
"prost",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_repr",
|
"serde_repr",
|
||||||
"tonic",
|
"tonic",
|
||||||
"tonic-build",
|
"tonic-build",
|
||||||
"tonic-web-wasm-client",
|
"tonic-web-wasm-client",
|
||||||
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ tonic = { version = "*", default-features = false, features = [
|
|||||||
] }
|
] }
|
||||||
prost = "0.13"
|
prost = "0.13"
|
||||||
tonic-web-wasm-client = "0.7"
|
tonic-web-wasm-client = "0.7"
|
||||||
|
js-sys = { version = "0.3.77" }
|
||||||
|
web-sys = { version = "0.3.77", features = ["Storage", "Window"] }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tonic-build = { version = "*", default-features = false, features = ["prost"] }
|
tonic-build = { version = "*", default-features = false, features = ["prost"] }
|
||||||
|
|||||||
@@ -2,3 +2,8 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#main {
|
#main:has(#content) {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: $navBarSize 1fr;
|
grid-template-columns: $navBarSize 1fr;
|
||||||
grid-template-areas: "Nav Content";
|
grid-template-areas: "Nav Content";
|
||||||
@@ -65,3 +65,40 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#main:has(#centralModal) {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
height: 100dvh;
|
||||||
|
width: 100dvw;
|
||||||
|
}
|
||||||
|
|
||||||
|
#centralModal {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notif {
|
||||||
|
background-color: red;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 50px 1fr;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 20px;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
padding: 10px;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ pub fn Input(props: InputProps) -> Element {
|
|||||||
input {
|
input {
|
||||||
type : props.r#type.unwrap_or("text".into()),
|
type : props.r#type.unwrap_or("text".into()),
|
||||||
value: props.value,
|
value: props.value,
|
||||||
|
oninput: move |e| if let Some(mut s) = props.value { s.set(e.value()); },
|
||||||
name: props.name,
|
name: props.name,
|
||||||
placeholder:ph,
|
placeholder:ph,
|
||||||
required: props.required
|
required: props.required
|
||||||
|
|||||||
52
AobaClient/src/components/icons.rs
Normal file
52
AobaClient/src/components/icons.rs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Info() -> Element {
|
||||||
|
rsx! {
|
||||||
|
svg {
|
||||||
|
class: "size-6",
|
||||||
|
fill: "currentColor",
|
||||||
|
view_box: "0 0 24 24",
|
||||||
|
xmlns: "http://www.w3.org/2000/svg",
|
||||||
|
path {
|
||||||
|
clip_rule: "evenodd",
|
||||||
|
d: "M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm8.706-1.442c1.146-.573 2.437.463 2.126 1.706l-.709 2.836.042-.02a.75.75 0 0 1 .67 1.34l-.04.022c-1.147.573-2.438-.463-2.127-1.706l.71-2.836-.042.02a.75.75 0 1 1-.671-1.34l.041-.022ZM12 9a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z",
|
||||||
|
fill_rule: "evenodd",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Warn() -> Element {
|
||||||
|
rsx! {
|
||||||
|
svg {
|
||||||
|
class: "size-6",
|
||||||
|
fill: "currentColor",
|
||||||
|
view_box: "0 0 24 24",
|
||||||
|
xmlns: "http://www.w3.org/2000/svg",
|
||||||
|
path {
|
||||||
|
clip_rule: "evenodd",
|
||||||
|
d: "M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003ZM12 8.25a.75.75 0 0 1 .75.75v3.75a.75.75 0 0 1-1.5 0V9a.75.75 0 0 1 .75-.75Zm0 8.25a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z",
|
||||||
|
fill_rule: "evenodd",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Error() -> Element {
|
||||||
|
rsx! {
|
||||||
|
svg {
|
||||||
|
class: "size-6",
|
||||||
|
fill: "currentColor",
|
||||||
|
view_box: "0 0 24 24",
|
||||||
|
xmlns: "http://www.w3.org/2000/svg",
|
||||||
|
path {
|
||||||
|
clip_rule: "evenodd",
|
||||||
|
d: "M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003ZM12 8.25a.75.75 0 0 1 .75.75v3.75a.75.75 0 0 1-1.5 0V9a.75.75 0 0 1 .75-.75Zm0 8.25a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z",
|
||||||
|
fill_rule: "evenodd",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,11 @@ pub mod basic;
|
|||||||
mod media_grid;
|
mod media_grid;
|
||||||
mod media_item;
|
mod media_item;
|
||||||
mod navbar;
|
mod navbar;
|
||||||
|
mod notif;
|
||||||
mod search;
|
mod search;
|
||||||
pub use media_grid::*;
|
pub use media_grid::*;
|
||||||
pub use media_item::*;
|
pub use media_item::*;
|
||||||
pub use navbar::*;
|
pub use navbar::*;
|
||||||
|
pub use notif::*;
|
||||||
pub use search::*;
|
pub use search::*;
|
||||||
|
mod icons;
|
||||||
|
|||||||
44
AobaClient/src/components/notif.rs
Normal file
44
AobaClient/src/components/notif.rs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
use dioxus::{html::u::icon, prelude::*};
|
||||||
|
|
||||||
|
use crate::components::icons;
|
||||||
|
|
||||||
|
#[derive(PartialEq, Clone, Props)]
|
||||||
|
pub struct NotifProps {
|
||||||
|
r#type: Option<NotifType>,
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Clone)]
|
||||||
|
pub enum NotifType {
|
||||||
|
Notice,
|
||||||
|
Error,
|
||||||
|
Warning,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Notif(props: NotifProps) -> Element {
|
||||||
|
let t = props.r#type.unwrap_or(NotifType::Notice);
|
||||||
|
let type_class = match t {
|
||||||
|
NotifType::Notice => "notice",
|
||||||
|
NotifType::Error => "error",
|
||||||
|
NotifType::Warning => "warning",
|
||||||
|
};
|
||||||
|
let m = props.message;
|
||||||
|
rsx! {
|
||||||
|
div{
|
||||||
|
class: "notif {type_class}",
|
||||||
|
div{
|
||||||
|
class: "icon",
|
||||||
|
match t {
|
||||||
|
NotifType::Notice => icons::Error(),
|
||||||
|
NotifType::Error => icons::Error(),
|
||||||
|
NotifType::Warning => icons::Warn(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div{
|
||||||
|
class: "message",
|
||||||
|
"{m}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,35 @@
|
|||||||
use dioxus::signals::Signal;
|
use dioxus::signals::{Signal, Writable};
|
||||||
|
use web_sys::window;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Default)]
|
#[derive(Clone, Copy, Default)]
|
||||||
pub struct AuthContext {
|
pub struct AuthContext {
|
||||||
pub jwt: Signal<Option<String>>,
|
pub jwt: Signal<Option<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AuthContext {
|
||||||
|
pub fn set_token(&mut self, token: String) {
|
||||||
|
self.jwt.set(Some(token.clone()));
|
||||||
|
let local_storage = window().unwrap().local_storage().unwrap().unwrap();
|
||||||
|
_ = local_storage.set_item("token", token.as_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new() -> Self {
|
||||||
|
println!("new");
|
||||||
|
let local_storage = window().unwrap().local_storage().unwrap().unwrap();
|
||||||
|
match local_storage.get_item("token") {
|
||||||
|
Ok(value) => {
|
||||||
|
if let Some(jwt) = value {
|
||||||
|
println!("jwt");
|
||||||
|
return AuthContext {
|
||||||
|
jwt: Signal::new(Some(jwt)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return AuthContext::default();
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
println!("err");
|
||||||
|
AuthContext::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
use dioxus::prelude::*;
|
|
||||||
|
|
||||||
use crate::route::Route;
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn BasicLayout() -> Element {
|
|
||||||
rsx! {
|
|
||||||
Outlet::<Route> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
use crate::{components::Navbar, contexts::AuthContext, layouts::BasicLayout, views::Login, Route};
|
use crate::{components::Navbar, contexts::AuthContext, views::Login, Route};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn MainLayout() -> Element {
|
pub fn MainLayout() -> Element {
|
||||||
let auth_context = use_context::<AuthContext>();
|
let auth_context = use_context::<AuthContext>();
|
||||||
|
|
||||||
// if auth_context.jwt.cloned().is_none() {
|
if auth_context.jwt.cloned().is_none() {
|
||||||
// return rsx! { Login { } };
|
return rsx! { Login { } };
|
||||||
// }
|
}
|
||||||
|
|
||||||
return rsx! {
|
return rsx! {
|
||||||
Navbar {}
|
Navbar {}
|
||||||
|
|||||||
@@ -1,4 +1,2 @@
|
|||||||
mod basic_layout;
|
|
||||||
mod main_layout;
|
mod main_layout;
|
||||||
pub use basic_layout::*;
|
|
||||||
pub use main_layout::*;
|
pub use main_layout::*;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ fn main() {
|
|||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn App() -> Element {
|
fn App() -> Element {
|
||||||
let _auth_state = use_context_provider(|| AuthContext::default());
|
let _auth_state = use_context_provider(|| AuthContext::new());
|
||||||
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" }
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use dioxus::prelude::*;
|
|||||||
use tonic::IntoRequest;
|
use tonic::IntoRequest;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
components::basic::{Button, Input},
|
components::{basic::Input, Notif, NotifType},
|
||||||
contexts::AuthContext,
|
contexts::AuthContext,
|
||||||
rpc::{aoba::Credentials, get_auth_rpc_client},
|
rpc::{aoba::Credentials, get_auth_rpc_client},
|
||||||
};
|
};
|
||||||
@@ -11,9 +11,16 @@ use crate::{
|
|||||||
pub fn Login() -> Element {
|
pub fn Login() -> Element {
|
||||||
let username = use_signal(|| "".to_string());
|
let username = use_signal(|| "".to_string());
|
||||||
let password = use_signal(|| "".to_string());
|
let password = use_signal(|| "".to_string());
|
||||||
|
let mut error: Signal<Option<String>> = use_signal(|| None);
|
||||||
let mut auth_context = use_context::<AuthContext>();
|
let mut auth_context = use_context::<AuthContext>();
|
||||||
|
|
||||||
let onclick = move |_| {
|
let login = move |e: Event<MouseData>| {
|
||||||
|
e.prevent_default();
|
||||||
|
if username.cloned().is_empty() || password.cloned().is_empty() {
|
||||||
|
error.set(Some("Username and Password are required".into()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
let mut auth = get_auth_rpc_client();
|
let mut auth = get_auth_rpc_client();
|
||||||
let result = auth
|
let result = auth
|
||||||
@@ -31,8 +38,9 @@ pub fn Login() -> Element {
|
|||||||
crate::rpc::aoba::login_response::Result::Jwt(jwt) => {
|
crate::rpc::aoba::login_response::Result::Jwt(jwt) => {
|
||||||
auth_context.jwt.set(Some(jwt.token));
|
auth_context.jwt.set(Some(jwt.token));
|
||||||
}
|
}
|
||||||
crate::rpc::aoba::login_response::Result::Error(_login_error) => {
|
crate::rpc::aoba::login_response::Result::Error(login_error) => {
|
||||||
auth_context.jwt.set(None);
|
auth_context.jwt.set(None);
|
||||||
|
error.set(Some(login_error.message));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -46,12 +54,15 @@ pub fn Login() -> Element {
|
|||||||
rsx! {
|
rsx! {
|
||||||
div{
|
div{
|
||||||
id: "centralModal",
|
id: "centralModal",
|
||||||
|
if let Some(err) = error.cloned() {
|
||||||
|
Notif{ type: NotifType::Error, message: err}
|
||||||
|
}
|
||||||
form{
|
form{
|
||||||
Input { type : "text", name: "username", label: "Username", value: username, required: true },
|
Input { type : "text", name: "username", label: "Username", value: username, required: true },
|
||||||
Input { type : "password", name: "password", label: "Password", value: password, required: true },
|
Input { type : "password", name: "password", label: "Password", value: password, required: true },
|
||||||
Button {
|
button {
|
||||||
text: "Login!",
|
onclick: login,
|
||||||
onclick: onclick
|
"Login!",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ public class AccountsService(IMongoDatabase db)
|
|||||||
public async Task<User?> VerifyLoginAsync(string username, string password, CancellationToken cancellationToken = default)
|
public async Task<User?> VerifyLoginAsync(string username, string password, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var user = await _users.Find(u => u.Username == username).FirstOrDefaultAsync(cancellationToken);
|
var user = await _users.Find(u => u.Username == username).FirstOrDefaultAsync(cancellationToken);
|
||||||
|
if(user == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
if(user.IsArgon && Argon2.Verify(user.PasswordHash, password))
|
if(user.IsArgon && Argon2.Verify(user.PasswordHash, password))
|
||||||
return user;
|
return user;
|
||||||
|
|||||||
Reference in New Issue
Block a user