This commit is contained in:
@ -4,8 +4,10 @@ VITE_OAUTH_CLIENT_ID=https://log.syui.ai/client-metadata.json
|
|||||||
VITE_OAUTH_REDIRECT_URI=https://log.syui.ai/oauth/callback
|
VITE_OAUTH_REDIRECT_URI=https://log.syui.ai/oauth/callback
|
||||||
VITE_ADMIN_DID=did:plc:uqzpqmrjnptsxezjx4xuh2mn
|
VITE_ADMIN_DID=did:plc:uqzpqmrjnptsxezjx4xuh2mn
|
||||||
|
|
||||||
# Optional: Override collection names (if not set, auto-generated from host)
|
# Collection names for OAuth app
|
||||||
# VITE_COLLECTION_COMMENT=ai.syui.log
|
VITE_COLLECTION_COMMENT=ai.syui.log
|
||||||
# VITE_COLLECTION_USER=ai.syui.log.user
|
VITE_COLLECTION_USER=ai.syui.log.user
|
||||||
|
|
||||||
|
# Collection names for ailog (backward compatibility)
|
||||||
AILOG_COLLECTION_COMMENT=ai.syui.log
|
AILOG_COLLECTION_COMMENT=ai.syui.log
|
||||||
AILOG_COLLECTION_USER=ai.syui.log.user
|
AILOG_COLLECTION_USER=ai.syui.log.user
|
||||||
|
2
run.zsh
2
run.zsh
@ -6,6 +6,7 @@ function _env() {
|
|||||||
oauth=$d/oauth
|
oauth=$d/oauth
|
||||||
myblog=$d/my-blog
|
myblog=$d/my-blog
|
||||||
port=4173
|
port=4173
|
||||||
|
source $oauth/.env.production
|
||||||
case $OSTYPE in
|
case $OSTYPE in
|
||||||
darwin*)
|
darwin*)
|
||||||
export NVM_DIR="$HOME/.nvm"
|
export NVM_DIR="$HOME/.nvm"
|
||||||
@ -34,7 +35,6 @@ function _oauth_build() {
|
|||||||
cd $oauth
|
cd $oauth
|
||||||
nvm use 21
|
nvm use 21
|
||||||
npm i
|
npm i
|
||||||
source .env.production
|
|
||||||
npm run build
|
npm run build
|
||||||
rm -rf $myblog/static/assets
|
rm -rf $myblog/static/assets
|
||||||
cp -rf dist/* $myblog/static/
|
cp -rf dist/* $myblog/static/
|
||||||
|
@ -6,3 +6,4 @@ pub mod clean;
|
|||||||
pub mod doc;
|
pub mod doc;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod stream;
|
pub mod stream;
|
||||||
|
pub mod oauth;
|
190
src/commands/oauth.rs
Normal file
190
src/commands/oauth.rs
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
use anyhow::{Result, Context};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::fs;
|
||||||
|
use std::process::Command;
|
||||||
|
use toml::Value;
|
||||||
|
|
||||||
|
pub async fn build(project_dir: PathBuf) -> Result<()> {
|
||||||
|
println!("Building OAuth app for project: {}", project_dir.display());
|
||||||
|
|
||||||
|
// 1. Read config.toml from project directory
|
||||||
|
let config_path = project_dir.join("config.toml");
|
||||||
|
if !config_path.exists() {
|
||||||
|
anyhow::bail!("config.toml not found in {}", project_dir.display());
|
||||||
|
}
|
||||||
|
|
||||||
|
let config_content = fs::read_to_string(&config_path)
|
||||||
|
.with_context(|| format!("Failed to read config.toml from {}", config_path.display()))?;
|
||||||
|
|
||||||
|
let config: Value = config_content.parse()
|
||||||
|
.with_context(|| "Failed to parse config.toml")?;
|
||||||
|
|
||||||
|
// 2. Extract [oauth] section
|
||||||
|
let oauth_config = config.get("oauth")
|
||||||
|
.and_then(|v| v.as_table())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("No [oauth] section found in config.toml"))?;
|
||||||
|
|
||||||
|
let site_config = config.get("site")
|
||||||
|
.and_then(|v| v.as_table())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("No [site] section found in config.toml"))?;
|
||||||
|
|
||||||
|
// 3. Generate environment variables
|
||||||
|
let base_url = site_config.get("base_url")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("No base_url found in [site] section"))?;
|
||||||
|
|
||||||
|
let client_id_path = oauth_config.get("json")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("client-metadata.json");
|
||||||
|
|
||||||
|
let redirect_path = oauth_config.get("redirect")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("oauth/callback");
|
||||||
|
|
||||||
|
let admin_did = oauth_config.get("admin")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("No admin DID found in [oauth] section"))?;
|
||||||
|
|
||||||
|
let collection_comment = oauth_config.get("collection_comment")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("ai.syui.log");
|
||||||
|
|
||||||
|
let collection_user = oauth_config.get("collection_user")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("ai.syui.log.user");
|
||||||
|
|
||||||
|
// 4. Create .env.production content
|
||||||
|
let env_content = format!(
|
||||||
|
r#"# Production environment variables
|
||||||
|
VITE_APP_HOST={}
|
||||||
|
VITE_OAUTH_CLIENT_ID={}/{}
|
||||||
|
VITE_OAUTH_REDIRECT_URI={}/{}
|
||||||
|
VITE_ADMIN_DID={}
|
||||||
|
|
||||||
|
# Collection names for OAuth app
|
||||||
|
VITE_COLLECTION_COMMENT={}
|
||||||
|
VITE_COLLECTION_USER={}
|
||||||
|
|
||||||
|
# Collection names for ailog (backward compatibility)
|
||||||
|
AILOG_COLLECTION_COMMENT={}
|
||||||
|
AILOG_COLLECTION_USER={}
|
||||||
|
"#,
|
||||||
|
base_url,
|
||||||
|
base_url, client_id_path,
|
||||||
|
base_url, redirect_path,
|
||||||
|
admin_did,
|
||||||
|
collection_comment,
|
||||||
|
collection_user,
|
||||||
|
collection_comment,
|
||||||
|
collection_user
|
||||||
|
);
|
||||||
|
|
||||||
|
// 5. Find oauth directory (relative to current working directory)
|
||||||
|
let oauth_dir = Path::new("oauth");
|
||||||
|
if !oauth_dir.exists() {
|
||||||
|
anyhow::bail!("oauth directory not found in current working directory");
|
||||||
|
}
|
||||||
|
|
||||||
|
let env_path = oauth_dir.join(".env.production");
|
||||||
|
fs::write(&env_path, env_content)
|
||||||
|
.with_context(|| format!("Failed to write .env.production to {}", env_path.display()))?;
|
||||||
|
|
||||||
|
println!("Generated .env.production");
|
||||||
|
|
||||||
|
// 6. Build OAuth app
|
||||||
|
build_oauth_app(&oauth_dir).await?;
|
||||||
|
|
||||||
|
// 7. Copy build artifacts to project directory
|
||||||
|
copy_build_artifacts(&oauth_dir, &project_dir).await?;
|
||||||
|
|
||||||
|
println!("OAuth app built successfully!");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn build_oauth_app(oauth_dir: &Path) -> Result<()> {
|
||||||
|
println!("Installing dependencies...");
|
||||||
|
|
||||||
|
// Check if node is available
|
||||||
|
let node_check = Command::new("node")
|
||||||
|
.arg("--version")
|
||||||
|
.output();
|
||||||
|
|
||||||
|
if node_check.is_err() {
|
||||||
|
anyhow::bail!("Node.js not found. Please install Node.js or ensure it's in PATH");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install dependencies
|
||||||
|
let npm_install = Command::new("npm")
|
||||||
|
.arg("install")
|
||||||
|
.current_dir(oauth_dir)
|
||||||
|
.status()
|
||||||
|
.with_context(|| "Failed to run npm install")?;
|
||||||
|
|
||||||
|
if !npm_install.success() {
|
||||||
|
anyhow::bail!("npm install failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Building OAuth app...");
|
||||||
|
|
||||||
|
// Build the app
|
||||||
|
let npm_build = Command::new("npm")
|
||||||
|
.arg("run")
|
||||||
|
.arg("build")
|
||||||
|
.current_dir(oauth_dir)
|
||||||
|
.status()
|
||||||
|
.with_context(|| "Failed to run npm run build")?;
|
||||||
|
|
||||||
|
if !npm_build.success() {
|
||||||
|
anyhow::bail!("npm run build failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("OAuth app build completed");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn copy_build_artifacts(oauth_dir: &Path, project_dir: &Path) -> Result<()> {
|
||||||
|
let dist_dir = oauth_dir.join("dist");
|
||||||
|
let static_dir = project_dir.join("static");
|
||||||
|
let templates_dir = project_dir.join("templates");
|
||||||
|
|
||||||
|
// Remove old assets
|
||||||
|
let assets_dir = static_dir.join("assets");
|
||||||
|
if assets_dir.exists() {
|
||||||
|
fs::remove_dir_all(&assets_dir)
|
||||||
|
.with_context(|| format!("Failed to remove old assets directory: {}", assets_dir.display()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy all files from dist to static
|
||||||
|
copy_dir_recursive(&dist_dir, &static_dir)
|
||||||
|
.with_context(|| "Failed to copy dist files to static directory")?;
|
||||||
|
|
||||||
|
// Copy index.html to oauth-assets.html template
|
||||||
|
let index_html = dist_dir.join("index.html");
|
||||||
|
let oauth_assets = templates_dir.join("oauth-assets.html");
|
||||||
|
|
||||||
|
fs::copy(&index_html, &oauth_assets)
|
||||||
|
.with_context(|| "Failed to copy index.html to oauth-assets.html")?;
|
||||||
|
|
||||||
|
println!("Copied build artifacts to project directory");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
|
||||||
|
if !dst.exists() {
|
||||||
|
fs::create_dir_all(dst)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for entry in fs::read_dir(src)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
let dst_path = dst.join(entry.file_name());
|
||||||
|
|
||||||
|
if path.is_dir() {
|
||||||
|
copy_dir_recursive(&path, &dst_path)?;
|
||||||
|
} else {
|
||||||
|
fs::copy(&path, &dst_path)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -5,13 +5,74 @@ use serde::{Deserialize, Serialize};
|
|||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
use tokio::time::{sleep, Duration, interval};
|
use tokio::time::{sleep, Duration, interval};
|
||||||
use tokio_tungstenite::{connect_async, tungstenite::Message};
|
use tokio_tungstenite::{connect_async, tungstenite::Message};
|
||||||
|
use toml;
|
||||||
|
|
||||||
use super::auth::{load_config, load_config_with_refresh, AuthConfig};
|
use super::auth::{load_config, load_config_with_refresh, AuthConfig};
|
||||||
|
|
||||||
|
// Load collection config with priority: env vars > project config.toml > defaults
|
||||||
|
fn load_collection_config(project_dir: Option<&Path>) -> Result<(String, String)> {
|
||||||
|
// 1. Check environment variables first (highest priority)
|
||||||
|
if let (Ok(comment), Ok(user)) = (
|
||||||
|
std::env::var("AILOG_COLLECTION_COMMENT"),
|
||||||
|
std::env::var("AILOG_COLLECTION_USER")
|
||||||
|
) {
|
||||||
|
println!("{}", "📂 Using collection config from environment variables".cyan());
|
||||||
|
return Ok((comment, user));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Try to load from project config.toml (second priority)
|
||||||
|
if let Some(project_path) = project_dir {
|
||||||
|
match load_collection_config_from_project(project_path) {
|
||||||
|
Ok(config) => {
|
||||||
|
println!("{}", format!("📂 Using collection config from: {}", project_path.display()).cyan());
|
||||||
|
return Ok(config);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("{}", format!("⚠️ Failed to load project config: {}", e).yellow());
|
||||||
|
println!("{}", "📂 Falling back to default collections".cyan());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Use defaults (lowest priority)
|
||||||
|
println!("{}", "📂 Using default collection configuration".cyan());
|
||||||
|
Ok(("ai.syui.log".to_string(), "ai.syui.log.user".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load collection config from project's config.toml
|
||||||
|
fn load_collection_config_from_project(project_dir: &Path) -> Result<(String, String)> {
|
||||||
|
let config_path = project_dir.join("config.toml");
|
||||||
|
if !config_path.exists() {
|
||||||
|
return Err(anyhow::anyhow!("config.toml not found in {}", project_dir.display()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let config_content = fs::read_to_string(&config_path)
|
||||||
|
.with_context(|| format!("Failed to read config.toml from {}", config_path.display()))?;
|
||||||
|
|
||||||
|
let config: toml::Value = config_content.parse()
|
||||||
|
.with_context(|| "Failed to parse config.toml")?;
|
||||||
|
|
||||||
|
let oauth_config = config.get("oauth")
|
||||||
|
.and_then(|v| v.as_table())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("No [oauth] section found in config.toml"))?;
|
||||||
|
|
||||||
|
let collection_comment = oauth_config.get("collection_comment")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("ai.syui.log")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let collection_user = oauth_config.get("collection_user")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("ai.syui.log.user")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
Ok((collection_comment, collection_user))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct JetstreamMessage {
|
struct JetstreamMessage {
|
||||||
collection: Option<String>,
|
collection: Option<String>,
|
||||||
@ -57,8 +118,17 @@ fn get_pid_file() -> Result<PathBuf> {
|
|||||||
Ok(pid_dir.join("stream.pid"))
|
Ok(pid_dir.join("stream.pid"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start(daemon: bool) -> Result<()> {
|
pub async fn start(project_dir: Option<PathBuf>, daemon: bool) -> Result<()> {
|
||||||
let config = load_config_with_refresh().await?;
|
let mut config = load_config_with_refresh().await?;
|
||||||
|
|
||||||
|
// Load collection config with priority: env vars > project config > defaults
|
||||||
|
let (collection_comment, collection_user) = load_collection_config(project_dir.as_deref())?;
|
||||||
|
|
||||||
|
// Update config with loaded collections
|
||||||
|
config.collections.comment = collection_comment.clone();
|
||||||
|
config.collections.user = collection_user;
|
||||||
|
config.jetstream.collections = vec![collection_comment];
|
||||||
|
|
||||||
let pid_file = get_pid_file()?;
|
let pid_file = get_pid_file()?;
|
||||||
|
|
||||||
// Check if already running
|
// Check if already running
|
||||||
@ -74,8 +144,15 @@ pub async fn start(daemon: bool) -> Result<()> {
|
|||||||
|
|
||||||
// Fork process for daemon mode
|
// Fork process for daemon mode
|
||||||
let current_exe = std::env::current_exe()?;
|
let current_exe = std::env::current_exe()?;
|
||||||
|
let mut args = vec!["stream".to_string(), "start".to_string()];
|
||||||
|
|
||||||
|
// Add project_dir argument if provided
|
||||||
|
if let Some(project_path) = &project_dir {
|
||||||
|
args.push(project_path.to_string_lossy().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
let child = Command::new(current_exe)
|
let child = Command::new(current_exe)
|
||||||
.args(&["stream", "start"])
|
.args(&args)
|
||||||
.stdin(Stdio::null())
|
.stdin(Stdio::null())
|
||||||
.stdout(Stdio::null())
|
.stdout(Stdio::null())
|
||||||
.stderr(Stdio::null())
|
.stderr(Stdio::null())
|
||||||
|
27
src/main.rs
27
src/main.rs
@ -85,6 +85,11 @@ enum Commands {
|
|||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: StreamCommands,
|
command: StreamCommands,
|
||||||
},
|
},
|
||||||
|
/// OAuth app management
|
||||||
|
Oauth {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: OauthCommands,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
@ -101,6 +106,8 @@ enum AuthCommands {
|
|||||||
enum StreamCommands {
|
enum StreamCommands {
|
||||||
/// Start monitoring ATProto streams
|
/// Start monitoring ATProto streams
|
||||||
Start {
|
Start {
|
||||||
|
/// Path to the blog project directory
|
||||||
|
project_dir: Option<PathBuf>,
|
||||||
/// Run as daemon
|
/// Run as daemon
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
daemon: bool,
|
daemon: bool,
|
||||||
@ -113,6 +120,15 @@ enum StreamCommands {
|
|||||||
Test,
|
Test,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum OauthCommands {
|
||||||
|
/// Build OAuth app
|
||||||
|
Build {
|
||||||
|
/// Path to the blog project directory
|
||||||
|
project_dir: PathBuf,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
@ -159,8 +175,8 @@ async fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
Commands::Stream { command } => {
|
Commands::Stream { command } => {
|
||||||
match command {
|
match command {
|
||||||
StreamCommands::Start { daemon } => {
|
StreamCommands::Start { project_dir, daemon } => {
|
||||||
commands::stream::start(daemon).await?;
|
commands::stream::start(project_dir, daemon).await?;
|
||||||
}
|
}
|
||||||
StreamCommands::Stop => {
|
StreamCommands::Stop => {
|
||||||
commands::stream::stop().await?;
|
commands::stream::stop().await?;
|
||||||
@ -173,6 +189,13 @@ async fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Commands::Oauth { command } => {
|
||||||
|
match command {
|
||||||
|
OauthCommands::Build { project_dir } => {
|
||||||
|
commands::oauth::build(project_dir).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
Reference in New Issue
Block a user