Streamlined grpc auth
Added ShareX Destiation on client
This commit is contained in:
@@ -21,3 +21,8 @@ input[type="text"] {
|
|||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
min-height: 200px;
|
||||||
|
min-width: 500px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ body {
|
|||||||
|
|
||||||
#content {
|
#content {
|
||||||
grid-area: Content;
|
grid-area: Content;
|
||||||
|
overflow-x: hidden;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
/* margin-left: $navBarSize; */
|
/* margin-left: $navBarSize; */
|
||||||
}
|
}
|
||||||
@@ -106,3 +107,11 @@ form {
|
|||||||
align-self: center;
|
align-self: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.codeSelect {
|
||||||
|
line-break: anywhere;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
background-color: $featureColor;
|
||||||
|
padding: 5px;
|
||||||
|
user-select: all;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use tonic::{IntoRequest, Request};
|
use tonic::IntoRequest;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
components::MediaItem,
|
components::MediaItem,
|
||||||
contexts::AuthContext,
|
|
||||||
rpc::{aoba::PageFilter, get_rpc_client},
|
rpc::{aoba::PageFilter, get_rpc_client},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -35,18 +34,9 @@ impl Into<PageFilter> for MediaGridProps {
|
|||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn MediaGrid(props: MediaGridProps) -> Element {
|
pub fn MediaGrid(props: MediaGridProps) -> Element {
|
||||||
let jwt = use_context::<AuthContext>().jwt;
|
let media_result = use_resource(use_reactive!(|(props)| async move {
|
||||||
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 result = client.list_media(props.into_request()).await;
|
||||||
let token = if jwt.cloned().is_some() {
|
|
||||||
jwt.unwrap()
|
|
||||||
} else {
|
|
||||||
"".into()
|
|
||||||
};
|
|
||||||
req.metadata_mut()
|
|
||||||
.insert("authorization", format!("Bearer {token}").parse().unwrap());
|
|
||||||
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();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use dioxus::signals::{Signal, Writable};
|
use dioxus::signals::{Signal, Writable};
|
||||||
use web_sys::window;
|
use web_sys::window;
|
||||||
|
|
||||||
|
use crate::rpc::{login, logout};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Default)]
|
#[derive(Clone, Copy, Default)]
|
||||||
pub struct AuthContext {
|
pub struct AuthContext {
|
||||||
pub jwt: Signal<Option<String>>,
|
pub jwt: Signal<Option<String>>,
|
||||||
@@ -11,12 +13,14 @@ impl AuthContext {
|
|||||||
self.jwt.set(Some(token.clone()));
|
self.jwt.set(Some(token.clone()));
|
||||||
let local_storage = window().unwrap().local_storage().unwrap().unwrap();
|
let local_storage = window().unwrap().local_storage().unwrap().unwrap();
|
||||||
_ = local_storage.set_item("token", token.as_str());
|
_ = local_storage.set_item("token", token.as_str());
|
||||||
|
login(token.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn logout(&mut self) {
|
pub fn logout(&mut self) {
|
||||||
self.jwt.set(None);
|
self.jwt.set(None);
|
||||||
let local_storage = window().unwrap().local_storage().unwrap().unwrap();
|
let local_storage = window().unwrap().local_storage().unwrap().unwrap();
|
||||||
_ = local_storage.remove_item("token");
|
_ = local_storage.remove_item("token");
|
||||||
|
logout();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
@@ -25,17 +29,14 @@ impl AuthContext {
|
|||||||
match local_storage.get_item("token") {
|
match local_storage.get_item("token") {
|
||||||
Ok(value) => {
|
Ok(value) => {
|
||||||
if let Some(jwt) = value {
|
if let Some(jwt) = value {
|
||||||
println!("jwt");
|
login(jwt.clone());
|
||||||
return AuthContext {
|
return AuthContext {
|
||||||
jwt: Signal::new(Some(jwt)),
|
jwt: Signal::new(Some(jwt)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return AuthContext::default();
|
return AuthContext::default();
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => AuthContext::default(),
|
||||||
println!("err");
|
|
||||||
AuthContext::default()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
use crate::{components::Navbar, contexts::AuthContext, views::Login, Route};
|
use crate::{Route, components::Navbar, contexts::AuthContext, views::Login};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn MainLayout() -> Element {
|
pub fn MainLayout() -> Element {
|
||||||
@@ -12,6 +12,9 @@ pub fn MainLayout() -> Element {
|
|||||||
|
|
||||||
return rsx! {
|
return rsx! {
|
||||||
Navbar {}
|
Navbar {}
|
||||||
Outlet::<Route> {}
|
div{
|
||||||
|
id: "content",
|
||||||
|
Outlet::<Route> {}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
|
|
||||||
use aoba::{aoba_rpc_client::AobaRpcClient, auth_rpc_client::AuthRpcClient};
|
use aoba::{aoba_rpc_client::AobaRpcClient, auth_rpc_client::AuthRpcClient};
|
||||||
|
use tonic::service::{Interceptor, interceptor::InterceptedService};
|
||||||
use tonic_web_wasm_client::Client;
|
use tonic_web_wasm_client::Client;
|
||||||
|
|
||||||
use crate::HOST;
|
use crate::HOST;
|
||||||
@@ -13,16 +14,18 @@ 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),
|
||||||
|
jwt: RwLock::new(None),
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct RpcConnection {
|
pub struct RpcConnection {
|
||||||
aoba: RwLock<Option<AobaRpcClient<Client>>>,
|
aoba: RwLock<Option<AobaRpcClient<InterceptedService<Client, AuthInterceptor>>>>,
|
||||||
auth: RwLock<Option<AuthRpcClient<Client>>>,
|
auth: RwLock<Option<AuthRpcClient<Client>>>,
|
||||||
|
jwt: RwLock<Option<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RpcConnection {
|
impl RpcConnection {
|
||||||
pub fn get_client(&self) -> AobaRpcClient<Client> {
|
pub fn get_client(&self) -> AobaRpcClient<InterceptedService<Client, AuthInterceptor>> {
|
||||||
self.ensure_client();
|
self.ensure_client();
|
||||||
return self.aoba.read().unwrap().clone().unwrap();
|
return self.aoba.read().unwrap().clone().unwrap();
|
||||||
}
|
}
|
||||||
@@ -35,16 +38,38 @@ impl RpcConnection {
|
|||||||
fn ensure_client(&self) {
|
fn ensure_client(&self) {
|
||||||
if self.aoba.read().unwrap().is_none() {
|
if self.aoba.read().unwrap().is_none() {
|
||||||
let wasm_client = Client::new(HOST.into());
|
let wasm_client = Client::new(HOST.into());
|
||||||
*self.aoba.write().unwrap() = Some(AobaRpcClient::new(wasm_client.clone()));
|
let aoba_client = AobaRpcClient::with_interceptor(wasm_client.clone(), AuthInterceptor);
|
||||||
|
*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()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_rpc_client() -> AobaRpcClient<Client> {
|
#[derive(Clone)]
|
||||||
|
pub struct AuthInterceptor;
|
||||||
|
impl Interceptor for AuthInterceptor {
|
||||||
|
fn call(&mut self, mut request: tonic::Request<()>) -> Result<tonic::Request<()>, tonic::Status> {
|
||||||
|
if let Some(jwt) = RPC_CLIENT.jwt.read().unwrap().clone() {
|
||||||
|
request
|
||||||
|
.metadata_mut()
|
||||||
|
.insert("authorization", format!("Bearer {jwt}").parse().unwrap());
|
||||||
|
}
|
||||||
|
return Ok(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_rpc_client() -> AobaRpcClient<InterceptedService<Client, AuthInterceptor>> {
|
||||||
return RPC_CLIENT.get_client();
|
return RPC_CLIENT.get_client();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_auth_rpc_client() -> AuthRpcClient<Client> {
|
pub fn get_auth_rpc_client() -> AuthRpcClient<Client> {
|
||||||
return RPC_CLIENT.get_auth_client();
|
return RPC_CLIENT.get_auth_client();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn login(jwt: String) {
|
||||||
|
*RPC_CLIENT.jwt.write().unwrap() = Some(jwt);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn logout() {
|
||||||
|
*RPC_CLIENT.jwt.write().unwrap() = None;
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,12 +6,9 @@ pub fn Home() -> Element {
|
|||||||
let query = use_signal(|| "".to_string());
|
let query = use_signal(|| "".to_string());
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div {
|
Search {
|
||||||
id: "content",
|
query: query
|
||||||
Search {
|
},
|
||||||
query: query
|
MediaGrid { query: query.cloned() }
|
||||||
},
|
|
||||||
MediaGrid { query: query.cloned() }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,34 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
use crate::rpc::get_rpc_client;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Settings() -> Element {
|
pub fn Settings() -> Element {
|
||||||
rsx! { "this is settings" }
|
let dst = use_resource(async move || {
|
||||||
|
let result = get_rpc_client().get_share_x_destination(()).await;
|
||||||
|
if let Ok(d) = result {
|
||||||
|
if let Some(r) = d.into_inner().dst_result {
|
||||||
|
return match r {
|
||||||
|
crate::rpc::aoba::share_x_response::DstResult::Destination(json) => json,
|
||||||
|
crate::rpc::aoba::share_x_response::DstResult::Error(err) => err,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return "No Result".to_string();
|
||||||
|
}
|
||||||
|
let err = result.err().unwrap();
|
||||||
|
let status = err.message();
|
||||||
|
return format!("Failed to load config: {status}").to_string();
|
||||||
|
});
|
||||||
|
|
||||||
|
let d = dst.cloned().unwrap_or("".to_string());
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
"this is settings"
|
||||||
|
div {
|
||||||
|
pre {
|
||||||
|
class: "codeSelect",
|
||||||
|
"test {d}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
public class ShareXDestination
|
public class ShareXDestination
|
||||||
{
|
{
|
||||||
public string Version { get; set; } = "13.1.0";
|
public string Version { get; set; } = "14.0.1";
|
||||||
public string Name { get; set; } = "Aoba";
|
public string Name { get; set; } = "Aoba";
|
||||||
public string DestinationType { get; set; } = "ImageUploader, TextUploader, FileUploader";
|
public string DestinationType { get; set; } = "ImageUploader, TextUploader, FileUploader";
|
||||||
public string RequestMethod { get; set; } = "POST";
|
public string RequestMethod { get; set; } = "POST";
|
||||||
@@ -13,6 +13,6 @@ public class ShareXDestination
|
|||||||
public string FileFormName { get; set; } = "file";
|
public string FileFormName { get; set; } = "file";
|
||||||
public string[] RegexList { get; set; } = ["([^/]+)/?$"];
|
public string[] RegexList { get; set; } = ["([^/]+)/?$"];
|
||||||
public string URL { get; set; } = "https://aoba.app$json:url$";
|
public string URL { get; set; } = "https://aoba.app$json:url$";
|
||||||
public required string ThumbnailURL { get; set; }
|
public string? ThumbnailURL { get; set; }
|
||||||
public required string DeletionURL { get; set; }
|
public string? DeletionURL { get; set; }
|
||||||
}
|
}
|
||||||
@@ -109,7 +109,6 @@ if (!app.Environment.IsDevelopment())
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.UseGrpcWeb(new GrpcWebOptions { DefaultEnabled = true });
|
app.UseGrpcWeb(new GrpcWebOptions { DefaultEnabled = true });
|
||||||
app.UseHttpsRedirection();
|
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
|
|
||||||
@@ -123,6 +122,7 @@ app.UseAuthorization();
|
|||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
app.MapObserability();
|
app.MapObserability();
|
||||||
app.MapGrpcService<AobaRpcService>()
|
app.MapGrpcService<AobaRpcService>()
|
||||||
|
.RequireAuthorization()
|
||||||
.RequireCors("RPC");
|
.RequireCors("RPC");
|
||||||
app.MapGrpcService<AobaAuthService>()
|
app.MapGrpcService<AobaAuthService>()
|
||||||
.AllowAnonymous()
|
.AllowAnonymous()
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
syntax = "proto3";
|
syntax = "proto3";
|
||||||
|
import "google/protobuf/empty.proto";
|
||||||
|
|
||||||
option csharp_namespace = "Aoba.RPC";
|
option csharp_namespace = "Aoba.RPC";
|
||||||
package aoba;
|
package aoba;
|
||||||
|
|
||||||
service AobaRpc {
|
service AobaRpc {
|
||||||
rpc GetMedia (Id) returns (MediaResponse);
|
rpc GetMedia (Id) returns (MediaResponse);
|
||||||
rpc DeleteMedia (Id) returns (Empty);
|
rpc DeleteMedia (Id) returns (google.protobuf.Empty);
|
||||||
rpc UpdateMedia (Empty) returns (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);
|
||||||
|
rpc GetShareXDestination(google.protobuf.Empty) returns (ShareXResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
message PageFilter {
|
message PageFilter {
|
||||||
@@ -24,7 +26,7 @@ message Id {
|
|||||||
message MediaResponse {
|
message MediaResponse {
|
||||||
oneof result {
|
oneof result {
|
||||||
MediaModel value = 1;
|
MediaModel value = 1;
|
||||||
Empty empty = 2;
|
google.protobuf.Empty empty = 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +46,7 @@ message Pagination {
|
|||||||
message UserResponse {
|
message UserResponse {
|
||||||
oneof userResult {
|
oneof userResult {
|
||||||
UserModel user = 1;
|
UserModel user = 1;
|
||||||
Empty empty = 2;
|
google.protobuf.Empty empty = 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +57,6 @@ message UserModel {
|
|||||||
bool isAdmin = 4;
|
bool isAdmin = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Empty {}
|
|
||||||
|
|
||||||
message MediaModel {
|
message MediaModel {
|
||||||
Id id = 1;
|
Id id = 1;
|
||||||
@@ -67,7 +68,7 @@ message MediaModel {
|
|||||||
Id owner = 7;
|
Id owner = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum MediaType{
|
enum MediaType {
|
||||||
Image = 0;
|
Image = 0;
|
||||||
Audio = 1;
|
Audio = 1;
|
||||||
Video = 2;
|
Video = 2;
|
||||||
@@ -75,3 +76,10 @@ enum MediaType{
|
|||||||
Code = 4;
|
Code = 4;
|
||||||
Raw = 5;
|
Raw = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message ShareXResponse {
|
||||||
|
oneof dstResult {
|
||||||
|
string destination = 1;
|
||||||
|
string error = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +1,24 @@
|
|||||||
using AobaCore;
|
using Aoba.RPC;
|
||||||
|
|
||||||
using Aoba.RPC;
|
using AobaCore;
|
||||||
|
|
||||||
|
using AobaServer.Models;
|
||||||
using AobaServer.Utils;
|
using AobaServer.Utils;
|
||||||
|
|
||||||
|
using Google.Protobuf.WellKnownTypes;
|
||||||
|
|
||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
using MongoDB.Bson.IO;
|
||||||
|
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace AobaServer.Services;
|
namespace AobaServer.Services;
|
||||||
|
|
||||||
public class AobaRpcService(AobaService aobaService) : AobaRpc.AobaRpcBase
|
public class AobaRpcService(AobaService aobaService, AccountsService accountsService, AuthInfo authInfo) : AobaRpc.AobaRpcBase
|
||||||
{
|
{
|
||||||
public override async Task<MediaResponse> GetMedia(Id request, ServerCallContext context)
|
public override async Task<MediaResponse> GetMedia(Id request, ServerCallContext context)
|
||||||
{
|
{
|
||||||
@@ -22,4 +32,29 @@ public class AobaRpcService(AobaService aobaService) : AobaRpc.AobaRpcBase
|
|||||||
return result.ToResponse();
|
return result.ToResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
public override async Task<ShareXResponse> GetShareXDestination(Empty request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
var userId = context.GetHttpContext().User.GetId();
|
||||||
|
var user = await accountsService.GetUserAsync(userId, context.CancellationToken);
|
||||||
|
if (user == null)
|
||||||
|
return new ShareXResponse { Error = "User does not exist" };
|
||||||
|
var token = user.GetToken(authInfo);
|
||||||
|
var dest = new ShareXDestination
|
||||||
|
{
|
||||||
|
DeletionURL = string.Empty,
|
||||||
|
ThumbnailURL = string.Empty,
|
||||||
|
Headers = new()
|
||||||
|
{
|
||||||
|
{ "Authorization", $"Bearer {token}" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return new ShareXResponse
|
||||||
|
{
|
||||||
|
Destination = JsonSerializer.Serialize(dest, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
WriteIndented = true
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
using AobaServer.Models;
|
using AobaServer.Models;
|
||||||
|
|
||||||
|
using Grpc.Core;
|
||||||
|
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
using MongoDB.Bson;
|
using MongoDB.Bson;
|
||||||
@@ -37,4 +39,9 @@ public static class Extensions
|
|||||||
{
|
{
|
||||||
return user.FindFirstValue(ClaimTypes.NameIdentifier).ToObjectId();
|
return user.FindFirstValue(ClaimTypes.NameIdentifier).ToObjectId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ObjectId GetUserId(this ServerCallContext context)
|
||||||
|
{
|
||||||
|
return context.GetHttpContext().User.GetId();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using Aoba.RPC;
|
using Aoba.RPC;
|
||||||
|
|
||||||
using MongoDB.Bson;
|
using MongoDB.Bson;
|
||||||
|
using Google.Protobuf.WellKnownTypes;
|
||||||
|
|
||||||
namespace AobaServer.Utils;
|
namespace AobaServer.Utils;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user