fix: do some fix

This commit is contained in:
2025-03-09 01:22:30 +08:00
parent f94e175082
commit 07ac7e3376
29 changed files with 422 additions and 628 deletions

View File

@@ -5,8 +5,6 @@
}
```
^https://konobangu.com/api/playground*** reqHeaders://{x-forwarded.json} http://127.0.0.1:5002/api/playground$1
^wss://konobangu.com/api/playground*** reqHeaders://{x-forwarded.json} ws://127.0.0.1:5002/api/playground$1
^https://konobangu.com/api*** reqHeaders://{x-forwarded.json} http://127.0.0.1:5001/api$1 excludeFilter://^^https://konobangu.com/api/playground***
^https://konobangu.com/api*** reqHeaders://{x-forwarded.json} http://127.0.0.1:5001/api$1
^https://konobangu.com/*** reqHeaders://{x-forwarded.json} http://127.0.0.1:5000/$1 excludeFilter://^https://konobangu.com/api***
^wss://konobangu.com/*** reqHeaders://{x-forwarded.json} ws://127.0.0.1:5000/$1 excludeFilter://^wss://konobangu.com/api/playground***
^wss://konobangu.com/*** reqHeaders://{x-forwarded.json} ws://127.0.0.1:5000/$1 excludeFilter://^wss://konobangu.com/api

8
apps/recorder/.env Normal file
View File

@@ -0,0 +1,8 @@
AUTH_TYPE = "basic" # or oidc
BASIC_USER = "konobangu"
BASIC_PASSWORD = "konobangu"
# OIDC_ISSUER="https://auth.logto.io/oidc"
# OIDC_API_AUDIENCE = "https://konobangu.com/api"
# OIDC_CLIENT_ID = "client_id"
# OIDC_CLIENT_SECRET = "client_secret" # optional
# OIDC_EXTRA_SCOPES = "read:konobangu write:konobangu"

View File

@@ -29,7 +29,7 @@ pub struct AppBuilder {
dotenv_file: Option<String>,
config_file: Option<String>,
working_dir: String,
enviornment: Environment,
environment: Environment,
}
impl AppBuilder {
@@ -70,21 +70,21 @@ impl AppBuilder {
pub async fn build(self) -> RResult<App> {
AppConfig::load_dotenv(
&self.enviornment,
&self.environment,
&self.working_dir,
self.dotenv_file.as_deref(),
)
.await?;
let config = AppConfig::load_config(
&self.enviornment,
&self.environment,
&self.working_dir,
self.config_file.as_deref(),
)
.await?;
let app_context = Arc::new(
AppContext::new(self.enviornment.clone(), config, self.working_dir.clone()).await?,
AppContext::new(self.environment.clone(), config, self.working_dir.clone()).await?,
);
Ok(App {
@@ -101,7 +101,7 @@ impl AppBuilder {
pub fn environment(self, environment: Environment) -> Self {
let mut ret = self;
ret.enviornment = environment;
ret.environment = environment;
ret
}
@@ -130,7 +130,7 @@ impl AppBuilder {
impl Default for AppBuilder {
fn default() -> Self {
Self {
enviornment: Environment::Production,
environment: Environment::Production,
dotenv_file: None,
config_file: None,
working_dir: String::from("."),

View File

@@ -34,12 +34,13 @@ impl App {
let mut router = Router::<Arc<dyn AppContextTrait>>::new();
let (graphqlc, oidcc) = try_join!(
let (graphql_c, oidc_c, metadata_c) = try_join!(
controller::graphql::create(context.clone()),
controller::oidc::create(context.clone()),
controller::metadata::create(context.clone())
)?;
for c in [graphqlc, oidcc] {
for c in [graphql_c, oidc_c, metadata_c] {
router = c.apply_to(router);
}

View File

@@ -1,37 +0,0 @@
import { LogLevel, type OpenIdConfiguration } from 'oidc-client-rx';
export const isBasicAuth = process.env.AUTH_TYPE === 'basic';
export function buildOidcConfig(): OpenIdConfiguration {
const origin = window.location.origin;
const resource = process.env.OIDC_AUDIENCE!;
return {
authority: process.env.OIDC_ISSUER!,
redirectUrl: `${origin}/api/playground/oidc/callback`,
postLogoutRedirectUri: `${origin}/api/playground`,
clientId: process.env.OIDC_CLIENT_ID!,
clientSecret: process.env.OIDC_CLIENT_SECRET,
scope: process.env.OIDC_EXTRA_SCOPES
? `openid profile email offline_access ${process.env.OIDC_EXTRA_SCOPES}`
: 'openid profile email offline_access',
triggerAuthorizationResultEvent: true,
responseType: 'code',
silentRenew: true,
useRefreshToken: true,
logLevel: LogLevel.Debug,
autoUserInfo: !resource,
renewUserInfoAfterTokenRenew: !resource,
customParamsAuthRequest: {
prompt: 'consent',
resource,
},
customParamsRefreshTokenRequest: {
resource,
},
customParamsCodeRequest: {
resource,
},
};
}

View File

@@ -45,7 +45,7 @@ pub enum AuthError {
#[error("Invalid oidc request callback code")]
OidcInvalidCodeError,
#[error(transparent)]
OidcCallbackTokenConfigrationError(#[from] ConfigurationError),
OidcCallbackTokenConfigurationError(#[from] ConfigurationError),
#[error(transparent)]
OidcRequestTokenError(
#[from] RequestTokenError<HttpClientError, StandardErrorResponse<CoreErrorResponseType>>,
@@ -120,22 +120,26 @@ fn display_graphql_permission_error(
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AuthErrorBody {
pub error_code: i32,
pub error_msg: String,
pub struct AuthErrorResponse {
pub success: bool,
pub message: String,
}
impl From<AuthError> for AuthErrorBody {
impl From<AuthError> for AuthErrorResponse {
fn from(value: AuthError) -> Self {
AuthErrorBody {
error_code: StatusCode::UNAUTHORIZED.as_u16() as i32,
error_msg: value.to_string(),
AuthErrorResponse {
success: false,
message: value.to_string(),
}
}
}
impl IntoResponse for AuthError {
fn into_response(self) -> Response {
(StatusCode::UNAUTHORIZED, Json(AuthErrorBody::from(self))).into_response()
(
StatusCode::UNAUTHORIZED,
Json(AuthErrorResponse::from(self)),
)
.into_response()
}
}

View File

@@ -1,19 +0,0 @@
import { runInInjectionContext } from '@outposts/injection-js';
import { autoLoginPartialRoutesGuard } from 'oidc-client-rx';
import { firstValueFrom } from 'rxjs';
import type { RouterContext } from '../web/controller/__root';
export const beforeLoadGuard = async ({
context,
}: { context: RouterContext }) => {
if (!context.isAuthenticated) {
const guard$ = runInInjectionContext(context.injector, () =>
autoLoginPartialRoutesGuard()
);
const isAuthenticated = await firstValueFrom(guard$);
if (!isAuthenticated) {
throw !isAuthenticated;
}
}
};

View File

@@ -1,52 +0,0 @@
import { useObservableEagerState, useObservableState } from 'observable-hooks';
import {
InjectorContextVoidInjector,
useOidcClient,
} from 'oidc-client-rx/adapters/react';
import { useMemo } from 'react';
import { NEVER, type Observable, of } from 'rxjs';
import { isBasicAuth } from './config';
import {
CHECK_AUTH_RESULT_EVENT,
type CheckAuthResultEventType,
} from './event';
const BASIC_AUTH_IS_AUTHENTICATED$ = of({
isAuthenticated: true,
allConfigsAuthenticated: [],
});
const BASIC_AUTH_USER_DATA$ = of({
userData: {},
allUserData: [],
});
export function useAuth() {
const { oidcSecurityService, injector } = isBasicAuth
? { oidcSecurityService: undefined, injector: InjectorContextVoidInjector }
: // biome-ignore lint/correctness/useHookAtTopLevel: <explanation>
useOidcClient();
const { isAuthenticated } = useObservableEagerState(
oidcSecurityService?.isAuthenticated$ ?? BASIC_AUTH_IS_AUTHENTICATED$
);
const { userData } = useObservableEagerState(
oidcSecurityService?.userData$ ?? BASIC_AUTH_USER_DATA$
);
const checkAuthResultEvent = useObservableState(
useMemo(
() => (isBasicAuth ? NEVER : injector.get(CHECK_AUTH_RESULT_EVENT)),
[injector]
) as Observable<CheckAuthResultEventType>
);
return {
oidcSecurityService,
isAuthenticated,
userData,
injector,
checkAuthResultEvent,
};
}

View File

@@ -1,6 +1,9 @@
use std::{borrow::Cow, error::Error as StdError};
use axum::response::{IntoResponse, Response};
use axum::{
Json,
response::{IntoResponse, Response},
};
use http::StatusCode;
use serde::{Deserialize, Deserializer, Serialize};
use thiserror::Error as ThisError;
@@ -105,11 +108,33 @@ impl RError {
}
}
#[derive(Serialize, Debug, Clone)]
pub struct StandardErrorResponse<T = ()> {
pub success: bool,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<T>,
}
impl<T> From<String> for StandardErrorResponse<T> {
fn from(value: String) -> Self {
StandardErrorResponse {
success: false,
message: value,
result: None,
}
}
}
impl IntoResponse for RError {
fn into_response(self) -> Response {
match self {
Self::AuthError(auth_error) => auth_error.into_response(),
err => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response(),
err => (
StatusCode::INTERNAL_SERVER_ERROR,
Json::<StandardErrorResponse>(StandardErrorResponse::from(err.to_string())),
)
.into_response(),
}
}
}

View File

@@ -22,7 +22,6 @@ use crate::{
},
fetch::{html::fetch_html, image::fetch_image},
storage::StorageContentCategory,
tasks::core::{StandardStreamTaskReplayLayout, StreamTaskRunnerTrait},
};
#[derive(Clone, Debug, PartialEq)]
@@ -349,24 +348,15 @@ pub async fn extract_mikan_bangumi_meta_from_bangumi_homepage(
})
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ExtractMikanBangumisMetaFromMyBangumiRequest {
pub my_bangumi_page_url: Url,
pub auth_securcy: Option<MikanAuthSecrecy>,
}
pub type ExtractMikanBangumisMetaFromMyBangumiTask =
StandardStreamTaskReplayLayout<ExtractMikanBangumisMetaFromMyBangumiRequest, MikanBangumiMeta>;
#[instrument(skip_all, fields(my_bangumi_page_url, auth_securcy = ?auth_securcy, history = history.len()))]
#[instrument(skip_all, fields(my_bangumi_page_url, auth_secrecy = ?auth_secrecy, history = history.len()))]
pub fn extract_mikan_bangumis_meta_from_my_bangumi_page(
context: Arc<dyn AppContextTrait>,
my_bangumi_page_url: Url,
auth_securcy: Option<MikanAuthSecrecy>,
auth_secrecy: Option<MikanAuthSecrecy>,
history: &[Arc<RResult<MikanBangumiMeta>>],
) -> impl Stream<Item = RResult<MikanBangumiMeta>> {
try_stream! {
let http_client = &context.mikan().fork_with_auth(auth_securcy.clone())?;
let http_client = &context.mikan().fork_with_auth(auth_secrecy.clone())?;
let mikan_base_url = Url::parse(&my_bangumi_page_url.origin().unicode_serialization())?;
@@ -498,22 +488,6 @@ pub fn extract_mikan_bangumis_meta_from_my_bangumi_page(
}
}
impl StreamTaskRunnerTrait for ExtractMikanBangumisMetaFromMyBangumiTask {
fn run(
context: Arc<dyn AppContextTrait>,
request: &Self::Request,
history: &[Arc<RResult<Self::Item>>],
) -> impl Stream<Item = RResult<Self::Item>> {
let context = context.clone();
extract_mikan_bangumis_meta_from_my_bangumi_page(
context,
request.my_bangumi_page_url.clone(),
request.auth_securcy.clone(),
history,
)
}
}
#[cfg(test)]
mod test {
#![allow(unused_variables)]

View File

@@ -1,38 +0,0 @@
// use std::borrow::Cow;
// use futures::{TryStreamExt, pin_mut};
// use crate::{
// app::AppContextTrait,
// errors::RResult,
// extract::mikan::{
// MikanAuthSecrecy,
// web_extract::extract_mikan_bangumis_meta_from_my_bangumi_page, },
// tasks::core::{StreamTaskTrait, TaskVars},
// };
// #[derive(Debug)]
// pub struct CreateMikanRSSFromMyBangumiTask {
// pub subscriber_id: i32,
// pub task_id: String,
// pub auth_secrecy: MikanAuthSecrecy,
// }
// async fn run(app_context: &dyn AppContextTrait, _vars: &TaskVars) ->
// RResult<()> { let mikan_client = app_context
// .mikan
// .fork_with_auth(todo!().auth_secrecy.clone())?;
// {
// let bangumi_metas = extract_mikan_bangumis_meta_from_my_bangumi_page(
// &mikan_client,
// mikan_client.base_url().join("/Home/MyBangumi")?,
// );
// pin_mut!(bangumi_metas);
// let _bangumi_metas = bangumi_metas.try_collect::<Vec<_>>().await?;
// }
// Ok(())
// }

View File

@@ -0,0 +1,37 @@
use std::sync::Arc;
use futures::Stream;
use serde::{Deserialize, Serialize};
use url::Url;
use crate::{
app::AppContextTrait,
errors::RResult,
extract::mikan::{MikanAuthSecrecy, MikanBangumiMeta, web_extract},
tasks::core::{StandardStreamTaskReplayLayout, StreamTaskRunnerTrait},
};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ExtractMikanBangumisMetaFromMyBangumiRequest {
pub my_bangumi_page_url: Url,
pub auth_secrecy: Option<MikanAuthSecrecy>,
}
pub type ExtractMikanBangumisMetaFromMyBangumiTask =
StandardStreamTaskReplayLayout<ExtractMikanBangumisMetaFromMyBangumiRequest, MikanBangumiMeta>;
impl StreamTaskRunnerTrait for ExtractMikanBangumisMetaFromMyBangumiTask {
fn run(
context: Arc<dyn AppContextTrait>,
request: &Self::Request,
history: &[Arc<RResult<Self::Item>>],
) -> impl Stream<Item = RResult<Self::Item>> {
let context = context.clone();
web_extract::extract_mikan_bangumis_meta_from_my_bangumi_page(
context,
request.my_bangumi_page_url.clone(),
request.auth_secrecy.clone(),
history,
)
}
}

View File

@@ -1 +1 @@
pub mod create_mikan_bangumi_subscriptions_from_my_bangumi_page;
pub mod extract_mikan_bangumis_meta_from_my_bangumi;

View File

@@ -1 +1,4 @@
#[derive(Debug)]
pub struct TaskService {}
impl TaskService {}

View File

@@ -0,0 +1,40 @@
use std::sync::Arc;
use axum::{Json, Router, extract::State, routing::get};
use serde::Serialize;
use crate::{app::AppContextTrait, errors::RResult, web::controller::Controller};
pub const CONTROLLER_PREFIX: &str = "/api/metadata";
#[derive(Serialize)]
pub struct StandardResponse {
pub success: bool,
pub message: String,
}
async fn health(State(ctx): State<Arc<dyn AppContextTrait>>) -> RResult<Json<StandardResponse>> {
ctx.db().ping().await.inspect_err(
|err| tracing::error!(err.msg = %err, err.detail = ?err, "health check database ping error"),
)?;
Ok(Json(StandardResponse {
success: true,
message: "ok".to_string(),
}))
}
async fn ping() -> Json<StandardResponse> {
Json(StandardResponse {
success: true,
message: "ok".to_string(),
})
}
pub async fn create(_context: Arc<dyn AppContextTrait>) -> RResult<Controller> {
let router = Router::<Arc<dyn AppContextTrait>>::new()
.route("/health", get(health))
.route("/ping", get(ping));
Ok(Controller::from_prefix(CONTROLLER_PREFIX, router))
}

View File

@@ -1,5 +1,6 @@
pub mod core;
pub mod graphql;
pub mod metadata;
pub mod oidc;
pub use core::{Controller, ControllerTrait, PrefixController};

View File

@@ -0,0 +1 @@
{"rustc_fingerprint":12631718921104437280,"outputs":{"9566862992471862046":{"success":true,"status":"","code":0,"stdout":"___.exe\nlib___.rlib\n___.dll\n___.dll\n___.lib\n___.dll\nC:\\code\\scoop\\persist\\rustup\\.rustup\\toolchains\\nightly-x86_64-pc-windows-msvc\npacked\n___\ndebug_assertions\nfmt_debug=\"full\"\noverflow_checks\npanic=\"unwind\"\nproc_macro\nrelocation_model=\"pic\"\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"msvc\"\ntarget_family=\"windows\"\ntarget_feature=\"cmpxchg16b\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"lahfsahf\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_feature=\"sse3\"\ntarget_feature=\"x87\"\ntarget_has_atomic\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_has_atomic_equal_alignment=\"128\"\ntarget_has_atomic_equal_alignment=\"16\"\ntarget_has_atomic_equal_alignment=\"32\"\ntarget_has_atomic_equal_alignment=\"64\"\ntarget_has_atomic_equal_alignment=\"8\"\ntarget_has_atomic_equal_alignment=\"ptr\"\ntarget_has_atomic_load_store\ntarget_has_atomic_load_store=\"128\"\ntarget_has_atomic_load_store=\"16\"\ntarget_has_atomic_load_store=\"32\"\ntarget_has_atomic_load_store=\"64\"\ntarget_has_atomic_load_store=\"8\"\ntarget_has_atomic_load_store=\"ptr\"\ntarget_os=\"windows\"\ntarget_pointer_width=\"64\"\ntarget_thread_local\ntarget_vendor=\"pc\"\nub_checks\nwindows\n","stderr":""},"5537925964935398022":{"success":true,"status":"","code":0,"stdout":"rustc 1.86.0-nightly (43ca9d18e 2025-02-08)\nbinary: rustc\ncommit-hash: 43ca9d18e333797f0aa3b525501a7cec8d61a96b\ncommit-date: 2025-02-08\nhost: x86_64-pc-windows-msvc\nrelease: 1.86.0-nightly\nLLVM version: 19.1.7\n","stderr":""}},"successes":{}}

View File

@@ -0,0 +1,3 @@
Signature: 8a477f597d28d172789f06886806bc55
# This file is a cache directory tag created by cargo.
# For information about cache directory tags see https://bford.info/cachedir/

13
apps/webui/.env Normal file
View File

@@ -0,0 +1,13 @@
HOST="konobangu.com"
DATABASE_URL = "postgres://konobangu:konobangu@localhost:5432/konobangu"
STORAGE_DATA_DIR = "./data"
AUTH_TYPE = "basic" # or oidc
BASIC_USER = "konobangu"
BASIC_PASSWORD = "konobangu"
# OIDC_ISSUER="https://auth.logto.io/oidc"
# OIDC_API_AUDIENCE = "https://konobangu.com/api"
# OIDC_CLIENT_ID = "client_id"
# OIDC_CLIENT_SECRET = "client_secret" # optional
# OIDC_EXTRA_SCOPES = "read:konobangu write:konobangu"
# OIDC_EXTRA_CLAIM_KEY = ""
# OIDC_EXTRA_CLAIM_VALUE = ""

View File

@@ -0,0 +1 @@
../../../../assets/favicon.webp

View File

@@ -1,4 +1,4 @@
import { Image } from '@kobalte/core/image';
import {Image} from '@kobalte/core/image';
import {
SidebarMenu,
SidebarMenuButton,
@@ -13,13 +13,14 @@ export function AppIcon() {
size="lg"
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<div class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
<div
class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
<Image fallbackDelay={1000}>
<Image.Img
src="/assets/favicon.png"
class="size-8 object-cover"
/>
<Image.Fallback>KONOBANGU</Image.Fallback>
<Image.Fallback>KO</Image.Fallback>
</Image>
</div>
<div class="grid flex-1 gap-1 py-1 text-left text-sm leading-tight">