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>
 | 
				
			||||||
@ -2,137 +2,137 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Application logging configuration
 | 
					# Application logging configuration
 | 
				
			||||||
logger:
 | 
					logger:
 | 
				
			||||||
  # Enable or disable logging.
 | 
					    # Enable or disable logging.
 | 
				
			||||||
  enable: true
 | 
					    enable: true
 | 
				
			||||||
  # Enable pretty backtrace (sets RUST_BACKTRACE=1)
 | 
					    # Enable pretty backtrace (sets RUST_BACKTRACE=1)
 | 
				
			||||||
  pretty_backtrace: true
 | 
					    pretty_backtrace: true
 | 
				
			||||||
  # Log level, options: trace, debug, info, warn or error.
 | 
					    # Log level, options: trace, debug, info, warn or error.
 | 
				
			||||||
  level: debug
 | 
					    level: debug
 | 
				
			||||||
  # Define the logging format. options: compact, pretty or Json
 | 
					    # Define the logging format. options: compact, pretty or Json
 | 
				
			||||||
  format: compact
 | 
					    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
 | 
					    # 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.
 | 
					    # 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
 | 
					    # override_filter: trace
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Web server configuration
 | 
					# Web server configuration
 | 
				
			||||||
server:
 | 
					server:
 | 
				
			||||||
  # Port on which the server will listen. the server binding is 0.0.0.0:{PORT}
 | 
					    # Port on which the server will listen. the server binding is 0.0.0.0:{PORT}
 | 
				
			||||||
  port: 5001
 | 
					    port: 5001
 | 
				
			||||||
  # The UI hostname or IP address that mailers will point to.
 | 
					    # The UI hostname or IP address that mailers will point to.
 | 
				
			||||||
  host: http://webui.konobangu.com
 | 
					    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
 | 
					    # Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block
 | 
				
			||||||
  middlewares:
 | 
					    middlewares:
 | 
				
			||||||
    # Enable Etag cache header middleware
 | 
					        # Enable Etag cache header middleware
 | 
				
			||||||
    etag:
 | 
					        etag:
 | 
				
			||||||
      enable: true
 | 
					            enable: true
 | 
				
			||||||
    # Allows to limit the payload size request. payload that bigger than this file will blocked the request.
 | 
					        # Allows to limit the payload size request. payload that bigger than this file will blocked the request.
 | 
				
			||||||
    limit_payload:
 | 
					        limit_payload:
 | 
				
			||||||
      # Enable/Disable the middleware.
 | 
					            # Enable/Disable the middleware.
 | 
				
			||||||
      enable: true
 | 
					            enable: true
 | 
				
			||||||
      # the limit size. can be b,kb,kib,mb,mib,gb,gib
 | 
					            # the limit size. can be b,kb,kib,mb,mib,gb,gib
 | 
				
			||||||
      body_limit: 5mb
 | 
					            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.
 | 
					        # 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:
 | 
					        logger:
 | 
				
			||||||
      # Enable/Disable the middleware.
 | 
					            # Enable/Disable the middleware.
 | 
				
			||||||
      enable: true
 | 
					            enable: true
 | 
				
			||||||
    # when your code is panicked, the request still returns 500 status code.
 | 
					        # when your code is panicked, the request still returns 500 status code.
 | 
				
			||||||
    catch_panic:
 | 
					        catch_panic:
 | 
				
			||||||
      # Enable/Disable the middleware.
 | 
					            # Enable/Disable the middleware.
 | 
				
			||||||
      enable: true
 | 
					            enable: true
 | 
				
			||||||
    # Timeout for incoming requests middleware. requests that take more time from the configuration will cute and 408 status code will returned.
 | 
					        # Timeout for incoming requests middleware. requests that take more time from the configuration will cute and 408 status code will returned.
 | 
				
			||||||
    timeout_request:
 | 
					        timeout_request:
 | 
				
			||||||
      # Enable/Disable the middleware.
 | 
					            # Enable/Disable the middleware.
 | 
				
			||||||
      enable: false
 | 
					            enable: false
 | 
				
			||||||
      # Duration time in milliseconds.
 | 
					            # Duration time in milliseconds.
 | 
				
			||||||
      timeout: 5000
 | 
					            timeout: 5000
 | 
				
			||||||
    cors:
 | 
					
 | 
				
			||||||
      enable: true
 | 
					        cors:
 | 
				
			||||||
      # Set the value of the [`Access-Control-Allow-Origin`][mdn] header
 | 
					            enable: true
 | 
				
			||||||
      # allow_origins:
 | 
					            # Set the value of the [`Access-Control-Allow-Origin`][mdn] header
 | 
				
			||||||
      #   - https://loco.rs
 | 
					            # allow_origins:
 | 
				
			||||||
      # Set the value of the [`Access-Control-Allow-Headers`][mdn] header
 | 
					            #   - https://loco.rs
 | 
				
			||||||
      # allow_headers:
 | 
					            # Set the value of the [`Access-Control-Allow-Headers`][mdn] header
 | 
				
			||||||
      # - Content-Type
 | 
					            # allow_headers:
 | 
				
			||||||
      # Set the value of the [`Access-Control-Allow-Methods`][mdn] header
 | 
					            # - Content-Type
 | 
				
			||||||
      # allow_methods:
 | 
					            # Set the value of the [`Access-Control-Allow-Methods`][mdn] header
 | 
				
			||||||
      #   - POST
 | 
					            # allow_methods:
 | 
				
			||||||
      # Set the value of the [`Access-Control-Max-Age`][mdn] header in seconds
 | 
					            #   - POST
 | 
				
			||||||
      # max_age: 3600
 | 
					            # Set the value of the [`Access-Control-Max-Age`][mdn] header in seconds
 | 
				
			||||||
    fallback:
 | 
					            # max_age: 3600
 | 
				
			||||||
      enable: false
 | 
					        fallback:
 | 
				
			||||||
 | 
					            enable: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Worker Configuration
 | 
					# Worker Configuration
 | 
				
			||||||
