diff --git a/.editorconfig b/.editorconfig
index d8e085a..9571781 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -2,7 +2,7 @@ root = true
[*]
indent_style = space
-indent_size = 2
+indent_size = 4
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
diff --git a/Cargo.lock b/Cargo.lock
index e8ba6f9..fd525f0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3298,8 +3298,6 @@ dependencies = [
[[package]]
name = "loco-gen"
version = "0.14.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef868bd2df99c949018850b36fb700bba01b10001715f94390bcdb81f412f874"
dependencies = [
"chrono",
"clap",
@@ -3318,8 +3316,6 @@ dependencies = [
[[package]]
name = "loco-rs"
version = "0.14.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2250c89f0f996c3493ec3d2588a2d63e2861a48df7b9585cb28fbf6faf15a1a0"
dependencies = [
"argon2",
"async-trait",
@@ -4621,12 +4617,15 @@ dependencies = [
"serde",
"serde_json",
"serde_with",
+ "serde_yaml",
"serial_test",
+ "tera",
"testcontainers",
"testcontainers-modules",
"thiserror 2.0.10",
"tokio",
"tower 0.5.2",
+ "tower-http",
"tracing",
"tracing-subscriber",
"url",
diff --git a/Cargo.toml b/Cargo.toml
index 7c05fad..0034f94 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,6 +6,7 @@ resolver = "2"
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/loco-rs/loco.git" }
+loco-rs = { path = "./patches/loco" }
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" }
jwt-authorizer = { git = "https://github.com/blablacio/jwt-authorizer.git", rev = "e956774" }
diff --git a/apps/app/middleware.ts b/apps/app/middleware.ts
index 11fede4..9f463a1 100644
--- a/apps/app/middleware.ts
+++ b/apps/app/middleware.ts
@@ -3,11 +3,11 @@ import {
noseconeConfig,
noseconeMiddleware,
} from '@konobangu/security/middleware';
-import { NextRequest } from 'next/server';
+import type { NextRequest } from 'next/server';
const securityHeaders = noseconeMiddleware(noseconeConfig);
-export async function middleware (_request: NextRequest) {
+export async function middleware(_request: NextRequest) {
const response = await securityHeaders();
return authMiddleware(response as any);
}
diff --git a/apps/recorder/.gitignore b/apps/recorder/.gitignore
index 8861634..3a6a884 100644
--- a/apps/recorder/.gitignore
+++ b/apps/recorder/.gitignore
@@ -15,3 +15,18 @@ Cargo.lock
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
+
+
+# Local
+.DS_Store
+*.local
+*.log*
+
+# Dist
+node_modules
+dist/
+
+# IDE
+.vscode/*
+!.vscode/extensions.json
+.idea
diff --git a/apps/recorder/Cargo.toml b/apps/recorder/Cargo.toml
index 0909b07..3075aa1 100644
--- a/apps/recorder/Cargo.toml
+++ b/apps/recorder/Cargo.toml
@@ -99,6 +99,9 @@ quirks_path = "0.1.0"
base64 = "0.22.1"
tower = "0.5.2"
axum-extra = "0.10.0"
+tower-http = "0.6.2"
+serde_yaml = "0.9.34"
+tera = "1.20.0"
[dev-dependencies]
serial_test = "3"
diff --git a/apps/recorder/assets/.gitkeep b/apps/recorder/assets/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/apps/recorder/assets/static/404.html b/apps/recorder/assets/static/404.html
new file mode 100644
index 0000000..dbd7df4
--- /dev/null
+++ b/apps/recorder/assets/static/404.html
@@ -0,0 +1,7 @@
+
+
+
+ not found :-(
+
+
+
\ No newline at end of file
diff --git a/apps/recorder/config/development.yaml b/apps/recorder/config/development.yaml
index f56ba2d..2932a9b 100644
--- a/apps/recorder/config/development.yaml
+++ b/apps/recorder/config/development.yaml
@@ -2,137 +2,137 @@
# Application logging configuration
logger:
- # Enable or disable logging.
- enable: true
- # Enable pretty backtrace (sets RUST_BACKTRACE=1)
- pretty_backtrace: true
- # Log level, options: trace, debug, info, warn or error.
- level: debug
- # Define the logging format. options: compact, pretty or Json
- format: compact
- # By default the logger has filtering only logs that came from your code or logs that came from `loco` framework. to see all third party libraries
- # Uncomment the line below to override to see all third party libraries you can enable this config and override the logger filters.
- # override_filter: trace
+ # Enable or disable logging.
+ enable: true
+ # Enable pretty backtrace (sets RUST_BACKTRACE=1)
+ pretty_backtrace: true
+ # Log level, options: trace, debug, info, warn or error.
+ level: debug
+ # Define the logging format. options: compact, pretty or Json
+ format: compact
+ # By default the logger has filtering only logs that came from your code or logs that came from `loco` framework. to see all third party libraries
+ # Uncomment the line below to override to see all third party libraries you can enable this config and override the logger filters.
+ # override_filter: trace
# Web server configuration
server:
- # Port on which the server will listen. the server binding is 0.0.0.0:{PORT}
- port: 5001
- # The UI hostname or IP address that mailers will point to.
- host: http://webui.konobangu.com
- # Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block
- middlewares:
- # Enable Etag cache header middleware
- etag:
- enable: true
- # Allows to limit the payload size request. payload that bigger than this file will blocked the request.
- limit_payload:
- # Enable/Disable the middleware.
- enable: true
- # the limit size. can be b,kb,kib,mb,mib,gb,gib
- body_limit: 5mb
- # Generating a unique request ID and enhancing logging with additional information such as the start and completion of request processing, latency, status code, and other request details.
- logger:
- # Enable/Disable the middleware.
- enable: true
- # when your code is panicked, the request still returns 500 status code.
- catch_panic:
- # Enable/Disable the middleware.
- enable: true
- # Timeout for incoming requests middleware. requests that take more time from the configuration will cute and 408 status code will returned.
- timeout_request:
- # Enable/Disable the middleware.
- enable: false
- # Duration time in milliseconds.
- timeout: 5000
- cors:
- enable: true
- # Set the value of the [`Access-Control-Allow-Origin`][mdn] header
- # allow_origins:
- # - https://loco.rs
- # Set the value of the [`Access-Control-Allow-Headers`][mdn] header
- # allow_headers:
- # - Content-Type
- # Set the value of the [`Access-Control-Allow-Methods`][mdn] header
- # allow_methods:
- # - POST
- # Set the value of the [`Access-Control-Max-Age`][mdn] header in seconds
- # max_age: 3600
- fallback:
- enable: false
+ # Port on which the server will listen. the server binding is 0.0.0.0:{PORT}
+ port: 5001
+ # The UI hostname or IP address that mailers will point to.
+ host: http://webui.konobangu.com
+ # Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block
+ middlewares:
+ # Enable Etag cache header middleware
+ etag:
+ enable: true
+ # Allows to limit the payload size request. payload that bigger than this file will blocked the request.
+ limit_payload:
+ # Enable/Disable the middleware.
+ enable: true
+ # the limit size. can be b,kb,kib,mb,mib,gb,gib
+ body_limit: 5mb
+ # Generating a unique request ID and enhancing logging with additional information such as the start and completion of request processing, latency, status code, and other request details.
+ logger:
+ # Enable/Disable the middleware.
+ enable: true
+ # when your code is panicked, the request still returns 500 status code.
+ catch_panic:
+ # Enable/Disable the middleware.
+ enable: true
+ # Timeout for incoming requests middleware. requests that take more time from the configuration will cute and 408 status code will returned.
+ timeout_request:
+ # Enable/Disable the middleware.
+ enable: false
+ # Duration time in milliseconds.
+ timeout: 5000
+
+ cors:
+ enable: true
+ # Set the value of the [`Access-Control-Allow-Origin`][mdn] header
+ # allow_origins:
+ # - https://loco.rs
+ # Set the value of the [`Access-Control-Allow-Headers`][mdn] header
+ # allow_headers:
+ # - Content-Type
+ # Set the value of the [`Access-Control-Allow-Methods`][mdn] header
+ # allow_methods:
+ # - POST
+ # Set the value of the [`Access-Control-Max-Age`][mdn] header in seconds
+ # max_age: 3600
+ fallback:
+ enable: false
# Worker Configuration
workers:
- # specifies the worker mode. Options:
- # - BackgroundQueue - Workers operate asynchronously in the background, processing queued.
- # - ForegroundBlocking - Workers operate in the foreground and block until tasks are completed.
- # - BackgroundAsync - Workers operate asynchronously in the background, processing tasks with async capabilities.
- mode: BackgroundQueue
+ # specifies the worker mode. Options:
+ # - BackgroundQueue - Workers operate asynchronously in the background, processing queued.
+ # - ForegroundBlocking - Workers operate in the foreground and block until tasks are completed.
+ # - BackgroundAsync - Workers operate asynchronously in the background, processing tasks with async capabilities.
+ mode: BackgroundQueue
# Mailer Configuration.
mailer:
- # SMTP mailer configuration.
- smtp:
- # Enable/Disable smtp mailer.
- enable: true
- # SMTP server host. e.x localhost, smtp.gmail.com
- host: '{{ get_env(name="MAILER_HOST", default="localhost") }}'
- # SMTP server port
- port: 1025
- # Use secure connection (SSL/TLS).
- secure: false
- # auth:
- # user:
- # password:
+ # SMTP mailer configuration.
+ smtp:
+ # Enable/Disable smtp mailer.
+ enable: true
+ # SMTP server host. e.x localhost, smtp.gmail.com
+ host: '{{ get_env(name="MAILER_HOST", default="localhost") }}'
+ # SMTP server port
+ port: 1025
+ # Use secure connection (SSL/TLS).
+ secure: false
+ # auth:
+ # user:
+ # password:
# Database Configuration
database:
- # Database connection URI
- uri: '{{ get_env(name="DATABASE_URL", default="postgres://konobangu:konobangu@127.0.0.1:5432/konobangu") }}'
- # When enabled, the sql query will be logged.
- enable_logging: true
- # Set the timeout duration when acquiring a connection.
- connect_timeout: 500
- # Set the idle duration before closing a connection.
- idle_timeout: 500
- # Minimum number of connections for a pool.
- min_connections: 1
- # Maximum number of connections for a pool.
- max_connections: 1
- # Run migration up when application loaded
- auto_migrate: true
- # Truncate database when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode
- dangerously_truncate: false
- # Recreating schema when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode
- dangerously_recreate: false
+ # Database connection URI
+ uri: '{{ get_env(name="DATABASE_URL", default="postgres://konobangu:konobangu@127.0.0.1:5432/konobangu") }}'
+ # When enabled, the sql query will be logged.
+ enable_logging: true
+ # Set the timeout duration when acquiring a connection.
+ connect_timeout: 500
+ # Set the idle duration before closing a connection.
+ idle_timeout: 500
+ # Minimum number of connections for a pool.
+ min_connections: 1
+ # Maximum number of connections for a pool.
+ max_connections: 1
+ # Run migration up when application loaded
+ auto_migrate: true
+ # Truncate database when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode
+ dangerously_truncate: false
+ # Recreating schema when application loaded. This is a dangerous operation, make sure that you using this flag only on dev environments or test mode
+ dangerously_recreate: false
# Redis Configuration
redis:
- # Redis connection URI
- uri: '{{ get_env(name="REDIS_URL", default="redis://127.0.0.1:6379") }}'
- # Dangerously flush all data in Redis on startup. dangerous operation, make sure that you using this flag only on dev environments or test mode
- dangerously_flush: false
+ # Redis connection URI
+ uri: '{{ get_env(name="REDIS_URL", default="redis://127.0.0.1:6379") }}'
+ # Dangerously flush all data in Redis on startup. dangerous operation, make sure that you using this flag only on dev environments or test mode
+ dangerously_flush: false
settings:
+ dal:
+ data_dir: ./data
- dal:
- data_dir: ./data
+ mikan:
+ base_url: "https://mikanani.me/"
+ http_client:
+ exponential_backoff_max_retries: 3
+ leaky_bucket_max_tokens: 2
+ leaky_bucket_initial_tokens: 0
+ leaky_bucket_refill_tokens: 1
+ leaky_bucket_refill_interval: 500
- mikan:
- base_url: "https://mikanani.me/"
- http_client:
- exponential_backoff_max_retries: 3
- leaky_bucket_max_tokens: 2
- leaky_bucket_initial_tokens: 0
- leaky_bucket_refill_tokens: 1
- leaky_bucket_refill_interval: 500
-
- auth:
- auth_type: "oidc" # or "basic"
- basic_user: "konobangu"
- basic_password: "konobangu"
- oidc_api_issuer: "https://some-oidc-auth.com/oidc"
- oidc_api_audience: "https://konobangu.com/api"
- oidc_extra_scopes: "read:konobangu,write:konobangu"
- oidc_extra_claim_key: ""
- oidc_extra_claim_value: ""
+ auth:
+ auth_type: "oidc" # or "basic"
+ basic_user: "konobangu"
+ basic_password: "konobangu"
+ oidc_api_issuer: "https://some-oidc-auth.com/oidc"
+ oidc_api_audience: "https://konobangu.com/api"
+ oidc_extra_scopes: "read:konobangu,write:konobangu"
+ oidc_extra_claim_key: ""
+ oidc_extra_claim_value: ""
diff --git a/apps/recorder/package.json b/apps/recorder/package.json
new file mode 100644
index 0000000..8c53afb
--- /dev/null
+++ b/apps/recorder/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "recorder",
+ "version": "1.0.0",
+ "dependencies": {
+ "altair-static": "^8.1.3"
+ }
+}
diff --git a/apps/recorder/src/app.rs b/apps/recorder/src/app.rs
deleted file mode 100644
index 9c4261d..0000000
--- a/apps/recorder/src/app.rs
+++ /dev/null
@@ -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 {
- 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>> {
- let initializers: Vec> = 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 {
- create_app::(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 {
- 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(())
- }
-}
diff --git a/apps/recorder/src/app/ext.rs b/apps/recorder/src/app/ext.rs
new file mode 100644
index 0000000..0057abd
--- /dev/null
+++ b/apps/recorder/src/app/ext.rs
@@ -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 {}
diff --git a/apps/recorder/src/app/mod.rs b/apps/recorder/src/app/mod.rs
new file mode 100644
index 0000000..52bcb91
--- /dev/null
+++ b/apps/recorder/src/app/mod.rs
@@ -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 = 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 {
+ create_app::(mode, environment, config).await
+ }
+
+ async fn load_config(env: &Environment) -> Result {
+ 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>> {
+ let initializers: Vec> = 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> {
+ let mut middlewares = middleware::default_middleware_stack(ctx);
+ middlewares.extend(controllers::graphql::asset_middlewares());
+ middlewares
+ }
+
+ async fn after_context(ctx: AppContext) -> Result {
+ 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(())
+ }
+}
diff --git a/apps/recorder/src/auth/basic.rs b/apps/recorder/src/auth/basic.rs
index c90c70a..e1eccf3 100644
--- a/apps/recorder/src/auth/basic.rs
+++ b/apps/recorder/src/auth/basic.rs
@@ -1,5 +1,5 @@
use async_trait::async_trait;
-use axum::http::request::Parts;
+use axum::http::{request::Parts, HeaderValue};
use base64::{self, Engine};
use reqwest::header::AUTHORIZATION;
@@ -76,4 +76,8 @@ impl AuthService for BasicAuthService {
}
Err(AuthError::BasicInvalidCredentials)
}
+
+ fn www_authenticate_header_value(&self) -> Option {
+ Some(HeaderValue::from_static(r#"Basic realm="konobangu""#))
+ }
}
diff --git a/apps/recorder/src/auth/errors.rs b/apps/recorder/src/auth/errors.rs
index 358ede6..0fa2798 100644
--- a/apps/recorder/src/auth/errors.rs
+++ b/apps/recorder/src/auth/errors.rs
@@ -3,15 +3,16 @@ use axum::{
response::{IntoResponse, Response},
Json,
};
+use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum AuthError {
- #[error(transparent)]
- OidcInitError(#[from] jwt_authorizer::error::InitError),
#[error("Invalid credentials")]
BasicInvalidCredentials,
#[error(transparent)]
+ OidcInitError(#[from] jwt_authorizer::error::InitError),
+ #[error(transparent)]
OidcJwtAuthError(#[from] jwt_authorizer::AuthError),
#[error("Extra scopes {expected} do not match found scopes {found}")]
OidcExtraScopesMatchError { expected: String, found: String },
@@ -29,8 +30,23 @@ pub enum AuthError {
OidcSubMissingError,
}
-impl IntoResponse for AuthError {
- fn into_response(self) -> Response {
- (StatusCode::UNAUTHORIZED, Json(self.to_string())).into_response()
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct AuthErrorBody {
+ pub error_code: i32,
+ pub error_msg: String,
+}
+
+impl From 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()
}
}
diff --git a/apps/recorder/src/auth/middleware.rs b/apps/recorder/src/auth/middleware.rs
new file mode 100644
index 0000000..8a5e886
--- /dev/null
+++ b/apps/recorder/src/auth/middleware.rs
@@ -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,
+ 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,
+ 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
+}
diff --git a/apps/recorder/src/auth/mod.rs b/apps/recorder/src/auth/mod.rs
index 73d162f..ae46bde 100644
--- a/apps/recorder/src/auth/mod.rs
+++ b/apps/recorder/src/auth/mod.rs
@@ -1,9 +1,11 @@
pub mod basic;
pub mod config;
pub mod errors;
+pub mod middleware;
pub mod oidc;
pub mod service;
pub use config::{AppAuthConfig, BasicAuthConfig, OidcAuthConfig};
pub use errors::AuthError;
+pub use middleware::{api_auth_middleware, webui_auth_middleware};
pub use service::{AppAuthService, AuthService, AuthUserInfo};
diff --git a/apps/recorder/src/auth/oidc.rs b/apps/recorder/src/auth/oidc.rs
index d6dd605..2dff393 100644
--- a/apps/recorder/src/auth/oidc.rs
+++ b/apps/recorder/src/auth/oidc.rs
@@ -1,7 +1,7 @@
use std::collections::{HashMap, HashSet};
use async_trait::async_trait;
-use axum::http::request::Parts;
+use axum::http::{request::Parts, HeaderValue};
use itertools::Itertools;
use jwt_authorizer::{authorizer::Authorizer, NumericDate, OneOrArray};
use serde::{Deserialize, Serialize};
@@ -135,4 +135,8 @@ impl AuthService for OidcAuthService {
auth_type: AuthType::Oidc,
})
}
+
+ fn www_authenticate_header_value(&self) -> Option {
+ Some(HeaderValue::from_static(r#"Bearer realm="konobangu""#))
+ }
}
diff --git a/apps/recorder/src/auth/service.rs b/apps/recorder/src/auth/service.rs
index ff47446..66ca719 100644
--- a/apps/recorder/src/auth/service.rs
+++ b/apps/recorder/src/auth/service.rs
@@ -7,6 +7,7 @@ use axum::{
use jwt_authorizer::{JwtAuthorizer, Validation};
use loco_rs::app::{AppContext, Initializer};
use once_cell::sync::OnceCell;
+use reqwest::header::HeaderValue;
use super::{
basic::BasicAuthService,
@@ -16,6 +17,7 @@ use super::{
};
use crate::{app::AppContextExt as _, config::AppConfigExt, models::auth::AuthType};
+#[derive(Clone, Debug)]
pub struct AuthUserInfo {
pub user_pid: String,
pub auth_type: AuthType,
@@ -40,6 +42,7 @@ impl FromRequestParts for AuthUserInfo {
#[async_trait]
pub trait AuthService {
async fn extract_user_info(&self, request: &mut Parts) -> Result;
+ fn www_authenticate_header_value(&self) -> Option;
}
pub enum AppAuthService {
@@ -87,6 +90,13 @@ impl AuthService for AppAuthService {
AppAuthService::Oidc(service) => service.extract_user_info(request).await,
}
}
+
+ fn www_authenticate_header_value(&self) -> Option {
+ match self {
+ AppAuthService::Basic(service) => service.www_authenticate_header_value(),
+ AppAuthService::Oidc(service) => service.www_authenticate_header_value(),
+ }
+ }
}
pub struct AppAuthServiceInitializer;
diff --git a/apps/recorder/src/config/settings_mixin.yaml b/apps/recorder/src/config/settings_mixin.yaml
index 4a7621d..c33b94e 100644
--- a/apps/recorder/src/config/settings_mixin.yaml
+++ b/apps/recorder/src/config/settings_mixin.yaml
@@ -1,15 +1,16 @@
dal:
- data_dir: ./data
+ data_dir: ./data
mikan:
- http_client:
- exponential_backoff_max_retries: 3
- leaky_bucket_max_tokens: 2
- leaky_bucket_initial_tokens: 0
- leaky_bucket_refill_tokens: 1
- leaky_bucket_refill_interval: 500
- base_url: "https://mikanani.me/"
+ http_client:
+ exponential_backoff_max_retries: 3
+ leaky_bucket_max_tokens: 2
+ leaky_bucket_initial_tokens: 0
+ leaky_bucket_refill_tokens: 1
+ leaky_bucket_refill_interval: 500
+ base_url: "https://mikanani.me/"
graphql:
- depth_limit: null
- complexity_limit: null
+ playground_static: "./node_modules/altair-static/build/dist"
+ depth_limit: null
+ complexity_limit: null
diff --git a/apps/recorder/src/controllers/auth.rs b/apps/recorder/src/controllers/auth.rs
deleted file mode 100644
index f4e3484..0000000
--- a/apps/recorder/src/controllers/auth.rs
+++ /dev/null
@@ -1,14 +0,0 @@
-use loco_rs::prelude::*;
-
-use crate::{models::subscribers, views::subscribers::CurrentResponse};
-
-async fn current(State(ctx): State) -> Result {
- 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))
-}
diff --git a/apps/recorder/src/controllers/auth/mod.rs b/apps/recorder/src/controllers/auth/mod.rs
new file mode 100644
index 0000000..80ff4c0
--- /dev/null
+++ b/apps/recorder/src/controllers/auth/mod.rs
@@ -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))
+}
diff --git a/apps/recorder/src/controllers/graphql.rs b/apps/recorder/src/controllers/graphql.rs
deleted file mode 100644
index e349e7d..0000000
--- a/apps/recorder/src/controllers/graphql.rs
+++ /dev/null
@@ -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,
- 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::(state)),
- )
-}
diff --git a/apps/recorder/src/controllers/graphql/mod.rs b/apps/recorder/src/controllers/graphql/mod.rs
new file mode 100644
index 0000000..ab05e5b
--- /dev/null
+++ b/apps/recorder/src/controllers/graphql/mod.rs
@@ -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> {
+ 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,
+ Extension(auth_user_info): Extension,
+ 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> {
+ vec![Box::new(altair_graphql_playground_asset_middleware(
+ "/api/graphql/playground/static/",
+ ))]
+}
diff --git a/apps/recorder/src/controllers/graphql/playground.rs b/apps/recorder/src/controllers/graphql/playground.rs
new file mode 100644
index 0000000..0b539f2
--- /dev/null
+++ b/apps/recorder/src/controllers/graphql/playground.rs
@@ -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"").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>,
+ pub initial_settings: Option>,
+ #[serde(flatten)]
+ pub other: Option>,
+}
+
+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 {
+ let option = serde_json::to_string(self)?;
+ let render_str = ALTAIR_GRAPHQL_BASE_REGEX
+ .replace(ALTAIR_GRAPHQL_HTML, format!(r#""#))
+ .replace(
+ "",
+ &format!("