diff --git a/AobaClient/build.rs b/AobaClient/build.rs index fb2229e..e63af67 100644 --- a/AobaClient/build.rs +++ b/AobaClient/build.rs @@ -2,7 +2,10 @@ fn main() -> Result<(), Box> { tonic_build::configure() .build_server(false) .build_client(true) - .compile_protos(&["..\\AobaServer\\Proto\\Aoba.proto"], &["..\\AobaServer\\Proto\\"])?; + .compile_protos( + &["..\\AobaServer\\Proto\\Aoba.proto", "..\\AobaServer\\Proto\\Auth.proto"], + &["..\\AobaServer\\Proto\\"], + )?; Ok(()) } diff --git a/AobaClient/src/components/basic/button.rs b/AobaClient/src/components/basic/button.rs index 221ad62..d57fc2b 100644 --- a/AobaClient/src/components/basic/button.rs +++ b/AobaClient/src/components/basic/button.rs @@ -2,8 +2,9 @@ use dioxus::prelude::*; #[derive(PartialEq, Clone, Props)] pub struct ButtonProps { - variant: Option, - text: String, + pub variant: Option, + pub text: String, + pub onclick: Option>>, } #[derive(PartialEq, Clone)] @@ -16,6 +17,13 @@ pub enum ButtonVariant { #[component] pub fn Button(props: ButtonProps) -> Element { rsx! { - button { "{props.text}" } + button { + onclick: move |event| { + if let Some(h) = props.onclick { + h.call(event); + } + }, + "{props.text}" + } } } diff --git a/AobaClient/src/components/basic/input.rs b/AobaClient/src/components/basic/input.rs index 19966e3..9bc33d6 100644 --- a/AobaClient/src/components/basic/input.rs +++ b/AobaClient/src/components/basic/input.rs @@ -7,6 +7,7 @@ pub struct InputProps { pub label: Option, pub placeholder: Option, pub name: String, + pub oninput: Option>, } #[component] diff --git a/AobaClient/src/components/media_grid.rs b/AobaClient/src/components/media_grid.rs index e7e9020..b3dab4a 100644 --- a/AobaClient/src/components/media_grid.rs +++ b/AobaClient/src/components/media_grid.rs @@ -1,17 +1,17 @@ -use std::str::FromStr; - use dioxus::prelude::*; -use tonic::{metadata::MetadataValue, IntoRequest, Request}; +use tonic::{IntoRequest, Request}; -use crate::rpc::{ - aoba::{MediaModel, PageFilter}, - get_rpc_client, +use crate::{ + components::MediaItem, + rpc::{aoba::PageFilter, get_rpc_client}, }; #[derive(PartialEq, Clone, Props)] pub struct MediaGridProps { pub query: Option, + #[props(default = Some(1))] pub page: Option, + #[props(default = Some(100))] pub page_size: Option, } @@ -34,14 +34,14 @@ impl Into for MediaGridProps { #[component] pub fn MediaGrid(props: MediaGridProps) -> Element { - let media_result = use_resource(|| async move { + let media_result = use_resource(use_reactive!(|(props,)| async move { let mut client = get_rpc_client(); - let mut req = Request::new(PageFilter::default()); + let mut req = Request::new(props.into()); req.metadata_mut() .insert("authorization", "Bearer ".parse().unwrap()); let result = client.list_media(req).await; return result.expect("Failed to load media").into_inner(); - }); + })); match &*media_result.read_unchecked() { Some(result) => rsx! { @@ -49,53 +49,16 @@ pub fn MediaGrid(props: MediaGridProps) -> Element { class: "mediaGrid", {result.items.iter().map(|itm| rsx!{ MediaItem { item: itm.clone() } - })} + })}, } }, - None => rsx!(), - } - // let items = media_result..unwrap().items; - // rsx! { - // div{ - // class: "mediaGrid", - // {items.iter().map(|itm| rsx!{ - // MediaItem { item: itm.clone() } - // })} - // } - // } -} - -#[derive(PartialEq, Clone, Props)] -pub struct MediaItemProps { - pub item: MediaModel, -} - -#[component] -pub fn MediaItem(props: MediaItemProps) -> Element { - let filename = props.item.file_name; - let id = props.item.id.unwrap().value; - let mtype = props.item.media_type.to_string(); - // let url = "https://aoba.app/i/{}"; - rsx! { - div{ - class: "mediaItem", - img{ src: "https://aoba.app/i/{id}" } - div { - class: "info", - span{ - class: "name", - "{filename}" - }, - div{ - class: "details", - span{ - "{mtype}" - }, - span{ - "{props.item.view_count}" - }, + None => rsx! { + div{ + class: "mediaGrid", + div { + "No results could be loaded" } } - } + }, } } diff --git a/AobaClient/src/components/media_item.rs b/AobaClient/src/components/media_item.rs new file mode 100644 index 0000000..36469ea --- /dev/null +++ b/AobaClient/src/components/media_item.rs @@ -0,0 +1,43 @@ +use dioxus::prelude::*; + +use crate::rpc::aoba::MediaModel; + +#[derive(PartialEq, Clone, Props)] +pub struct MediaItemProps { + pub item: MediaModel, +} + +#[component] +pub fn MediaItem(props: MediaItemProps) -> Element { + let mtype = props.item.media_type().as_str_name(); + let filename = props.item.file_name; + let id = props.item.media_id.unwrap().value; + + #[cfg(debug_assertions)] + let src = format!("http://localhost:5164/m/{id}"); + #[cfg(not(debug_assertions))] + let src = format!("https://aoba.app/m/{id}"); + // let url = "https://aoba.app/i/{}"; + rsx! { + div{ + class: "mediaItem", + img{ src: src } + div { + class: "info", + span{ + class: "name", + "{filename}" + }, + div{ + class: "details", + span{ + "{mtype}" + }, + span{ + "{props.item.view_count}" + }, + } + } + } + } +} diff --git a/AobaClient/src/components/mod.rs b/AobaClient/src/components/mod.rs index 450f4a2..f45d407 100644 --- a/AobaClient/src/components/mod.rs +++ b/AobaClient/src/components/mod.rs @@ -1,7 +1,9 @@ pub mod basic; mod media_grid; +mod media_item; mod navbar; mod search; pub use media_grid::*; +pub use media_item::*; pub use navbar::*; pub use search::*; diff --git a/AobaClient/src/components/search.rs b/AobaClient/src/components/search.rs index 5fe5e2e..74b053d 100644 --- a/AobaClient/src/components/search.rs +++ b/AobaClient/src/components/search.rs @@ -1,13 +1,15 @@ use dioxus::prelude::*; #[component] -pub fn Search() -> Element { +pub fn Search(query: Option, oninput: EventHandler) -> Element { rsx! { div{ class: "searchBar", input { type: "search", - placeholder: "Search Files" + placeholder: "Search Files", + value: query.unwrap_or("".into()), + oninput: move |event| oninput.call(event) } } } diff --git a/AobaClient/src/views/home.rs b/AobaClient/src/views/home.rs index 16bd59a..14e55df 100644 --- a/AobaClient/src/views/home.rs +++ b/AobaClient/src/views/home.rs @@ -3,10 +3,18 @@ use dioxus::prelude::*; #[component] pub fn Home() -> Element { + let mut query = use_signal(|| "".to_string()); + rsx! { - div { id: "content", - Search { }, - MediaGrid { } + div { + id: "content", + Search { + query: query.cloned(), + oninput: move |event:FormEvent| { + query.set(event.value()) + } + }, + MediaGrid { query: query.cloned() } } } } diff --git a/AobaCore/AobaCore.csproj b/AobaCore/AobaCore.csproj index 835ab6f..68588d2 100644 --- a/AobaCore/AobaCore.csproj +++ b/AobaCore/AobaCore.csproj @@ -9,6 +9,7 @@ + diff --git a/AobaCore/AobaIndexCreationService.cs b/AobaCore/AobaIndexCreationService.cs new file mode 100644 index 0000000..7412a39 --- /dev/null +++ b/AobaCore/AobaIndexCreationService.cs @@ -0,0 +1,26 @@ +using AobaCore.Models; + +using Microsoft.Extensions.Hosting; + +using MongoDB.Driver; + +namespace AobaCore; + +public class AobaIndexCreationService(IMongoDatabase db): BackgroundService +{ + private readonly IMongoCollection _media = db.GetCollection("media"); + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + var textKeys = Builders.IndexKeys + .Text(m => m.Filename); + + var textModel = new CreateIndexModel(textKeys, new CreateIndexOptions + { + Name = "Text", + Background = true + }); + + await _media.EnsureIndexAsync(textModel); + } +} \ No newline at end of file diff --git a/AobaCore/Extensions.cs b/AobaCore/Extensions.cs index 405ea6d..266c023 100644 --- a/AobaCore/Extensions.cs +++ b/AobaCore/Extensions.cs @@ -23,6 +23,20 @@ public static class Extensions services.AddSingleton(dbClient); services.AddSingleton(db); services.AddSingleton(); + services.AddHostedService(); return services; } + + public static async Task EnsureIndexAsync(this IMongoCollection collection, CreateIndexModel indexModel) + { + try + { + await collection.Indexes.CreateOneAsync(indexModel); + } + catch (MongoCommandException e) when (e.Code == 85 || e.Code == 86) //CodeName "IndexOptionsConflict" or "NameConflict" + { + await collection.Indexes.DropOneAsync(indexModel.Options.Name); + await collection.Indexes.CreateOneAsync(indexModel); + } + } } diff --git a/AobaServer/AobaServer.csproj b/AobaServer/AobaServer.csproj index 94af97f..70bff30 100644 --- a/AobaServer/AobaServer.csproj +++ b/AobaServer/AobaServer.csproj @@ -32,7 +32,8 @@ - + + diff --git a/AobaServer/Controllers/MediaController.cs b/AobaServer/Controllers/MediaController.cs index 2d869b7..e759c8c 100644 --- a/AobaServer/Controllers/MediaController.cs +++ b/AobaServer/Controllers/MediaController.cs @@ -16,16 +16,12 @@ public class MediaController(AobaService aobaService, ILogger l [ResponseCache(Duration = int.MaxValue)] public async Task MediaAsync(ObjectId id, [FromServices] MongoClient client, CancellationToken cancellationToken) { - using var session = await client.StartSessionAsync(cancellationToken: cancellationToken); - session.StartTransaction(); var file = await aobaService.GetFileStreamAsync(id, cancellationToken: cancellationToken); if (file.HasError) { - await session.AbortTransactionAsync(cancellationToken: cancellationToken); logger.LogError(file.Error.Exception, "Failed to load media stream"); return NotFound(); } - await session.CommitTransactionAsync(cancellationToken: cancellationToken); var mime = MimeTypesMap.GetMimeType(file.Value.FileInfo.Filename); _ = aobaService.IncrementFileViewCountAsync(id, cancellationToken); return File(file, mime, true); diff --git a/AobaServer/Program.cs b/AobaServer/Program.cs index 49c4b19..dfc071a 100644 --- a/AobaServer/Program.cs +++ b/AobaServer/Program.cs @@ -42,7 +42,16 @@ builder.Services.AddCors(o => p.AllowAnyOrigin(); p.AllowAnyMethod(); p.AllowAnyHeader(); - //p.WithOrigins("http://127.0.0.1:8080", "https://aoba.app"); + }); + o.AddPolicy("RPC", p => + { + p.AllowAnyMethod(); + p.AllowAnyHeader(); +#if DEBUG + p.AllowAnyOrigin(); +#else + p.WithOrigins("http://127.0.0.1:8080", "https://aoba.app"); +#endif }); }); @@ -107,7 +116,10 @@ app.UseAuthorization(); app.MapControllers(); app.MapObserability(); app.MapGrpcService() - .RequireCors("AllowAll"); + .RequireCors("RPC"); +app.MapGrpcService() + .AllowAnonymous() + .RequireCors("RPC"); diff --git a/AobaServer/Proto/Auth.proto b/AobaServer/Proto/Auth.proto new file mode 100644 index 0000000..359bbd2 --- /dev/null +++ b/AobaServer/Proto/Auth.proto @@ -0,0 +1,33 @@ +syntax = "proto3"; + +option csharp_namespace = "Aoba.RPC.Auth"; +package aoba.Auth; + +service AuthRpc { + rpc Login(Credentials) returns (LoginResponse); + rpc LoginPasskey(PassKeyPayload) returns (LoginResponse); +} + +message Credentials{ + string user = 1; + string password = 2; +} + +message PassKeyPayload { + +} + +message Jwt{ + string token = 1; +} + +message LoginResponse{ + oneof result { + Jwt jwt = 1; + LoginError error = 2; + } +} + +message LoginError{ + string message = 1; +} \ No newline at end of file diff --git a/AobaServer/Services/AobaAuthService.cs b/AobaServer/Services/AobaAuthService.cs new file mode 100644 index 0000000..be73916 --- /dev/null +++ b/AobaServer/Services/AobaAuthService.cs @@ -0,0 +1,6 @@ +namespace AobaServer.Services; + +public class AobaAuthService() : Aoba.RPC.Auth.AuthRpc.AuthRpcBase +{ + +} \ No newline at end of file diff --git a/AobaServer/Utils/Extensions.cs b/AobaServer/Utils/Extensions.cs index 393d356..324bf39 100644 --- a/AobaServer/Utils/Extensions.cs +++ b/AobaServer/Utils/Extensions.cs @@ -1,4 +1,5 @@ using MongoDB.Bson; +using MongoDB.Driver; namespace AobaServer.Utils; @@ -12,4 +13,6 @@ public static class Extensions return result; return ObjectId.Empty; } + + }