workers:
 | 
					workers:
 | 
				
			||||||
  # specifies the worker mode. Options:
 | 
					    # specifies the worker mode. Options:
 | 
				
			||||||
  #   - BackgroundQueue - Workers operate asynchronously in the background, processing queued.
 | 
					    #   - BackgroundQueue - Workers operate asynchronously in the background, processing queued.
 | 
				
			||||||
  #   - ForegroundBlocking - Workers operate in the foreground and block until tasks are completed.
 | 
					    #   - ForegroundBlocking - Workers operate in the foreground and block until tasks are completed.
 | 
				
			||||||
  #   - BackgroundAsync - Workers operate asynchronously in the background, processing tasks with async capabilities.
 | 
					    #   - BackgroundAsync - Workers operate asynchronously in the background, processing tasks with async capabilities.
 | 
				
			||||||
  mode: BackgroundQueue
 | 
					    mode: BackgroundQueue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Mailer Configuration.
 | 
					# Mailer Configuration.
 | 
				
			||||||
mailer:
 | 
					mailer:
 | 
				
			||||||
  # SMTP mailer configuration.
 | 
					    # SMTP mailer configuration.
 | 
				
			||||||
  smtp:
 | 
					    smtp:
 | 
				
			||||||
    # Enable/Disable smtp mailer.
 | 
					        # Enable/Disable smtp mailer.
 | 
				
			||||||
    enable: true
 | 
					        enable: true
 | 
				
			||||||
    # SMTP server host. e.x localhost, smtp.gmail.com
 | 
					        # SMTP server host. e.x localhost, smtp.gmail.com
 | 
				
			||||||
    host: '{{ get_env(name="MAILER_HOST", default="localhost") }}'
 | 
					        host: '{{ get_env(name="MAILER_HOST", default="localhost") }}'
 | 
				
			||||||
    # SMTP server port
 | 
					        # SMTP server port
 | 
				
			||||||
    port: 1025
 | 
					        port: 1025
 | 
				
			||||||
    # Use secure connection (SSL/TLS).
 | 
					        # Use secure connection (SSL/TLS).
 | 
				
			||||||
    secure: false
 | 
					        secure: false
 | 
				
			||||||
    # auth:
 | 
					        # auth:
 | 
				
			||||||
    #   user:
 | 
					        #   user:
 | 
				
			||||||
    #   password:
 | 
					        #   password:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Database Configuration
 | 
					# Database Configuration
 | 
				
			||||||
