feat: replace graphql playground to altair
This commit is contained in:
parent
97b7bfb7fb
commit
c6677d414d
@ -2,7 +2,7 @@ root = true
|
|||||||
|
|
||||||
[*]
|
[*]
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 2
|
indent_size = 4
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
|
7
Cargo.lock
generated
7
Cargo.lock
generated
@ -3298,8 +3298,6 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "loco-gen"
|
name = "loco-gen"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ef868bd2df99c949018850b36fb700bba01b10001715f94390bcdb81f412f874"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
@ -3318,8 +3316,6 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "loco-rs"
|
name = "loco-rs"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2250c89f0f996c3493ec3d2588a2d63e2861a48df7b9585cb28fbf6faf15a1a0"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -4621,12 +4617,15 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_with",
|
"serde_with",
|
||||||
|
"serde_yaml",
|
||||||
"serial_test",
|
"serial_test",
|
||||||
|
"tera",
|
||||||
"testcontainers",
|
"testcontainers",
|
||||||
"testcontainers-modules",
|
"testcontainers-modules",
|
||||||
"thiserror 2.0.10",
|
"thiserror 2.0.10",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower 0.5.2",
|
"tower 0.5.2",
|
||||||
|
"tower-http",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"url",
|
"url",
|
||||||
|
@ -6,6 +6,7 @@ resolver = "2"
|
|||||||
testcontainers = { git = "https://github.com/testcontainers/testcontainers-rs.git", rev = "af21727" }
|
testcontainers = { git = "https://github.com/testcontainers/testcontainers-rs.git", rev = "af21727" }
|
||||||
# loco-rs = { git = "https://github.com/lonelyhentxi/loco.git", rev = "beb890e" }
|
# loco-rs = { git = "https://github.com/lonelyhentxi/loco.git", rev = "beb890e" }
|
||||||
# loco-rs = { git = "https://github.com/loco-rs/loco.git" }
|
# loco-rs = { git = "https://github.com/loco-rs/loco.git" }
|
||||||
|
loco-rs = { path = "./patches/loco" }
|
||||||
async-graphql = { git = "https://github.com/aumetra/async-graphql.git", rev = "690ece7" }
|
async-graphql = { git = "https://github.com/aumetra/async-graphql.git", rev = "690ece7" }
|
||||||
async-graphql-axum = { git = "https://github.com/aumetra/async-graphql.git", rev = "690ece7" }
|
async-graphql-axum = { git = "https://github.com/aumetra/async-graphql.git", rev = "690ece7" }
|
||||||
jwt-authorizer = { git = "https://github.com/blablacio/jwt-authorizer.git", rev = "e956774" }
|
jwt-authorizer = { git = "https://github.com/blablacio/jwt-authorizer.git", rev = "e956774" }
|
||||||
|
@ -3,11 +3,11 @@ import {
|
|||||||
noseconeConfig,
|
noseconeConfig,
|
||||||
noseconeMiddleware,
|
noseconeMiddleware,
|
||||||
} from '@konobangu/security/middleware';
|
} from '@konobangu/security/middleware';
|
||||||
import { NextRequest } from 'next/server';
|
import type { NextRequest } from 'next/server';
|
||||||
|
|
||||||
const securityHeaders = noseconeMiddleware(noseconeConfig);
|
const securityHeaders = noseconeMiddleware(noseconeConfig);
|
||||||
|
|
||||||
export async function middleware (_request: NextRequest) {
|
export async function middleware(_request: NextRequest) {
|
||||||
const response = await securityHeaders();
|
const response = await securityHeaders();
|
||||||
return authMiddleware(response as any);
|
return authMiddleware(response as any);
|
||||||
}
|
}
|
||||||
|
15
apps/recorder/.gitignore
vendored
15
apps/recorder/.gitignore
vendored
@ -15,3 +15,18 @@ Cargo.lock
|
|||||||
|
|
||||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
*.pdb
|
*.pdb
|
||||||
|
|
||||||
|
|
||||||
|
# Local
|
||||||
|
.DS_Store
|
||||||
|
*.local
|
||||||
|
*.log*
|
||||||
|
|
||||||
|
# Dist
|
||||||
|
node_modules
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
@ -99,6 +99,9 @@ quirks_path = "0.1.0"
|
|||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
tower = "0.5.2"
|
tower = "0.5.2"
|
||||||
axum-extra = "0.10.0"
|
axum-extra = "0.10.0"
|
||||||
|
tower-http = "0.6.2"
|
||||||
|
serde_yaml = "0.9.34"
|
||||||
|
tera = "1.20.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serial_test = "3"
|
serial_test = "3"
|
||||||
|
0
apps/recorder/assets/.gitkeep
Normal file
0
apps/recorder/assets/.gitkeep
Normal file
7
apps/recorder/assets/static/404.html
Normal file
7
apps/recorder/assets/static/404.html
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<html>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
not found :-(
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
@ -45,6 +45,7 @@ server:
|
|||||||
enable: false
|
enable: false
|
||||||
# Duration time in milliseconds.
|
# Duration time in milliseconds.
|
||||||
timeout: 5000
|
timeout: 5000
|
||||||
|
|
||||||
cors:
|
cors:
|
||||||
enable: true
|
enable: true
|
||||||
# Set the value of the [`Access-Control-Allow-Origin`][mdn] header
|
# Set the value of the [`Access-Control-Allow-Origin`][mdn] header
|
||||||
@ -114,7 +115,6 @@ redis:
|
|||||||
dangerously_flush: false
|
dangerously_flush: false
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
|
|
||||||
dal:
|
dal:
|
||||||
data_dir: ./data
|
data_dir: ./data
|
||||||
|
|
||||||
|
7
apps/recorder/package.json
Normal file
7
apps/recorder/package.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"name": "recorder",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"altair-static": "^8.1.3"
|
||||||
|
}
|
||||||
|
}
|
@ -1,130 +0,0 @@
|
|||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use loco_rs::{
|
|
||||||
app::{AppContext, Hooks},
|
|
||||||
boot::{create_app, BootResult, StartMode},
|
|
||||||
cache,
|
|
||||||
config::Config,
|
|
||||||
controller::AppRoutes,
|
|
||||||
db::truncate_table,
|
|
||||||
environment::Environment,
|
|
||||||
prelude::*,
|
|
||||||
task::Tasks,
|
|
||||||
Result,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
auth::service::{AppAuthService, AppAuthServiceInitializer},
|
|
||||||
controllers::{self},
|
|
||||||
dal::{AppDalClient, AppDalInitalizer},
|
|
||||||
extract::mikan::{client::AppMikanClientInitializer, AppMikanClient},
|
|
||||||
graphql::service::{AppGraphQLService, AppGraphQLServiceInitializer},
|
|
||||||
migrations::Migrator,
|
|
||||||
models::subscribers,
|
|
||||||
workers::subscription_worker::SubscriptionWorker,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const CONFIG_FOLDER: &str = "LOCO_CONFIG_FOLDER";
|
|
||||||
|
|
||||||
pub trait AppContextExt {
|
|
||||||
fn get_dal_client(&self) -> &AppDalClient {
|
|
||||||
AppDalClient::app_instance()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_mikan_client(&self) -> &AppMikanClient {
|
|
||||||
AppMikanClient::app_instance()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_auth_service(&self) -> &AppAuthService {
|
|
||||||
AppAuthService::app_instance()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_graphql_service(&self) -> &AppGraphQLService {
|
|
||||||
AppGraphQLService::app_instance()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppContextExt for AppContext {}
|
|
||||||
|
|
||||||
pub struct App;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Hooks for App {
|
|
||||||
async fn load_config(env: &Environment) -> Result<Config> {
|
|
||||||
std::env::var(CONFIG_FOLDER).map_or_else(
|
|
||||||
|_| {
|
|
||||||
let monorepo_project_config_dir = Path::new("./apps/recorder/config");
|
|
||||||
if monorepo_project_config_dir.exists() && monorepo_project_config_dir.is_dir() {
|
|
||||||
return env.load_from_folder(monorepo_project_config_dir);
|
|
||||||
}
|
|
||||||
let current_config_dir = Path::new("./config");
|
|
||||||
env.load_from_folder(current_config_dir)
|
|
||||||
},
|
|
||||||
|config_folder| env.load_from_folder(Path::new(&config_folder)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn app_name() -> &'static str {
|
|
||||||
env!("CARGO_CRATE_NAME")
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {
|
|
||||||
let initializers: Vec<Box<dyn Initializer>> = vec![
|
|
||||||
Box::new(AppDalInitalizer),
|
|
||||||
Box::new(AppMikanClientInitializer),
|
|
||||||
Box::new(AppGraphQLServiceInitializer),
|
|
||||||
Box::new(AppAuthServiceInitializer),
|
|
||||||
];
|
|
||||||
|
|
||||||
Ok(initializers)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn app_version() -> String {
|
|
||||||
format!(
|
|
||||||
"{} ({})",
|
|
||||||
env!("CARGO_PKG_VERSION"),
|
|
||||||
option_env!("BUILD_SHA")
|
|
||||||
.or(option_env!("GITHUB_SHA"))
|
|
||||||
.unwrap_or("dev")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn boot(
|
|
||||||
mode: StartMode,
|
|
||||||
environment: &Environment,
|
|
||||||
config: Config,
|
|
||||||
) -> Result<BootResult> {
|
|
||||||
create_app::<Self, Migrator>(mode, environment, config).await
|
|
||||||
}
|
|
||||||
|
|
||||||
fn routes(ctx: &AppContext) -> AppRoutes {
|
|
||||||
AppRoutes::with_default_routes()
|
|
||||||
.prefix("/api")
|
|
||||||
.add_route(controllers::auth::routes())
|
|
||||||
.add_route(controllers::graphql::routes(ctx.clone()))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn connect_workers(ctx: &AppContext, queue: &Queue) -> Result<()> {
|
|
||||||
queue.register(SubscriptionWorker::build(ctx)).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn after_context(ctx: AppContext) -> Result<AppContext> {
|
|
||||||
Ok(AppContext {
|
|
||||||
cache: cache::Cache::new(cache::drivers::inmem::new()).into(),
|
|
||||||
..ctx
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn register_tasks(_tasks: &mut Tasks) {}
|
|
||||||
|
|
||||||
async fn truncate(ctx: &AppContext) -> Result<()> {
|
|
||||||
truncate_table(&ctx.db, subscribers::Entity).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn seed(_ctx: &AppContext, _base: &Path) -> Result<()> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
26
apps/recorder/src/app/ext.rs
Normal file
26
apps/recorder/src/app/ext.rs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
use loco_rs::app::AppContext;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
auth::service::AppAuthService, dal::AppDalClient, extract::mikan::AppMikanClient,
|
||||||
|
graphql::service::AppGraphQLService,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub trait AppContextExt {
|
||||||
|
fn get_dal_client(&self) -> &AppDalClient {
|
||||||
|
AppDalClient::app_instance()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_mikan_client(&self) -> &AppMikanClient {
|
||||||
|
AppMikanClient::app_instance()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_auth_service(&self) -> &AppAuthService {
|
||||||
|
AppAuthService::app_instance()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_graphql_service(&self) -> &AppGraphQLService {
|
||||||
|
AppGraphQLService::app_instance()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppContextExt for AppContext {}
|
174
apps/recorder/src/app/mod.rs
Normal file
174
apps/recorder/src/app/mod.rs
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
pub mod ext;
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
fs,
|
||||||
|
path::{self, Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
pub use ext::AppContextExt;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use loco_rs::{
|
||||||
|
app::{AppContext, Hooks},
|
||||||
|
boot::{create_app, BootResult, StartMode},
|
||||||
|
cache,
|
||||||
|
config::Config,
|
||||||
|
controller::{middleware, middleware::MiddlewareLayer, AppRoutes},
|
||||||
|
db::truncate_table,
|
||||||
|
environment::Environment,
|
||||||
|
prelude::*,
|
||||||
|
task::Tasks,
|
||||||
|
Result,
|
||||||
|
};
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
auth::service::AppAuthServiceInitializer,
|
||||||
|
controllers::{self},
|
||||||
|
dal::AppDalInitalizer,
|
||||||
|
extract::mikan::client::AppMikanClientInitializer,
|
||||||
|
graphql::service::AppGraphQLServiceInitializer,
|
||||||
|
migrations::Migrator,
|
||||||
|
models::subscribers,
|
||||||
|
workers::subscription_worker::SubscriptionWorker,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const WORKING_ROOT_VAR_NAME: &str = "WORKING_ROOT";
|
||||||
|
|
||||||
|
static APP_WORKING_ROOT: OnceCell<quirks_path::PathBuf> = OnceCell::new();
|
||||||
|
|
||||||
|
pub struct App;
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub fn set_working_root(path: PathBuf) {
|
||||||
|
APP_WORKING_ROOT.get_or_init(|| {
|
||||||
|
quirks_path::PathBuf::from(path.as_os_str().to_string_lossy().to_string())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_working_root() -> &'static quirks_path::Path {
|
||||||
|
APP_WORKING_ROOT
|
||||||
|
.get()
|
||||||
|
.map(|p| p.as_path())
|
||||||
|
.expect("working root not set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Hooks for App {
|
||||||
|
fn app_version() -> String {
|
||||||
|
format!(
|
||||||
|
"{} ({})",
|
||||||
|
env!("CARGO_PKG_VERSION"),
|
||||||
|
option_env!("BUILD_SHA")
|
||||||
|
.or(option_env!("GITHUB_SHA"))
|
||||||
|
.unwrap_or("dev")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app_name() -> &'static str {
|
||||||
|
env!("CARGO_CRATE_NAME")
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn boot(
|
||||||
|
mode: StartMode,
|
||||||
|
environment: &Environment,
|
||||||
|
config: Config,
|
||||||
|
) -> Result<BootResult> {
|
||||||
|
create_app::<Self, Migrator>(mode, environment, config).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_config(env: &Environment) -> Result<Config> {
|
||||||
|
let working_roots_to_search = [
|
||||||
|
std::env::var(WORKING_ROOT_VAR_NAME).ok(),
|
||||||
|
Some(String::from("./apps/recorder")),
|
||||||
|
Some(String::from(".")),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.collect_vec();
|
||||||
|
|
||||||
|
for working_root in working_roots_to_search.iter() {
|
||||||
|
let working_root = PathBuf::from(working_root);
|
||||||
|
let config_dir = working_root.as_path().join("config");
|
||||||
|
for config_file in [
|
||||||
|
config_dir.join(format!("{env}.local.yaml")),
|
||||||
|
config_dir.join(format!("{env}.yaml")),
|
||||||
|
] {
|
||||||
|
if config_file.exists() && config_file.is_file() {
|
||||||
|
tracing::info!(config_file =? config_file, "loading environment from");
|
||||||
|
|
||||||
|
let content = fs::read_to_string(config_file.clone())?;
|
||||||
|
let rendered = tera::Tera::one_off(
|
||||||
|
&content,
|
||||||
|
&tera::Context::from_serialize(serde_json::json!({}))?,
|
||||||
|
false,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
App::set_working_root(working_root);
|
||||||
|
|
||||||
|
return serde_yaml::from_str(&rendered).map_err(|err| {
|
||||||
|
loco_rs::Error::YAMLFile(err, config_file.to_string_lossy().to_string())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(loco_rs::Error::Message(format!(
|
||||||
|
"no configuration file found in search paths: {}",
|
||||||
|
working_roots_to_search
|
||||||
|
.iter()
|
||||||
|
.map(|p| path::absolute(PathBuf::from(p)))
|
||||||
|
.flatten()
|
||||||
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
|
.join(",")
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {
|
||||||
|
let initializers: Vec<Box<dyn Initializer>> = vec![
|
||||||
|
Box::new(AppDalInitalizer),
|
||||||
|
Box::new(AppMikanClientInitializer),
|
||||||
|
Box::new(AppGraphQLServiceInitializer),
|
||||||
|
Box::new(AppAuthServiceInitializer),
|
||||||
|
];
|
||||||
|
|
||||||
|
Ok(initializers)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn routes(ctx: &AppContext) -> AppRoutes {
|
||||||
|
AppRoutes::with_default_routes()
|
||||||
|
.prefix("/api")
|
||||||
|
.add_route(controllers::auth::routes())
|
||||||
|
.add_route(controllers::graphql::routes(ctx.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn middlewares(ctx: &AppContext) -> Vec<Box<dyn MiddlewareLayer>> {
|
||||||
|
let mut middlewares = middleware::default_middleware_stack(ctx);
|
||||||
|
middlewares.extend(controllers::graphql::asset_middlewares());
|
||||||
|
middlewares
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn after_context(ctx: AppContext) -> Result<AppContext> {
|
||||||
|
Ok(AppContext {
|
||||||
|
cache: cache::Cache::new(cache::drivers::inmem::new()).into(),
|
||||||
|
..ctx
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn connect_workers(ctx: &AppContext, queue: &Queue) -> Result<()> {
|
||||||
|
queue.register(SubscriptionWorker::build(ctx)).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_tasks(_tasks: &mut Tasks) {}
|
||||||
|
|
||||||
|
async fn truncate(ctx: &AppContext) -> Result<()> {
|
||||||
|
truncate_table(&ctx.db, subscribers::Entity).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn seed(_ctx: &AppContext, _base: &Path) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use axum::http::request::Parts;
|
use axum::http::{request::Parts, HeaderValue};
|
||||||
use base64::{self, Engine};
|
use base64::{self, Engine};
|
||||||
use reqwest::header::AUTHORIZATION;
|
use reqwest::header::AUTHORIZATION;
|
||||||
|
|
||||||
@ -76,4 +76,8 @@ impl AuthService for BasicAuthService {
|
|||||||
}
|
}
|
||||||
Err(AuthError::BasicInvalidCredentials)
|
Err(AuthError::BasicInvalidCredentials)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn www_authenticate_header_value(&self) -> Option<HeaderValue> {
|
||||||
|
Some(HeaderValue::from_static(r#"Basic realm="konobangu""#))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,15 +3,16 @@ use axum::{
|
|||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
Json,
|
Json,
|
||||||
};
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum AuthError {
|
pub enum AuthError {
|
||||||
#[error(transparent)]
|
|
||||||
OidcInitError(#[from] jwt_authorizer::error::InitError),
|
|
||||||
#[error("Invalid credentials")]
|
#[error("Invalid credentials")]
|
||||||
BasicInvalidCredentials,
|
BasicInvalidCredentials,
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
|
OidcInitError(#[from] jwt_authorizer::error::InitError),
|
||||||
|
#[error(transparent)]
|
||||||
OidcJwtAuthError(#[from] jwt_authorizer::AuthError),
|
OidcJwtAuthError(#[from] jwt_authorizer::AuthError),
|
||||||
#[error("Extra scopes {expected} do not match found scopes {found}")]
|
#[error("Extra scopes {expected} do not match found scopes {found}")]
|
||||||
OidcExtraScopesMatchError { expected: String, found: String },
|
OidcExtraScopesMatchError { expected: String, found: String },
|
||||||
@ -29,8 +30,23 @@ pub enum AuthError {
|
|||||||
OidcSubMissingError,
|
OidcSubMissingError,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoResponse for AuthError {
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
fn into_response(self) -> Response {
|
pub struct AuthErrorBody {
|
||||||
(StatusCode::UNAUTHORIZED, Json(self.to_string())).into_response()
|
pub error_code: i32,
|
||||||
|
pub error_msg: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<AuthError> for AuthErrorBody {
|
||||||
|
fn from(value: AuthError) -> Self {
|
||||||
|
AuthErrorBody {
|
||||||
|
error_code: StatusCode::UNAUTHORIZED.as_u16() as i32,
|
||||||
|
error_msg: value.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for AuthError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
(StatusCode::UNAUTHORIZED, Json(AuthErrorBody::from(self))).into_response()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
63
apps/recorder/src/auth/middleware.rs
Normal file
63
apps/recorder/src/auth/middleware.rs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
use axum::{
|
||||||
|
extract::{Request, State},
|
||||||
|
http::header,
|
||||||
|
middleware::Next,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use loco_rs::prelude::AppContext;
|
||||||
|
|
||||||
|
use crate::{app::AppContextExt, auth::AuthService};
|
||||||
|
|
||||||
|
pub async fn api_auth_middleware(
|
||||||
|
State(ctx): State<AppContext>,
|
||||||
|
request: Request,
|
||||||
|
next: Next,
|
||||||
|
) -> Response {
|
||||||
|
let auth_service = ctx.get_auth_service();
|
||||||
|
|
||||||
|
let (mut parts, body) = request.into_parts();
|
||||||
|
|
||||||
|
let mut response = match auth_service.extract_user_info(&mut parts).await {
|
||||||
|
Ok(auth_user_info) => {
|
||||||
|
let mut request = Request::from_parts(parts, body);
|
||||||
|
request.extensions_mut().insert(auth_user_info);
|
||||||
|
next.run(request).await
|
||||||
|
}
|
||||||
|
Err(auth_error) => auth_error.into_response(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(header_value) = auth_service.www_authenticate_header_value() {
|
||||||
|
response
|
||||||
|
.headers_mut()
|
||||||
|
.insert(header::WWW_AUTHENTICATE, header_value);
|
||||||
|
};
|
||||||
|
|
||||||
|
response
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn webui_auth_middleware(
|
||||||
|
State(ctx): State<AppContext>,
|
||||||
|
request: Request,
|
||||||
|
next: Next,
|
||||||
|
) -> Response {
|
||||||
|
let auth_service = ctx.get_auth_service();
|
||||||
|
|
||||||
|
let (mut parts, body) = request.into_parts();
|
||||||
|
|
||||||
|
let mut response = match auth_service.extract_user_info(&mut parts).await {
|
||||||
|
Ok(auth_user_info) => {
|
||||||
|
let mut request = Request::from_parts(parts, body);
|
||||||
|
request.extensions_mut().insert(auth_user_info);
|
||||||
|
next.run(request).await
|
||||||
|
}
|
||||||
|
Err(auth_error) => auth_error.into_response(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(header_value) = auth_service.www_authenticate_header_value() {
|
||||||
|
response
|
||||||
|
.headers_mut()
|
||||||
|
.insert(header::WWW_AUTHENTICATE, header_value);
|
||||||
|
};
|
||||||
|
|
||||||
|
response
|
||||||
|
}
|
@ -1,9 +1,11 @@
|
|||||||
pub mod basic;
|
pub mod basic;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
|
pub mod middleware;
|
||||||
pub mod oidc;
|
pub mod oidc;
|
||||||
pub mod service;
|
pub mod service;
|
||||||
|
|
||||||
pub use config::{AppAuthConfig, BasicAuthConfig, OidcAuthConfig};
|
pub use config::{AppAuthConfig, BasicAuthConfig, OidcAuthConfig};
|
||||||
pub use errors::AuthError;
|
pub use errors::AuthError;
|
||||||
|
pub use middleware::{api_auth_middleware, webui_auth_middleware};
|
||||||
pub use service::{AppAuthService, AuthService, AuthUserInfo};
|
pub use service::{AppAuthService, AuthService, AuthUserInfo};
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use axum::http::request::Parts;
|
use axum::http::{request::Parts, HeaderValue};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use jwt_authorizer::{authorizer::Authorizer, NumericDate, OneOrArray};
|
use jwt_authorizer::{authorizer::Authorizer, NumericDate, OneOrArray};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@ -135,4 +135,8 @@ impl AuthService for OidcAuthService {
|
|||||||
auth_type: AuthType::Oidc,
|
auth_type: AuthType::Oidc,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn www_authenticate_header_value(&self) -> Option<HeaderValue> {
|
||||||
|
Some(HeaderValue::from_static(r#"Bearer realm="konobangu""#))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ use axum::{
|
|||||||
use jwt_authorizer::{JwtAuthorizer, Validation};
|
use jwt_authorizer::{JwtAuthorizer, Validation};
|
||||||
use loco_rs::app::{AppContext, Initializer};
|
use loco_rs::app::{AppContext, Initializer};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
|
use reqwest::header::HeaderValue;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
basic::BasicAuthService,
|
basic::BasicAuthService,
|
||||||
@ -16,6 +17,7 @@ use super::{
|
|||||||
};
|
};
|
||||||
use crate::{app::AppContextExt as _, config::AppConfigExt, models::auth::AuthType};
|
use crate::{app::AppContextExt as _, config::AppConfigExt, models::auth::AuthType};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub struct AuthUserInfo {
|
pub struct AuthUserInfo {
|
||||||
pub user_pid: String,
|
pub user_pid: String,
|
||||||
pub auth_type: AuthType,
|
pub auth_type: AuthType,
|
||||||
@ -40,6 +42,7 @@ impl FromRequestParts<AppContext> for AuthUserInfo {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait AuthService {
|
pub trait AuthService {
|
||||||
async fn extract_user_info(&self, request: &mut Parts) -> Result<AuthUserInfo, AuthError>;
|
async fn extract_user_info(&self, request: &mut Parts) -> Result<AuthUserInfo, AuthError>;
|
||||||
|
fn www_authenticate_header_value(&self) -> Option<HeaderValue>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum AppAuthService {
|
pub enum AppAuthService {
|
||||||
@ -87,6 +90,13 @@ impl AuthService for AppAuthService {
|
|||||||
AppAuthService::Oidc(service) => service.extract_user_info(request).await,
|
AppAuthService::Oidc(service) => service.extract_user_info(request).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn www_authenticate_header_value(&self) -> Option<HeaderValue> {
|
||||||
|
match self {
|
||||||
|
AppAuthService::Basic(service) => service.www_authenticate_header_value(),
|
||||||
|
AppAuthService::Oidc(service) => service.www_authenticate_header_value(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AppAuthServiceInitializer;
|
pub struct AppAuthServiceInitializer;
|
||||||
|
@ -11,5 +11,6 @@ mikan:
|
|||||||
base_url: "https://mikanani.me/"
|
base_url: "https://mikanani.me/"
|
||||||
|
|
||||||
graphql:
|
graphql:
|
||||||
|
playground_static: "./node_modules/altair-static/build/dist"
|
||||||
depth_limit: null
|
depth_limit: null
|
||||||
complexity_limit: null
|
complexity_limit: null
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
use loco_rs::prelude::*;
|
|
||||||
|
|
||||||
use crate::{models::subscribers, views::subscribers::CurrentResponse};
|
|
||||||
|
|
||||||
async fn current(State(ctx): State<AppContext>) -> Result<impl IntoResponse> {
|
|
||||||
let subscriber = subscribers::Model::find_root(&ctx).await?;
|
|
||||||
format::json(CurrentResponse::new(&subscriber))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn routes() -> Routes {
|
|
||||||
Routes::new()
|
|
||||||
.prefix("subscribers")
|
|
||||||
.add("/current", get(current))
|
|
||||||
}
|
|
10
apps/recorder/src/controllers/auth/mod.rs
Normal file
10
apps/recorder/src/controllers/auth/mod.rs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
use axum::response::IntoResponse;
|
||||||
|
use loco_rs::prelude::*;
|
||||||
|
|
||||||
|
async fn current() -> impl IntoResponse {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn routes() -> Routes {
|
||||||
|
Routes::new().prefix("/auth").add("/current", get(current))
|
||||||
|
}
|
@ -1,40 +0,0 @@
|
|||||||
use async_graphql::http::{playground_source, GraphQLPlaygroundConfig};
|
|
||||||
use async_graphql_axum::{GraphQLRequest, GraphQLResponse};
|
|
||||||
use axum::{
|
|
||||||
extract::State,
|
|
||||||
middleware::from_extractor_with_state,
|
|
||||||
response::{Html, IntoResponse},
|
|
||||||
routing::{get, post},
|
|
||||||
};
|
|
||||||
use loco_rs::{app::AppContext, prelude::Routes};
|
|
||||||
|
|
||||||
use crate::{app::AppContextExt, auth::AuthUserInfo};
|
|
||||||
|
|
||||||
async fn graphql_playground() -> impl IntoResponse {
|
|
||||||
Html(playground_source(GraphQLPlaygroundConfig::new(
|
|
||||||
"/api/graphql",
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn graphql_handler(
|
|
||||||
State(ctx): State<AppContext>,
|
|
||||||
auth_user_info: AuthUserInfo,
|
|
||||||
req: GraphQLRequest,
|
|
||||||
) -> GraphQLResponse {
|
|
||||||
let graphql_service = ctx.get_graphql_service();
|
|
||||||
let mut req = req.into_inner();
|
|
||||||
req = req.data(auth_user_info);
|
|
||||||
|
|
||||||
graphql_service.schema.execute(req).await.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn routes(state: AppContext) -> Routes {
|
|
||||||
Routes::new()
|
|
||||||
.prefix("/graphql")
|
|
||||||
.add("/playground", get(graphql_playground))
|
|
||||||
.add(
|
|
||||||
"/",
|
|
||||||
post(graphql_handler)
|
|
||||||
.layer(from_extractor_with_state::<AuthUserInfo, AppContext>(state)),
|
|
||||||
)
|
|
||||||
}
|
|
71
apps/recorder/src/controllers/graphql/mod.rs
Normal file
71
apps/recorder/src/controllers/graphql/mod.rs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
pub mod playground;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use async_graphql_axum::{GraphQLRequest, GraphQLResponse};
|
||||||
|
use axum::{
|
||||||
|
extract::State,
|
||||||
|
http::HeaderMap,
|
||||||
|
middleware::from_fn_with_state,
|
||||||
|
response::Html,
|
||||||
|
routing::{get, post},
|
||||||
|
Extension,
|
||||||
|
};
|
||||||
|
use loco_rs::{app::AppContext, controller::middleware::MiddlewareLayer, prelude::Routes};
|
||||||
|
use playground::{altair_graphql_playground_asset_middleware, AltairGraphQLPlayground};
|
||||||
|
use reqwest::header;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::AppContextExt,
|
||||||
|
auth::{api_auth_middleware, webui_auth_middleware, AuthUserInfo},
|
||||||
|
};
|
||||||
|
|
||||||
|
async fn graphql_playground(header_map: HeaderMap) -> loco_rs::Result<Html<String>> {
|
||||||
|
let mut playground_config = AltairGraphQLPlayground::new("/api/graphql");
|
||||||
|
|
||||||
|
if let Some(authorization) = header_map.get(header::AUTHORIZATION) {
|
||||||
|
if let Ok(authorization) = authorization.to_str() {
|
||||||
|
playground_config.initial_headers = {
|
||||||
|
let mut m = HashMap::new();
|
||||||
|
m.insert(header::AUTHORIZATION.to_string(), authorization.to_string());
|
||||||
|
Some(m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = Html(playground_config.render("/api/graphql/playground/static/")?);
|
||||||
|
|
||||||
|
Ok(html)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn graphql_handler(
|
||||||
|
State(ctx): State<AppContext>,
|
||||||
|
Extension(auth_user_info): Extension<AuthUserInfo>,
|
||||||
|
req: GraphQLRequest,
|
||||||
|
) -> GraphQLResponse {
|
||||||
|
let graphql_service = ctx.get_graphql_service();
|
||||||
|
|
||||||
|
let mut req = req.into_inner();
|
||||||
|
req = req.data(auth_user_info);
|
||||||
|
|
||||||
|
graphql_service.schema.execute(req).await.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn routes(state: AppContext) -> Routes {
|
||||||
|
Routes::new()
|
||||||
|
.prefix("/graphql")
|
||||||
|
.add(
|
||||||
|
"/playground",
|
||||||
|
get(graphql_playground).layer(from_fn_with_state(state.clone(), webui_auth_middleware)),
|
||||||
|
)
|
||||||
|
.add(
|
||||||
|
"/",
|
||||||
|
post(graphql_handler).layer(from_fn_with_state(state, api_auth_middleware)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn asset_middlewares() -> Vec<Box<dyn MiddlewareLayer>> {
|
||||||
|
vec![Box::new(altair_graphql_playground_asset_middleware(
|
||||||
|
"/api/graphql/playground/static/",
|
||||||
|
))]
|
||||||
|
}
|
74
apps/recorder/src/controllers/graphql/playground.rs
Normal file
74
apps/recorder/src/controllers/graphql/playground.rs
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use loco_rs::controller::middleware::static_assets::{FolderConfig, StaticAssets};
|
||||||
|
use regex::Regex;
|
||||||
|
use serde::Serialize;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::app::App;
|
||||||
|
|
||||||
|
const ALTAIR_GRAPHQL_HTML: &'static str =
|
||||||
|
include_str!("../../../node_modules/altair-static/build/dist/index.html");
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref ALTAIR_GRAPHQL_BASE_REGEX: Regex = Regex::new(r"<base.*>").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AltairGraphQLPlayground<'a> {
|
||||||
|
#[serde(rename = "endpointURL")]
|
||||||
|
pub endpoint_url: &'a str,
|
||||||
|
/**
|
||||||
|
* URL to set as the subscription endpoint. This can be relative or
|
||||||
|
* absolute.
|
||||||
|
*/
|
||||||
|
pub subscriptions_endpoint: Option<&'a str>,
|
||||||
|
pub initial_headers: Option<HashMap<String, String>>,
|
||||||
|
pub initial_settings: Option<HashMap<String, Value>>,
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub other: Option<HashMap<String, Value>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> AltairGraphQLPlayground<'a> {
|
||||||
|
/// Create a config for GraphQL playground.
|
||||||
|
pub fn new(endpoint_url: &'a str) -> Self {
|
||||||
|
Self {
|
||||||
|
endpoint_url,
|
||||||
|
subscriptions_endpoint: Default::default(),
|
||||||
|
initial_headers: Default::default(),
|
||||||
|
initial_settings: Default::default(),
|
||||||
|
other: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(&self, base_url: &str) -> loco_rs::Result<String> {
|
||||||
|
let option = serde_json::to_string(self)?;
|
||||||
|
let render_str = ALTAIR_GRAPHQL_BASE_REGEX
|
||||||
|
.replace(ALTAIR_GRAPHQL_HTML, format!(r#"<base href="{base_url}">"#))
|
||||||
|
.replace(
|
||||||
|
"</body>",
|
||||||
|
&format!("<script>AltairGraphQL.init({});</script></body>", option),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(render_str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn altair_graphql_playground_asset_middleware(base_url: &str) -> StaticAssets {
|
||||||
|
StaticAssets {
|
||||||
|
enable: true,
|
||||||
|
must_exist: true,
|
||||||
|
folder: FolderConfig {
|
||||||
|
uri: String::from(base_url),
|
||||||
|
path: App::get_working_root()
|
||||||
|
.join("node_modules/altair-static/build/dist")
|
||||||
|
.into(),
|
||||||
|
},
|
||||||
|
fallback: App::get_working_root()
|
||||||
|
.join("assets/static/404.html")
|
||||||
|
.into(),
|
||||||
|
precompressed: false,
|
||||||
|
}
|
||||||
|
}
|
@ -11,7 +11,7 @@ use url::Url;
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::AppDalConfig;
|
use super::AppDalConfig;
|
||||||
use crate::config::AppConfigExt;
|
use crate::{app::App, config::AppConfigExt};
|
||||||
|
|
||||||
// TODO: wait app-context-trait to integrate
|
// TODO: wait app-context-trait to integrate
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
@ -52,12 +52,16 @@ impl fmt::Display for DalStoredUrl {
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct AppDalClient {
|
pub struct AppDalClient {
|
||||||
pub config: AppDalConfig,
|
pub data_dir: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppDalClient {
|
impl AppDalClient {
|
||||||
pub fn new(config: AppDalConfig) -> Self {
|
pub fn new(config: AppDalConfig) -> Self {
|
||||||
Self { config }
|
Self {
|
||||||
|
data_dir: App::get_working_root()
|
||||||
|
.join(config.data_dir.as_deref().unwrap_or("./data"))
|
||||||
|
.to_string(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn app_instance() -> &'static AppDalClient {
|
pub fn app_instance() -> &'static AppDalClient {
|
||||||
@ -67,13 +71,7 @@ impl AppDalClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_fs(&self) -> Fs {
|
pub fn get_fs(&self) -> Fs {
|
||||||
Fs::default().root(
|
Fs::default().root(&self.data_dir)
|
||||||
self.config
|
|
||||||
.data_dir
|
|
||||||
.as_ref()
|
|
||||||
.map(|s| s as &str)
|
|
||||||
.unwrap_or("./data"),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_filename(extname: &str) -> String {
|
pub fn create_filename(extname: &str) -> String {
|
||||||
|
@ -31,7 +31,6 @@
|
|||||||
"import-in-the-middle": "^1.11.3",
|
"import-in-the-middle": "^1.11.3",
|
||||||
"lucide-react": "^0.468.0",
|
"lucide-react": "^0.468.0",
|
||||||
"mdx-bundler": "^10.0.3",
|
"mdx-bundler": "^10.0.3",
|
||||||
"next": "^15.1.3",
|
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-wrap-balancer": "^1.1.1",
|
"react-wrap-balancer": "^1.1.1",
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
"node": ">=22"
|
"node": ">=22"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.4.1",
|
||||||
"commander": "^12.1.0"
|
"commander": "^12.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -40,7 +40,7 @@
|
|||||||
"shx": "^0.3.4",
|
"shx": "^0.3.4",
|
||||||
"tsx": "^4.19.2",
|
"tsx": "^4.19.2",
|
||||||
"turbo": "^2.3.3",
|
"turbo": "^2.3.3",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.3",
|
||||||
"ultracite": "^4.1.12"
|
"ultracite": "^4.1.14"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,14 +8,14 @@
|
|||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"incremental": false,
|
"incremental": false,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"lib": ["es2022", "DOM", "DOM.Iterable"],
|
"lib": ["es2024", "DOM", "DOM.Iterable"],
|
||||||
"module": "NodeNext",
|
"module": "NodeNext",
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"moduleResolution": "nodenext",
|
"moduleResolution": "nodenext",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"target": "ES2022",
|
"target": "ES2020",
|
||||||
"strictNullChecks": true
|
"strictNullChecks": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
808
pnpm-lock.yaml
generated
808
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user