Compare commits
6 Commits
v1.3.2
...
14a6e18966
| Author | SHA1 | Date | |
|---|---|---|---|
| 14a6e18966 | |||
| a6e2156d97 | |||
| 8ff4fa74e4 | |||
| 6c2305cac9 | |||
| 7a43d5c11f | |||
| 4325280020 |
Generated
+18
-18
@@ -24,8 +24,10 @@ dependencies = [
|
|||||||
"dioxus",
|
"dioxus",
|
||||||
"dioxus-primitives",
|
"dioxus-primitives",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
|
"js-sys",
|
||||||
"prost",
|
"prost",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde-wasm-bindgen",
|
||||||
"serde_repr",
|
"serde_repr",
|
||||||
"tonic",
|
"tonic",
|
||||||
"tonic-build",
|
"tonic-build",
|
||||||
@@ -1669,10 +1671,12 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.91"
|
version = "0.3.95"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
|
checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"futures-util",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
@@ -3340,9 +3344,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.114"
|
version = "0.2.118"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
|
checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@@ -3353,23 +3357,19 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-futures"
|
name = "wasm-bindgen-futures"
|
||||||
version = "0.4.64"
|
version = "0.4.68"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8"
|
checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
|
||||||
"futures-util",
|
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"once_cell",
|
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"web-sys",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.114"
|
version = "0.2.118"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
|
checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"wasm-bindgen-macro-support",
|
"wasm-bindgen-macro-support",
|
||||||
@@ -3377,9 +3377,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro-support"
|
name = "wasm-bindgen-macro-support"
|
||||||
version = "0.2.114"
|
version = "0.2.118"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
|
checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -3390,9 +3390,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-shared"
|
name = "wasm-bindgen-shared"
|
||||||
version = "0.2.114"
|
version = "0.2.118"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
|
checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
@@ -3412,9 +3412,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web-sys"
|
name = "web-sys"
|
||||||
version = "0.3.91"
|
version = "0.3.95"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9"
|
checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ edition = "2024"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
dioxus = { version = "0.7.5", features = ["router"] }
|
dioxus = { version = "0.7.5", features = ["router"] }
|
||||||
serde = "1.0.228"
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
serde_repr = "0.1.20"
|
serde_repr = "0.1.20"
|
||||||
tonic = { version = "*", default-features = false, features = [
|
tonic = { version = "*", default-features = false, features = [
|
||||||
"codegen",
|
"codegen",
|
||||||
@@ -15,8 +15,10 @@ tonic = { version = "*", default-features = false, features = [
|
|||||||
] }
|
] }
|
||||||
prost = "0.13"
|
prost = "0.13"
|
||||||
tonic-web-wasm-client = "0.7"
|
tonic-web-wasm-client = "0.7"
|
||||||
web-sys = { version = "0.3.91", features = ["Storage", "Window", "Navigator", "CredentialsContainer", "CredentialCreationOptions"] }
|
web-sys = { version = "0.3.91", features = ["Storage", "Window", "Navigator", "CredentialsContainer", "CredentialCreationOptions", "AttestationConveyancePreference", "PublicKeyCredentialCreationOptions", "PublicKeyCredentialRpEntity", "PublicKeyCredentialUserEntity"] }
|
||||||
dioxus-primitives = { git = "https://github.com/DioxusLabs/components", version = "0.0.1", default-features = false }
|
dioxus-primitives = { git = "https://github.com/DioxusLabs/components", version = "0.0.1", default-features = false }
|
||||||
|
serde-wasm-bindgen = "0.6.5"
|
||||||
|
js-sys = "0.3.95"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tonic-build = { version = "*", default-features = false, features = ["prost"] }
|
tonic-build = { version = "*", default-features = false, features = ["prost"] }
|
||||||
|
|||||||
+9
-4
@@ -3,13 +3,15 @@ use std::env;
|
|||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>>
|
||||||
|
{
|
||||||
tonic_build::configure()
|
tonic_build::configure()
|
||||||
.build_server(false)
|
.build_server(false)
|
||||||
.build_client(true)
|
.build_client(true)
|
||||||
.compile_protos(
|
.compile_protos(
|
||||||
&[
|
&[
|
||||||
"../AobaServer/Proto/Aoba.proto",
|
"../AobaServer/Proto/Aoba.proto",
|
||||||
|
"../AobaServer/Proto/Account.proto",
|
||||||
"../AobaServer/Proto/Auth.proto",
|
"../AobaServer/Proto/Auth.proto",
|
||||||
"../AobaServer/Proto/Metrics.proto",
|
"../AobaServer/Proto/Metrics.proto",
|
||||||
"../AobaServer/Proto/Types.proto",
|
"../AobaServer/Proto/Types.proto",
|
||||||
@@ -20,15 +22,18 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn forward_env() {
|
fn forward_env()
|
||||||
|
{
|
||||||
let dest_path = "./src/env.rs";
|
let dest_path = "./src/env.rs";
|
||||||
let mut f = File::create(&dest_path).unwrap();
|
let mut f = File::create(&dest_path).unwrap();
|
||||||
f.write_all(b"// This file is automatically generated by build.rs\n\n")
|
f.write_all(b"// This file is automatically generated by build.rs\n\n")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
dotenv().ok();
|
dotenv().ok();
|
||||||
for (key, value) in env::vars() {
|
for (key, value) in env::vars()
|
||||||
if key.starts_with("APP_") {
|
{
|
||||||
|
if key.starts_with("APP_")
|
||||||
|
{
|
||||||
f.write_all("#[allow(dead_code)]\n".as_bytes()).unwrap();
|
f.write_all("#[allow(dead_code)]\n".as_bytes()).unwrap();
|
||||||
let line = format!(
|
let line = format!(
|
||||||
"pub const {}: &'static str = \"{}\";\n",
|
"pub const {}: &'static str = \"{}\";\n",
|
||||||
|
|||||||
@@ -1,64 +1,63 @@
|
|||||||
|
use super::props::*;
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use dioxus_primitives::context_menu::{
|
|
||||||
self, ContextMenuContentProps, ContextMenuItemProps, ContextMenuProps, ContextMenuTriggerProps,
|
const CONTEXT_MENU_CSS: Asset = asset!("./style.scss");
|
||||||
};
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ContextMenu(props: ContextMenuProps) -> Element {
|
pub fn ContextMenu(props: ContextMenuProps) -> Element
|
||||||
rsx! {
|
{
|
||||||
document::Link { rel: "stylesheet", href: asset!("./style.css") }
|
rsx! {
|
||||||
context_menu::ContextMenu {
|
document::Link { rel: "stylesheet", href: CONTEXT_MENU_CSS }
|
||||||
disabled: props.disabled,
|
{props.children}
|
||||||
open: props.open,
|
}
|
||||||
default_open: props.default_open,
|
|
||||||
on_open_change: props.on_open_change,
|
|
||||||
roving_loop: props.roving_loop,
|
|
||||||
attributes: props.attributes,
|
|
||||||
{props.children}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ContextMenuTrigger(props: ContextMenuTriggerProps) -> Element {
|
pub fn ContextMenuTrigger(props: ContextMenuTriggerProps) -> Element
|
||||||
rsx! {
|
{
|
||||||
context_menu::ContextMenuTrigger {
|
rsx! {
|
||||||
padding: "20px",
|
div{
|
||||||
background: "var(--primary-color)",
|
class: "contextMenuTrigger",
|
||||||
border: "1px dashed var(--primary-color-6)",
|
oncontextmenu: move|e|{
|
||||||
border_radius: ".5rem",
|
|
||||||
cursor: "context-menu",
|
},
|
||||||
user_select: "none",
|
{props.children}
|
||||||
text_align: "center",
|
}
|
||||||
attributes: props.attributes,
|
}
|
||||||
{props.children}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ContextMenuContent(props: ContextMenuContentProps) -> Element {
|
pub fn ContextMenuContent(props: ContextMenuContentProps) -> Element
|
||||||
rsx! {
|
{
|
||||||
context_menu::ContextMenuContent {
|
rsx! {
|
||||||
class: "context-menu-content",
|
div{
|
||||||
id: props.id,
|
class: "contextMenuContent",
|
||||||
attributes: props.attributes,
|
{props.children}
|
||||||
{props.children}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ContextMenuItem(props: ContextMenuItemProps) -> Element {
|
pub fn ContextMenuItem(props: ContextMenuItemProps) -> Element
|
||||||
rsx! {
|
{
|
||||||
context_menu::ContextMenuItem {
|
rsx! {
|
||||||
class: "context-menu-item",
|
div {
|
||||||
disabled: props.disabled,
|
class: "contextMenuItem",
|
||||||
value: props.value,
|
onclick: move |_|{
|
||||||
index: props.index,
|
props.on_select.call(props.value.clone());
|
||||||
on_select: props.on_select,
|
},
|
||||||
attributes: props.attributes,
|
div {
|
||||||
{props.children}
|
class: "content",
|
||||||
}
|
{props.children}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn ContextMenuNestedContent(props: ContextMenuNestedProps) -> Element
|
||||||
|
{
|
||||||
|
rsx! {
|
||||||
|
{props.children}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
mod component;
|
mod component;
|
||||||
pub use component::*;
|
mod props;
|
||||||
|
pub use component::*;
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Props, Clone, PartialEq)]
|
||||||
|
pub struct ContextMenuProps
|
||||||
|
{
|
||||||
|
pub children: Element,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Props, Clone, PartialEq)]
|
||||||
|
pub struct ContextMenuItemProps
|
||||||
|
{
|
||||||
|
pub value: String,
|
||||||
|
pub on_select: EventHandler<String>,
|
||||||
|
pub children: Element,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Props, Clone, PartialEq)]
|
||||||
|
pub struct ContextMenuTriggerProps
|
||||||
|
{
|
||||||
|
pub children: Element,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Props, Clone, PartialEq)]
|
||||||
|
pub struct ContextMenuContentProps
|
||||||
|
{
|
||||||
|
pub children: Element,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Props, Clone, PartialEq)]
|
||||||
|
pub struct ContextMenuNestedProps
|
||||||
|
{
|
||||||
|
pub children: Element,
|
||||||
|
}
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
.context-menu-content {
|
|
||||||
z-index: 1000;
|
|
||||||
min-width: 220px;
|
|
||||||
padding: 0.25rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
background: var(--dark, var(--primary-color-5))
|
|
||||||
var(--light, var(--primary-color));
|
|
||||||
box-shadow: inset 0 0 0 1px var(--dark, var(--primary-color-7))
|
|
||||||
var(--light, var(--primary-color-6));
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
will-change: transform, opacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
.context-menu-content[data-state="closed"] {
|
|
||||||
animation: context-menu-animate-out 150ms ease-in forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes context-menu-animate-out {
|
|
||||||
0% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1) translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.95) translateY(-2px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.context-menu-content[data-state="open"] {
|
|
||||||
animation: context-menu-animate-in 150ms ease-out forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes context-menu-animate-in {
|
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.95) translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1) translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.context-menu-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border-radius: calc(0.5rem - 0.25rem);
|
|
||||||
color: var(--secondary-color-4);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
outline: none;
|
|
||||||
transition: background-color 100ms ease-out;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.context-menu-item[data-disabled="true"] {
|
|
||||||
color: var(--secondary-color-5);
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.context-menu-item:hover:not([data-disabled="true"]),
|
|
||||||
.context-menu-item:focus-visible {
|
|
||||||
background: var(--light, var(--primary-color-4))
|
|
||||||
var(--dark, var(--primary-color-7));
|
|
||||||
color: var(--light, var(--secondary-color-1))
|
|
||||||
var(--dark, var(--secondary-color-4));
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
components::{MediaItem, MediaItemPlaceHolder},
|
components::{MediaClassChangeEvent, MediaItem, MediaItemPlaceHolder},
|
||||||
rpc::{
|
rpc::{
|
||||||
aoba::{MediaModel, PageFilter},
|
aoba::{MediaModel, PageFilter},
|
||||||
get_rpc_client,
|
get_rpc_client,
|
||||||
@@ -9,7 +9,8 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
#[derive(PartialEq, Clone, Props)]
|
#[derive(PartialEq, Clone, Props)]
|
||||||
pub struct MediaGridProps {
|
pub struct MediaGridProps
|
||||||
|
{
|
||||||
pub query: Signal<String>,
|
pub query: Signal<String>,
|
||||||
pub max_page: Signal<i32>,
|
pub max_page: Signal<i32>,
|
||||||
pub total_items: Signal<i32>,
|
pub total_items: Signal<i32>,
|
||||||
@@ -18,19 +19,21 @@ pub struct MediaGridProps {
|
|||||||
pub on_page_loaded: Option<EventHandler<PaginationInfo>>,
|
pub on_page_loaded: Option<EventHandler<PaginationInfo>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PaginationInfo {
|
pub struct PaginationInfo
|
||||||
|
{
|
||||||
pub total_pages: i32,
|
pub total_pages: i32,
|
||||||
pub total_items: i32,
|
pub total_items: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn MediaGrid(props: MediaGridProps) -> Element {
|
pub fn MediaGrid(props: MediaGridProps) -> Element
|
||||||
|
{
|
||||||
let mut error_display = use_signal(|| {
|
let mut error_display = use_signal(|| {
|
||||||
rsx! {}
|
rsx! {}
|
||||||
});
|
});
|
||||||
let mut items = use_signal::<Option<Vec<MediaModel>>>(|| None);
|
let mut items = use_signal::<Option<Vec<MediaModel>>>(|| None);
|
||||||
let media_result = use_resource(use_reactive!(|(props)| async move {
|
let media_result = use_resource(use_reactive!(|(props)| async move {
|
||||||
items.set(None);
|
// items.set(None);
|
||||||
let mut client = get_rpc_client();
|
let mut client = get_rpc_client();
|
||||||
let request = PageFilter {
|
let request = PageFilter {
|
||||||
page_size: Some(props.page_size.cloned()),
|
page_size: Some(props.page_size.cloned()),
|
||||||
@@ -38,24 +41,32 @@ pub fn MediaGrid(props: MediaGridProps) -> Element {
|
|||||||
query: Some(props.query.cloned()),
|
query: Some(props.query.cloned()),
|
||||||
};
|
};
|
||||||
let result = client.list_media(request).await;
|
let result = client.list_media(request).await;
|
||||||
if let Ok(items) = result {
|
if let Ok(items) = result
|
||||||
|
{
|
||||||
let res = items.into_inner();
|
let res = items.into_inner();
|
||||||
|
|
||||||
return Ok(res);
|
return Ok(res);
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
let err = result.err().unwrap();
|
let err = result.err().unwrap();
|
||||||
let message = err.message();
|
let message = err.message();
|
||||||
return Err(format!("Failed to load results: {message}"));
|
return Err(format!("Failed to load results: {message}"));
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
use_effect(move || match media_result() {
|
use_effect(move || match media_result()
|
||||||
Some(value) => match value {
|
{
|
||||||
Ok(result) => {
|
Some(value) => match value
|
||||||
if let Some(pagination) = result.pagination {
|
{
|
||||||
|
Ok(result) =>
|
||||||
|
{
|
||||||
|
if let Some(pagination) = result.pagination
|
||||||
|
{
|
||||||
let total_pages = pagination.total_pages;
|
let total_pages = pagination.total_pages;
|
||||||
let total_items = pagination.total_items;
|
let total_items = pagination.total_items;
|
||||||
if let Some(handler) = props.on_page_loaded {
|
if let Some(handler) = props.on_page_loaded
|
||||||
|
{
|
||||||
handler.call(PaginationInfo {
|
handler.call(PaginationInfo {
|
||||||
total_pages,
|
total_pages,
|
||||||
total_items,
|
total_items,
|
||||||
@@ -71,7 +82,8 @@ pub fn MediaGrid(props: MediaGridProps) -> Element {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
_ => {}
|
_ =>
|
||||||
|
{}
|
||||||
});
|
});
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
@@ -79,7 +91,35 @@ pub fn MediaGrid(props: MediaGridProps) -> Element {
|
|||||||
class: "mediaGrid",
|
class: "mediaGrid",
|
||||||
{error_display}
|
{error_display}
|
||||||
{match items(){
|
{match items(){
|
||||||
Some(itms) => rsx!{MediaList { items: itms }},
|
Some(itms) => rsx!{
|
||||||
|
MediaList {
|
||||||
|
items: itms,
|
||||||
|
on_item_deleted: move |id|{
|
||||||
|
if let Some(cur) = items.cloned(){
|
||||||
|
let filtered = cur.iter()
|
||||||
|
.filter(|i| i.id.clone().expect("No id").value != id)
|
||||||
|
.map(|i|i.clone())
|
||||||
|
.collect();
|
||||||
|
items.set(Some(filtered));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
on_class_changed: move |e: MediaClassChangeEvent|{
|
||||||
|
if let Some(cur) = items.cloned(){
|
||||||
|
let updated = cur.iter()
|
||||||
|
.map(|i|{
|
||||||
|
let mut itm = i.clone();
|
||||||
|
let id = itm.id.clone().expect("No id").value;
|
||||||
|
if id == e.id{
|
||||||
|
itm.class = e.class;
|
||||||
|
}
|
||||||
|
return itm;
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
items.set(Some(updated));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
None => rsx!{PlaceholderGrid { count: props.page_size.cloned() as usize }}
|
None => rsx!{PlaceholderGrid { count: props.page_size.cloned() as usize }}
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
@@ -87,7 +127,8 @@ pub fn MediaGrid(props: MediaGridProps) -> Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn PlaceholderGrid(count: usize) -> Element {
|
fn PlaceholderGrid(count: usize) -> Element
|
||||||
|
{
|
||||||
rsx! {
|
rsx! {
|
||||||
div{
|
div{
|
||||||
class: "mediaGrid",
|
class: "mediaGrid",
|
||||||
@@ -99,12 +140,18 @@ fn PlaceholderGrid(count: usize) -> Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn MediaList(items: Vec<MediaModel>) -> Element {
|
fn MediaList(
|
||||||
|
items: Vec<MediaModel>,
|
||||||
|
on_item_deleted: Option<EventHandler<String>>,
|
||||||
|
on_class_changed: Option<EventHandler<MediaClassChangeEvent>>,
|
||||||
|
) -> Element
|
||||||
|
{
|
||||||
rsx! {
|
rsx! {
|
||||||
{items.iter().enumerate().map(|(index, itm)| rsx!{
|
{items.iter().map(|itm| rsx!{
|
||||||
MediaItem {
|
MediaItem {
|
||||||
item: itm.clone(),
|
item: itm.clone(),
|
||||||
index
|
on_deleted: on_item_deleted,
|
||||||
|
on_class_changed: on_class_changed
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use dioxus_primitives::context_menu::{
|
use dioxus_primitives::context_menu::{ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger};
|
||||||
ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger,
|
|
||||||
};
|
|
||||||
use tonic::{Response, Status};
|
use tonic::{Response, Status};
|
||||||
use web_sys::window;
|
use web_sys::window;
|
||||||
|
|
||||||
@@ -13,47 +11,43 @@ use crate::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct MediaClassChangeEvent {
|
pub struct MediaClassChangeEvent
|
||||||
pub index: usize,
|
{
|
||||||
pub class: String,
|
pub id: String,
|
||||||
|
pub class: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Clone, Props)]
|
#[derive(PartialEq, Clone, Props)]
|
||||||
pub struct MediaItemProps {
|
pub struct MediaItemProps
|
||||||
|
{
|
||||||
pub item: MediaModel,
|
pub item: MediaModel,
|
||||||
pub index: usize,
|
|
||||||
pub on_class_changed: Option<EventHandler<MediaClassChangeEvent>>,
|
pub on_class_changed: Option<EventHandler<MediaClassChangeEvent>>,
|
||||||
pub on_deleted: Option<EventHandler<usize>>,
|
pub on_deleted: Option<EventHandler<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn MediaItem(props: MediaItemProps) -> Element {
|
pub fn MediaItem(props: MediaItemProps) -> Element
|
||||||
|
{
|
||||||
let item = props.item;
|
let item = props.item;
|
||||||
let mtype = item.media_type().as_str_name();
|
let mtype = item.media_type().as_str_name();
|
||||||
let filename = item.file_name;
|
let filename = item.file_name;
|
||||||
let id = item.id.unwrap().value;
|
let id = item.id.unwrap().value;
|
||||||
let thumb = item.thumb_url;
|
let thumb = item.thumb_url;
|
||||||
let class = item.class;
|
let class = item.class;
|
||||||
let mut class_signal = use_signal(|| match class {
|
let class_string = match class
|
||||||
|
{
|
||||||
1 => "blur",
|
1 => "blur",
|
||||||
2 => "secret",
|
2 => "secret",
|
||||||
_ => "",
|
_ => "",
|
||||||
});
|
};
|
||||||
let url = item.media_url;
|
let url = item.media_url;
|
||||||
let download = format!("{HOST}{url}");
|
let download = format!("{HOST}{url}");
|
||||||
|
|
||||||
// class_signal.set(match class
|
|
||||||
// {
|
|
||||||
// 1 => "blur",
|
|
||||||
// 2 => "secret",
|
|
||||||
// _ => "",
|
|
||||||
// });
|
|
||||||
|
|
||||||
return rsx! {
|
return rsx! {
|
||||||
ContextMenu{
|
ContextMenu{
|
||||||
ContextMenuTrigger{
|
ContextMenuTrigger{
|
||||||
a {
|
a {
|
||||||
class: "mediaItem {class_signal()}",
|
class: "mediaItem {class_string}",
|
||||||
href: "{HOST}{url}",
|
href: "{HOST}{url}",
|
||||||
target: "_blank",
|
target: "_blank",
|
||||||
"data-id" : id.clone(),
|
"data-id" : id.clone(),
|
||||||
@@ -99,14 +93,16 @@ pub fn MediaItem(props: MediaItemProps) -> Element {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
if class_signal() != "" {
|
if class != 0 {
|
||||||
rsx!{ContextMenuItem {
|
rsx!{ContextMenuItem {
|
||||||
index: 2 as usize,
|
index: 2 as usize,
|
||||||
value: "{id}",
|
value: "{id}",
|
||||||
on_select: move |id: String|{
|
on_select: move |id: String|{
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
if let Ok(_) = set_class(id, 0).await{
|
if let Ok(_) = set_class(&id, 0).await{
|
||||||
class_signal.set("");
|
if let Some(handler) = props.on_class_changed{
|
||||||
|
handler.call(MediaClassChangeEvent { id, class: 0 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -121,14 +117,16 @@ pub fn MediaItem(props: MediaItemProps) -> Element {
|
|||||||
}else{rsx!{}}
|
}else{rsx!{}}
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
if class_signal() != "blur" {
|
if class != 1 {
|
||||||
rsx!{ContextMenuItem {
|
rsx!{ContextMenuItem {
|
||||||
index: 2 as usize,
|
index: 3 as usize,
|
||||||
value: "{id}",
|
value: "{id}",
|
||||||
on_select: move |id: String|{
|
on_select: move |id: String|{
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
if let Ok(_) = set_class(id, 1).await{
|
if let Ok(_) = set_class(&id, 1).await{
|
||||||
class_signal.set("blur");
|
if let Some(handler) = props.on_class_changed{
|
||||||
|
handler.call(MediaClassChangeEvent { id, class: 1 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -143,14 +141,16 @@ pub fn MediaItem(props: MediaItemProps) -> Element {
|
|||||||
}else{rsx!{}}
|
}else{rsx!{}}
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
if class_signal() != "secret" {
|
if class != 3 {
|
||||||
rsx!{ContextMenuItem {
|
rsx!{ContextMenuItem {
|
||||||
index: 2 as usize,
|
index: 4 as usize,
|
||||||
value: "{id}",
|
value: "{id}",
|
||||||
on_select: move |id: String|{
|
on_select: move |id: String|{
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
if let Ok(_) = set_class(id, 2).await{
|
if let Ok(_) = set_class(&id, 2).await{
|
||||||
class_signal.set("secret");
|
if let Some(handler) = props.on_class_changed{
|
||||||
|
handler.call(MediaClassChangeEvent { id, class: 2 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -165,8 +165,17 @@ pub fn MediaItem(props: MediaItemProps) -> Element {
|
|||||||
}else{rsx!{}}
|
}else{rsx!{}}
|
||||||
}
|
}
|
||||||
ContextMenuItem {
|
ContextMenuItem {
|
||||||
index: 2 as usize,
|
index: 5 as usize,
|
||||||
value: "",
|
value: "{id}",
|
||||||
|
on_select: move |id: String|{
|
||||||
|
spawn(async move {
|
||||||
|
if let Ok(_) = delete_media(id.clone()).await{
|
||||||
|
if let Some(handler) = props.on_deleted {
|
||||||
|
handler.call(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
div{
|
div{
|
||||||
class: "contextItem",
|
class: "contextItem",
|
||||||
div{
|
div{
|
||||||
@@ -181,7 +190,8 @@ pub fn MediaItem(props: MediaItemProps) -> Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn MediaItemPlaceHolder() -> Element {
|
pub fn MediaItemPlaceHolder() -> Element
|
||||||
|
{
|
||||||
return rsx! {
|
return rsx! {
|
||||||
div { class: "mediaItem placeholder",
|
div { class: "mediaItem placeholder",
|
||||||
img { },
|
img { },
|
||||||
@@ -196,12 +206,19 @@ pub fn MediaItemPlaceHolder() -> Element {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_class(id: String, class: i32) -> Result<Response<()>, Status> {
|
async fn delete_media(id: String) -> Result<Response<()>, Status>
|
||||||
|
{
|
||||||
|
let mut client = get_rpc_client();
|
||||||
|
return client.delete_media(Id { value: id }).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_class(id: &String, class: i32) -> Result<Response<()>, Status>
|
||||||
|
{
|
||||||
let mut client = get_rpc_client();
|
let mut client = get_rpc_client();
|
||||||
return client
|
return client
|
||||||
.set_media_class(SetMediaClassRequest {
|
.set_media_class(SetMediaClassRequest {
|
||||||
class: class,
|
class: class,
|
||||||
id: Some(Id { value: id }),
|
id: Some(Id { value: id.clone() }),
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
pub mod basic;
|
pub mod basic;
|
||||||
// mod context_menu;
|
mod context_menu;
|
||||||
mod icons;
|
mod icons;
|
||||||
mod media_grid;
|
mod media_grid;
|
||||||
mod media_item;
|
mod media_item;
|
||||||
@@ -9,7 +9,7 @@ mod notif;
|
|||||||
mod pagination;
|
mod pagination;
|
||||||
mod passkey;
|
mod passkey;
|
||||||
mod search;
|
mod search;
|
||||||
// pub use context_menu::*;
|
pub use context_menu::*;
|
||||||
pub use media_grid::*;
|
pub use media_grid::*;
|
||||||
pub use media_item::*;
|
pub use media_item::*;
|
||||||
pub use metrics_token::*;
|
pub use metrics_token::*;
|
||||||
@@ -18,4 +18,4 @@ pub use notif::*;
|
|||||||
pub use pagination::*;
|
pub use pagination::*;
|
||||||
pub use passkey::*;
|
pub use passkey::*;
|
||||||
pub use search::*;
|
pub use search::*;
|
||||||
pub mod radio_group;
|
pub mod radio_group;
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use web_sys::{CredentialCreationOptions, window};
|
use js_sys::{Uint8Array, wasm_bindgen::JsValue};
|
||||||
|
use web_sys::{
|
||||||
|
CredentialCreationOptions, PublicKeyCredentialCreationOptions, PublicKeyCredentialRpEntity,
|
||||||
|
PublicKeyCredentialUserEntity, window,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::components::basic::Button;
|
use crate::{components::basic::Button, rpc::aoba::PasskeyCredentialCreateOptions};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn PasskeyRegistrationButton() -> Element {
|
pub fn PasskeyRegistrationButton() -> Element
|
||||||
|
{
|
||||||
rsx! {
|
rsx! {
|
||||||
Button{
|
Button{
|
||||||
text: "Register Passkey",
|
text: "Register Passkey",
|
||||||
@@ -15,22 +20,55 @@ pub fn PasskeyRegistrationButton() -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_passkey_registration() {
|
fn start_passkey_registration()
|
||||||
create_credential();
|
{
|
||||||
|
create_credential(todo!());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_credential() {
|
fn create_credential(req_opts: PasskeyCredentialCreateOptions)
|
||||||
let credentials = window()
|
{
|
||||||
.expect("Failed to get window")
|
let window = window().expect("Window does not exist");
|
||||||
.navigator()
|
let credentaials = window.navigator().credentials();
|
||||||
.credentials();
|
|
||||||
|
|
||||||
|
let opts = opts_from_rpc(req_opts);
|
||||||
|
|
||||||
|
let result = credentaials.create_with_options(&opts);
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn opts_from_rpc(rpc_opts: PasskeyCredentialCreateOptions) -> CredentialCreationOptions
|
||||||
|
{
|
||||||
|
let opt_user = &rpc_opts.user.expect("user is missing");
|
||||||
|
let opt_rp = &rpc_opts.rp.expect("rp is missing");
|
||||||
let opts = CredentialCreationOptions::new();
|
let opts = CredentialCreationOptions::new();
|
||||||
let _result = credentials.create_with_options(&opts);
|
let rp = PublicKeyCredentialRpEntity::new(&opt_rp.name);
|
||||||
|
rp.set_id(&opt_rp.id);
|
||||||
|
|
||||||
|
let user = PublicKeyCredentialUserEntity::new_with_u8_array(
|
||||||
|
&opt_user.name,
|
||||||
|
&opt_user.display_name,
|
||||||
|
&to_u8_array(&opt_user.id),
|
||||||
|
);
|
||||||
|
let pub_key_opts = PublicKeyCredentialCreationOptions::new_with_u8_array(
|
||||||
|
&to_u8_array(&rpc_opts.challenge),
|
||||||
|
&JsValue::undefined(),
|
||||||
|
&rp,
|
||||||
|
&user,
|
||||||
|
);
|
||||||
|
//pub_key_opts.set_exclude_credentials(val);
|
||||||
|
opts.set_public_key(&pub_key_opts);
|
||||||
|
|
||||||
|
return opts;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_u8_array(value: &String) -> Uint8Array
|
||||||
|
{
|
||||||
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn PasskeyLoginButton() -> Element {
|
pub fn PasskeyLoginButton() -> Element
|
||||||
|
{
|
||||||
rsx! {
|
rsx! {
|
||||||
Button{
|
Button{
|
||||||
text: "Login with Passkey"
|
text: "Login with Passkey"
|
||||||
|
|||||||
+18
-1
@@ -6,7 +6,9 @@ use tonic_web_wasm_client::Client;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
RPC_HOST,
|
RPC_HOST,
|
||||||
rpc::aoba::{auth_rpc_client::AuthRpcClient, metrics_rpc_client::MetricsRpcClient},
|
rpc::aoba::{
|
||||||
|
account_rpc_client::AccountRpcClient, auth_rpc_client::AuthRpcClient, metrics_rpc_client::MetricsRpcClient,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod aoba
|
pub mod aoba
|
||||||
@@ -17,6 +19,7 @@ pub mod aoba
|
|||||||
static RPC_CLIENT: RpcConnection = RpcConnection {
|
static RPC_CLIENT: RpcConnection = RpcConnection {
|
||||||
aoba: RwLock::new(None),
|
aoba: RwLock::new(None),
|
||||||
auth: RwLock::new(None),
|
auth: RwLock::new(None),
|
||||||
|
account: RwLock::new(None),
|
||||||
metrics: RwLock::new(None),
|
metrics: RwLock::new(None),
|
||||||
jwt: RwLock::new(None),
|
jwt: RwLock::new(None),
|
||||||
};
|
};
|
||||||
@@ -26,6 +29,7 @@ pub struct RpcConnection
|
|||||||
{
|
{
|
||||||
aoba: RwLock<Option<AobaRpcClient<InterceptedService<Client, AuthInterceptor>>>>,
|
aoba: RwLock<Option<AobaRpcClient<InterceptedService<Client, AuthInterceptor>>>>,
|
||||||
auth: RwLock<Option<AuthRpcClient<Client>>>,
|
auth: RwLock<Option<AuthRpcClient<Client>>>,
|
||||||
|
account: RwLock<Option<AccountRpcClient<InterceptedService<Client, AuthInterceptor>>>>,
|
||||||
metrics: RwLock<Option<MetricsRpcClient<InterceptedService<Client, AuthInterceptor>>>>,
|
metrics: RwLock<Option<MetricsRpcClient<InterceptedService<Client, AuthInterceptor>>>>,
|
||||||
jwt: RwLock<Option<String>>,
|
jwt: RwLock<Option<String>>,
|
||||||
}
|
}
|
||||||
@@ -38,6 +42,12 @@ impl RpcConnection
|
|||||||
return self.aoba.read().unwrap().clone().unwrap();
|
return self.aoba.read().unwrap().clone().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_account_client(&self) -> AccountRpcClient<InterceptedService<Client, AuthInterceptor>>
|
||||||
|
{
|
||||||
|
self.ensure_client();
|
||||||
|
return self.account.read().unwrap().clone().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_auth_client(&self) -> AuthRpcClient<Client>
|
pub fn get_auth_client(&self) -> AuthRpcClient<Client>
|
||||||
{
|
{
|
||||||
self.ensure_client();
|
self.ensure_client();
|
||||||
@@ -58,6 +68,8 @@ impl RpcConnection
|
|||||||
let aoba_client = AobaRpcClient::with_interceptor(wasm_client.clone(), AuthInterceptor);
|
let aoba_client = AobaRpcClient::with_interceptor(wasm_client.clone(), AuthInterceptor);
|
||||||
*self.aoba.write().unwrap() = Some(aoba_client);
|
*self.aoba.write().unwrap() = Some(aoba_client);
|
||||||
*self.auth.write().unwrap() = Some(AuthRpcClient::new(wasm_client.clone()));
|
*self.auth.write().unwrap() = Some(AuthRpcClient::new(wasm_client.clone()));
|
||||||
|
*self.account.write().unwrap() =
|
||||||
|
Some(AccountRpcClient::with_interceptor(wasm_client.clone(), AuthInterceptor));
|
||||||
*self.metrics.write().unwrap() =
|
*self.metrics.write().unwrap() =
|
||||||
Some(MetricsRpcClient::with_interceptor(wasm_client.clone(), AuthInterceptor));
|
Some(MetricsRpcClient::with_interceptor(wasm_client.clone(), AuthInterceptor));
|
||||||
}
|
}
|
||||||
@@ -90,6 +102,11 @@ pub fn get_auth_rpc_client() -> AuthRpcClient<Client>
|
|||||||
return RPC_CLIENT.get_auth_client();
|
return RPC_CLIENT.get_auth_client();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_account_rpc_client() -> AccountRpcClient<InterceptedService<Client, AuthInterceptor>>
|
||||||
|
{
|
||||||
|
return RPC_CLIENT.get_account_client();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_metrics_rpc_client() -> MetricsRpcClient<InterceptedService<Client, AuthInterceptor>>
|
pub fn get_metrics_rpc_client() -> MetricsRpcClient<InterceptedService<Client, AuthInterceptor>>
|
||||||
{
|
{
|
||||||
return RPC_CLIENT.get_metrics_client();
|
return RPC_CLIENT.get_metrics_client();
|
||||||
|
|||||||
@@ -8,15 +8,16 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="FFMpegCore" Version="5.4.0" />
|
<PackageReference Include="FFMpegCore" Version="5.4.0" />
|
||||||
|
<PackageReference Include="Fido2.Models" Version="4.0.1" />
|
||||||
<PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0" />
|
<PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.3" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.5" />
|
||||||
<PackageReference Include="MaybeError" Version="1.2.0" />
|
<PackageReference Include="MaybeError" Version="1.2.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.3" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.5" />
|
||||||
<PackageReference Include="MongoDB.Driver" Version="3.6.0" />
|
<PackageReference Include="MongoDB.Driver" Version="3.7.1" />
|
||||||
<PackageReference Include="MongoDB.Driver.Core.Extensions.DiagnosticSources" Version="3.0.0" />
|
<PackageReference Include="MongoDB.Driver.Core.Extensions.DiagnosticSources" Version="3.0.0" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.7" />
|
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.7" />
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.16.0" />
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.17.0" />
|
||||||
<PackageReference Include="ZLinq" Version="1.5.5" />
|
<PackageReference Include="ZLinq" Version="1.5.6" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using Microsoft.IdentityModel.Tokens;
|
using Fido2NetLib.Objects;
|
||||||
|
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
using MongoDB.Bson;
|
using MongoDB.Bson;
|
||||||
using MongoDB.Bson.Serialization.Attributes;
|
using MongoDB.Bson.Serialization.Attributes;
|
||||||
@@ -19,6 +21,7 @@ public class User
|
|||||||
public bool IsArgon { get; set; }
|
public bool IsArgon { get; set; }
|
||||||
public ObjectId[] ApiKeys { get; set; } = [];
|
public ObjectId[] ApiKeys { get; set; } = [];
|
||||||
public List<ObjectId> RegTokens { get; set; } = [];
|
public List<ObjectId> RegTokens { get; set; } = [];
|
||||||
|
public List<PublicKeyCredentialDescriptor> CredentialDescriptors { get; set; } = [];
|
||||||
|
|
||||||
public ClaimsIdentity GetIdentity()
|
public ClaimsIdentity GetIdentity()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using AobaCore.Models;
|
using AobaCore.Models;
|
||||||
|
|
||||||
|
using Fido2NetLib.Objects;
|
||||||
|
|
||||||
using Isopoh.Cryptography.Argon2;
|
using Isopoh.Cryptography.Argon2;
|
||||||
|
|
||||||
using MongoDB.Bson;
|
using MongoDB.Bson;
|
||||||
@@ -54,13 +56,18 @@ public class AccountsService(IMongoDatabase db)
|
|||||||
/* Get the salt */
|
/* Get the salt */
|
||||||
byte[] salt = new byte[16];
|
byte[] salt = new byte[16];
|
||||||
Array.Copy(hashBytes, 0, salt, 0, 16);
|
Array.Copy(hashBytes, 0, salt, 0, 16);
|
||||||
/* Compute the hash on the password the user entered */
|
|
||||||
var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 10000, HashAlgorithmName.SHA1);
|
var hash= Rfc2898DeriveBytes.Pbkdf2(password, salt, 10000, HashAlgorithmName.SHA1, 20);
|
||||||
byte[] hash = pbkdf2.GetBytes(20);
|
|
||||||
/* Compare the results */
|
/* Compare the results */
|
||||||
for (int i = 0; i < 20; i++)
|
for (int i = 0; i < 20; i++)
|
||||||
if (hashBytes[i + 16] != hash[i])
|
if (hashBytes[i + 16] != hash[i])
|
||||||
return false;
|
return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<List<PublicKeyCredentialDescriptor>> GetPublicKeyCredentialDescriptorsAsync(ObjectId id, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var creds = await _users.Find(u => u.Id == id).Project(u => u.CredentialDescriptors).FirstOrDefaultAsync(cancellationToken);
|
||||||
|
return creds ?? [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,6 +133,23 @@ public class AobaService(IMongoDatabase db)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task DeleteFilesAsync(IEnumerable<ObjectId> mediaIds, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
foreach (var id in mediaIds)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
await _gridFs.DeleteAsync(id, CancellationToken.None);
|
||||||
|
await _media.DeleteOneAsync(m => m.MediaId == id, CancellationToken.None);
|
||||||
|
}
|
||||||
|
catch (GridFSFileNotFoundException)
|
||||||
|
{
|
||||||
|
//ignore if file was not found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public async Task DeriveTagsAsync(CancellationToken cancellationToken = default)
|
public async Task DeriveTagsAsync(CancellationToken cancellationToken = default)
|
||||||
|
|||||||
@@ -9,22 +9,22 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Fido2" Version="4.0.0" />
|
<PackageReference Include="Fido2.AspNet" Version="4.0.1" />
|
||||||
<PackageReference Include="Grpc.AspNetCore" Version="2.76.0" />
|
<PackageReference Include="Grpc.AspNetCore" Version="2.76.0" />
|
||||||
<PackageReference Include="Grpc.AspNetCore.Web" Version="2.76.0" />
|
<PackageReference Include="Grpc.AspNetCore.Web" Version="2.76.0" />
|
||||||
<PackageReference Include="Grpc.Tools" Version="2.78.0">
|
<PackageReference Include="Grpc.Tools" Version="2.80.0">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0" />
|
<PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.3" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.5" />
|
||||||
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.16.0" />
|
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.17.0" />
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.23.0" />
|
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.23.0" />
|
||||||
<PackageReference Include="MimeTypesMap" Version="1.0.9" />
|
<PackageReference Include="MimeTypesMap" Version="1.0.9" />
|
||||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.0" />
|
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.2" />
|
||||||
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.9.0-beta.2" />
|
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.9.0-beta.2" />
|
||||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.15.0" />
|
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.15.2" />
|
||||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.0" />
|
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.1" />
|
||||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.15.0" />
|
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.15.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -121,6 +121,16 @@ builder.Services.AddAuthentication(options =>
|
|||||||
|
|
||||||
|
|
||||||
builder.Services.AddAoba();
|
builder.Services.AddAoba();
|
||||||
|
builder.Services.AddFido2(opts =>
|
||||||
|
{
|
||||||
|
opts.ServerName = "Aoba";
|
||||||
|
opts.ServerDomain = "aoba.app";
|
||||||
|
#if DEBUG
|
||||||
|
opts.Origins = new HashSet<string> { "http://localhost:8081", "http://127.0.0.1:8080" };
|
||||||
|
#else
|
||||||
|
opts.Origins = new HashSet<string> { "https://aoba.app" };
|
||||||
|
#endif
|
||||||
|
});
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
builder.Services.AddHostedService<DebugService>();
|
builder.Services.AddHostedService<DebugService>();
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import "google/protobuf/empty.proto";
|
|||||||
import "Proto/Types.proto";
|
import "Proto/Types.proto";
|
||||||
|
|
||||||
service AccountRpc {
|
service AccountRpc {
|
||||||
rpc RegisterPasskey(google.protobuf.Empty) returns (PasskeyRegistrationCreds);
|
rpc RegisterPasskey(google.protobuf.Empty) returns (PasskeyCredentialCreateOptions);
|
||||||
rpc CompletePasskeyRegistration(PasskeyPublicKey) returns (google.protobuf.Empty);
|
rpc CompletePasskeyRegistration(PasskeyRegistrationCredentials) returns (google.protobuf.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import "Proto/Types.proto";
|
|||||||
service AobaRpc {
|
service AobaRpc {
|
||||||
rpc GetMedia (Id) returns (MediaResponse);
|
rpc GetMedia (Id) returns (MediaResponse);
|
||||||
rpc DeleteMedia (Id) returns (google.protobuf.Empty);
|
rpc DeleteMedia (Id) returns (google.protobuf.Empty);
|
||||||
|
rpc DeleteMediaBulk (IdList) returns (google.protobuf.Empty);
|
||||||
rpc UpdateMedia (google.protobuf.Empty) returns (google.protobuf.Empty);
|
rpc UpdateMedia (google.protobuf.Empty) returns (google.protobuf.Empty);
|
||||||
rpc ListMedia(PageFilter) returns (ListResponse);
|
rpc ListMedia(PageFilter) returns (ListResponse);
|
||||||
rpc GetUser(Id) returns (UserResponse);
|
rpc GetUser(Id) returns (UserResponse);
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ message Id {
|
|||||||
string value = 1;
|
string value = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message IdList {
|
||||||
|
repeated Id value = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message MediaResponse {
|
message MediaResponse {
|
||||||
optional MediaModel value = 1;
|
optional MediaModel value = 1;
|
||||||
}
|
}
|
||||||
@@ -121,9 +125,46 @@ message PasskeyPayload {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message PasskeyRegistrationCreds{
|
message PasskeyCredentialCreateOptions{
|
||||||
|
string challenge = 1;
|
||||||
|
PublicKeyCredentialUser user = 2;
|
||||||
|
PublicKeyCredentialRpEntity rp = 3;
|
||||||
|
repeated PubKeyCredParam pubKeyParams = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PubKeyCredParam{
|
||||||
|
string alg = 1;
|
||||||
|
string type = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PublicKeyCredentialRpEntity{
|
||||||
|
string id = 1;
|
||||||
|
string icon = 2;
|
||||||
|
string name = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PublicKeyCredentialUser{
|
||||||
|
string id = 1;
|
||||||
|
string name = 2;
|
||||||
|
string displayName = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PasskeyRegistrationCredentials{
|
||||||
|
string id = 1;
|
||||||
|
string rawId = 2;
|
||||||
|
string type = 3;
|
||||||
|
CredentialsClientResponse response = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CredentialsClientResponse{
|
||||||
|
string clientDataJSON = 1;
|
||||||
|
string attestationObject = 2;
|
||||||
|
string authenticatorData = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PublicKeyCredentialDescriptor{
|
||||||
|
string type = 1;
|
||||||
|
string id = 2;
|
||||||
|
repeated string transports = 3;
|
||||||
}
|
}
|
||||||
message PasskeyPublicKey{
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,50 @@
|
|||||||
using Aoba.RPC;
|
using Aoba.RPC;
|
||||||
using Aoba.RPC.Account;
|
using Aoba.RPC.Account;
|
||||||
|
|
||||||
|
using AobaCore.Services;
|
||||||
|
|
||||||
|
using AobaServer.Utils;
|
||||||
|
|
||||||
|
using Fido2NetLib;
|
||||||
|
|
||||||
using Google.Protobuf.WellKnownTypes;
|
using Google.Protobuf.WellKnownTypes;
|
||||||
|
|
||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
|
|
||||||
|
using Isopoh.Cryptography.Argon2;
|
||||||
|
|
||||||
namespace AobaServer.Services;
|
namespace AobaServer.Services;
|
||||||
|
|
||||||
public class AccountRpcService : AccountRpc.AccountRpcBase
|
public class AccountRpcService(IFido2 fido2, AccountsService accounts) : AccountRpc.AccountRpcBase
|
||||||
{
|
{
|
||||||
public override Task<PasskeyRegistrationCreds> RegisterPasskey(Empty request, ServerCallContext context)
|
public override async Task<PasskeyCredentialCreateOptions> RegisterPasskey(Empty request, ServerCallContext context)
|
||||||
{
|
{
|
||||||
return base.RegisterPasskey(request, context);
|
var curUser = await accounts.GetUserAsync(context.GetUserId(), context.CancellationToken);
|
||||||
|
if (curUser == null)
|
||||||
|
throw new Exception($"Logged in user does not exist somehow. Id: {context.GetUserId()}");
|
||||||
|
var user = new Fido2User
|
||||||
|
{
|
||||||
|
DisplayName = curUser.Username,
|
||||||
|
Id = curUser.Id.ToByteArray(),
|
||||||
|
Name = curUser.Username
|
||||||
|
};
|
||||||
|
|
||||||
|
var credOptions = fido2.RequestNewCredential(new RequestNewCredentialParams
|
||||||
|
{
|
||||||
|
User = user,
|
||||||
|
ExcludeCredentials = curUser.CredentialDescriptors,
|
||||||
|
AuthenticatorSelection = new AuthenticatorSelection
|
||||||
|
{
|
||||||
|
ResidentKey = Fido2NetLib.Objects.ResidentKeyRequirement.Required,
|
||||||
|
UserVerification = Fido2NetLib.Objects.UserVerificationRequirement.Preferred
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
return credOptions.ToRPC();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task<Empty> CompletePasskeyRegistration(PasskeyPublicKey request, ServerCallContext context)
|
public override Task<Empty> CompletePasskeyRegistration(PasskeyRegistrationCredentials request, ServerCallContext context)
|
||||||
{
|
{
|
||||||
return base.CompletePasskeyRegistration(request, context);
|
return base.CompletePasskeyRegistration(request, context);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,4 +57,15 @@ public class AobaRpcService(AobaService aobaService, AccountsService accountsSer
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override async Task<Empty> DeleteMedia(Id request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
await aobaService.DeleteFileAsync(request.ToObjectId(), context.CancellationToken);
|
||||||
|
return new Empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<Empty> DeleteMediaBulk(IdList request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
await aobaService.DeleteFilesAsync(request.ToObjectId(), context.CancellationToken);
|
||||||
|
return new Empty();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
using Aoba.RPC;
|
||||||
|
|
||||||
|
using Isopoh.Cryptography.Argon2;
|
||||||
|
|
||||||
|
namespace AobaServer.Utils;
|
||||||
|
|
||||||
|
public static class PasskeyExtensions
|
||||||
|
{
|
||||||
|
public static PublicKeyCredentialRpEntity ToRPC(this Fido2NetLib.PublicKeyCredentialRpEntity value)
|
||||||
|
{
|
||||||
|
return new PublicKeyCredentialRpEntity
|
||||||
|
{
|
||||||
|
Id = value.Id,
|
||||||
|
Icon = value.Icon,
|
||||||
|
Name = value.Name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PublicKeyCredentialUser ToRPC(this Fido2NetLib.Fido2User value)
|
||||||
|
{
|
||||||
|
return new PublicKeyCredentialUser
|
||||||
|
{
|
||||||
|
Id = value.Id.ToB64String(),
|
||||||
|
DisplayName = value.DisplayName,
|
||||||
|
Name = value.Name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PubKeyCredParam ToRPC(this Fido2NetLib.PubKeyCredParam value)
|
||||||
|
{
|
||||||
|
return new PubKeyCredParam
|
||||||
|
{
|
||||||
|
Alg = value.Alg.ToString(),
|
||||||
|
Type = value.Type.ToString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<PubKeyCredParam> ToRPC(this IEnumerable<Fido2NetLib.PubKeyCredParam> value)
|
||||||
|
{
|
||||||
|
return value.Select(x => x.ToRPC());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PasskeyCredentialCreateOptions ToRPC(this Fido2NetLib.CredentialCreateOptions value)
|
||||||
|
{
|
||||||
|
var opts = new PasskeyCredentialCreateOptions
|
||||||
|
{
|
||||||
|
Challenge = value.Challenge.ToB64String(),
|
||||||
|
Rp = value.Rp.ToRPC(),
|
||||||
|
User = value.User.ToRPC()
|
||||||
|
};
|
||||||
|
//todo: excluded credentials
|
||||||
|
opts.PubKeyParams.AddRange(value.PubKeyCredParams.ToRPC());
|
||||||
|
return opts;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -70,4 +70,9 @@ public static class ProtoExtensions
|
|||||||
{
|
{
|
||||||
return id.Value.ToObjectId();
|
return id.Value.ToObjectId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<ObjectId> ToObjectId(this IdList id)
|
||||||
|
{
|
||||||
|
return id.Value.Select(v => v.ToObjectId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user