database:
 | 
					database:
 | 
				
			||||||
  # Database connection URI
 | 
					    # Database connection URI
 | 
				
			||||||
  uri: '{{ get_env(name="DATABASE_URL", default="postgres://konobangu:konobangu@127.0.0.1:5432/konobangu") }}'
 | 
					    uri: '{{ get_env(name="DATABASE_URL", default="postgres://konobangu:konobangu@127.0.0.1:5432/konobangu") }}'
 | 
				
			||||||
  # When enabled, the sql query will be logged.
 | 
					    # When enabled, the sql query will be logged.
 | 
				
			||||||
  enable_logging: true
 | 
					    enable_logging: true
 | 
				
			||||||
  # Set the timeout duration when acquiring a connection.
 | 
					    # Set the timeout duration when acquiring a connection.
 | 
				
			||||||
  connect_timeout: 500
 | 
					    connect_timeout: 500
 | 
				
			||||||
  # Set the idle duration before closing a connection.
 | 
					    # Set the idle duration before closing a connection.
 | 
				
			||||||
  idle_timeout: 500
 | 
					    idle_timeout: 500
 | 
				
			||||||
  # Minimum number of connections for a pool.
 | 
					    # Minimum number of connections for a pool.
 | 
				
			||||||
  min_connections: 1
 | 
					    min_connections: 1
 | 
				
			||||||
  # Maximum number of connections for a pool.
 | 
					    # Maximum number of connections for a pool.
 | 
				
			||||||
  max_connections: 1
 | 
					    max_connections: 1
 | 
				
			||||||
  # Run migration up when application loaded
 | 
					    # Run migration up when application loaded
 | 
				
			||||||
  auto_migrate: true
 | 
					    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
 | 
					    # 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
 | 
					    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
 | 
					    # 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
 | 
					    dangerously_recreate: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Redis Configuration
 | 
					# Redis Configuration
 | 
				
			||||||
redis:
 | 
					redis:
 | 
				
			||||||
  # Redis connection URI
 | 
					    # Redis connection URI
 | 
				
			||||||
  uri: '{{ get_env(name="REDIS_URL", default="redis://127.0.0.1:6379") }}'
 | 
					    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 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
 | 
					    dangerously_flush: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
settings:
 | 
					settings:
 | 
				
			||||||
 | 
					    dal:
 | 
				
			||||||
 | 
					        data_dir: ./data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  dal:
 | 
					    mikan:
 | 
				
			||||||
    data_dir: ./data
 | 
					        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:
 | 
					    auth:
 | 
				
			||||||
    base_url: "https://mikanani.me/"
 | 
					        auth_type: "oidc" # or "basic"
 | 
				
			||||||
    http_client:
 | 
					        basic_user: "konobangu"
 | 
				
			||||||
      exponential_backoff_max_retries: 3
 | 
					        basic_password: "konobangu"
 | 
				
			||||||
      leaky_bucket_max_tokens: 2
 | 
					        oidc_api_issuer: "https://some-oidc-auth.com/oidc"
 | 
				
			||||||
      leaky_bucket_initial_tokens: 0
 | 
					        oidc_api_audience: "https://konobangu.com/api"
 | 
				
			||||||
      leaky_bucket_refill_tokens: 1
 | 
					        oidc_extra_scopes: "read:konobangu,write:konobangu"
 | 
				
			||||||
      leaky_bucket_refill_interval: 500
 | 
					        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: ""
 | 
					 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										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;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,15 +1,16 @@
 | 
				
			|||||||
dal:
 | 
					dal:
 | 
				
			||||||
  data_dir: ./data
 | 
					    data_dir: ./data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
mikan:
 | 
					mikan:
 | 
				
			||||||
  http_client:
 | 
					    http_client:
 | 
				
			||||||
    exponential_backoff_max_retries: 3
 | 
					        exponential_backoff_max_retries: 3
 | 
				
			||||||
    leaky_bucket_max_tokens: 2
 | 
					        leaky_bucket_max_tokens: 2
 | 
				
			||||||
    leaky_bucket_initial_tokens: 0
 | 
					        leaky_bucket_initial_tokens: 0
 | 
				
			||||||
    leaky_bucket_refill_tokens: 1
 | 
					        leaky_bucket_refill_tokens: 1
 | 
				
			||||||
    leaky_bucket_refill_interval: 500
 | 
					        leaky_bucket_refill_interval: 500
 | 
				
			||||||
  base_url: "https://mikanani.me/"
 | 
					    base_url: "https://mikanani.me/"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
graphql:
 | 
					graphql:
 | 
				
			||||||
  depth_limit: null
 | 
					    playground_static: "./node_modules/altair-static/build/dist"
 | 
				
			||||||
  complexity_limit: null
 | 
					    depth_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