Compare commits
No commits in common. "1e71c9be0978207670c14cee9a755add016135ff" and "1bbb94d5006cab39ac0eb0f05088763615373230" have entirely different histories.
1e71c9be09
...
1bbb94d500
23 changed files with 1303 additions and 1150 deletions
594
LOGGING_PLAN.md
594
LOGGING_PLAN.md
|
|
@ -1,594 +0,0 @@
|
|||
# BotServer Cinema Viewer Logging Plan
|
||||
|
||||
## 🎬 The Cinema Viewer Philosophy
|
||||
|
||||
Think of your logs as a movie with different viewing modes:
|
||||
|
||||
- **INFO** = **The Movie** - Watch the main story unfold (production-ready)
|
||||
- **DEBUG** = **Director's Commentary** - Behind-the-scenes details (troubleshooting)
|
||||
- **TRACE** = **Raw Footage** - Every single take and detail (deep debugging)
|
||||
- **WARN** = **Plot Holes** - Things that shouldn't happen but are recoverable
|
||||
- **ERROR** = **Scene Failures** - Critical issues that break the narrative
|
||||
|
||||
---
|
||||
|
||||
## 📊 Log Level Definitions
|
||||
|
||||
### ERROR - Critical Failures
|
||||
**When to use:** System cannot proceed, requires immediate attention
|
||||
|
||||
```rust
|
||||
// ✅ GOOD - Actionable, clear context
|
||||
error!("Database connection failed - retrying in 5s: {}", e);
|
||||
error!("Authentication failed for user {}: invalid credentials", user_id);
|
||||
error!("Stage 2 BUILD failed: {}", e);
|
||||
|
||||
// ❌ BAD - Vague, no context
|
||||
error!("Error!");
|
||||
error!("Failed");
|
||||
error!("Something went wrong: {}", e);
|
||||
```
|
||||
|
||||
### WARN - Recoverable Issues
|
||||
**When to use:** Unexpected state but system can continue
|
||||
|
||||
```rust
|
||||
// ✅ GOOD - Explains the issue and impact
|
||||
warn!("Failed to create data directory: {}. Using fallback.", e);
|
||||
warn!("LLM server not ready - deferring embedding generation");
|
||||
warn!("Rate limit approaching for API key {}", key_id);
|
||||
|
||||
// ❌ BAD - Not actionable
|
||||
warn!("Warning");
|
||||
warn!("Something happened");
|
||||
```
|
||||
|
||||
### INFO - The Main Story
|
||||
**When to use:** Key events, state changes, business milestones
|
||||
|
||||
```rust
|
||||
// ✅ GOOD - Tells a story, shows progress
|
||||
info!("Pipeline starting - task: {}, intent: {}", task_id, intent);
|
||||
info!("Stage 1 PLAN complete - {} nodes planned", node_count);
|
||||
info!("User {} logged in from {}", user_id, ip_address);
|
||||
info!("Server started on port {}", port);
|
||||
|
||||
// ❌ BAD - Too verbose, implementation details
|
||||
info!("Entering function process_request");
|
||||
info!("Variable x = {}", x);
|
||||
info!("Loop iteration {}", i);
|
||||
```
|
||||
|
||||
### DEBUG - Behind the Scenes
|
||||
**When to use:** Troubleshooting information, decision points, state inspections
|
||||
|
||||
```rust
|
||||
// ✅ GOOD - Helps diagnose issues
|
||||
debug!("Request payload: {:?}", payload);
|
||||
debug!("Using cache key: {}", cache_key);
|
||||
debug!("Retry attempt {} of {}", attempt, max_retries);
|
||||
debug!("Selected LLM model: {} for task type: {}", model, task_type);
|
||||
|
||||
// ❌ BAD - Too trivial
|
||||
debug!("Variable assigned");
|
||||
debug!("Function called");
|
||||
```
|
||||
|
||||
### TRACE - Raw Footage
|
||||
**When to use:** Step-by-step execution, loop iterations, detailed flow
|
||||
|
||||
```rust
|
||||
// ✅ GOOD - Detailed execution path
|
||||
trace!("Starting monitoring loop");
|
||||
trace!("Processing file: {:?}", path);
|
||||
trace!("Checking bot directory: {}", dir);
|
||||
trace!("WebSocket message received: {} bytes", len);
|
||||
|
||||
// ❌ BAD - Noise without value
|
||||
trace!("Line 100");
|
||||
trace!("Got here");
|
||||
trace!("...");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎭 Logging Patterns by Module Type
|
||||
|
||||
### 1. Orchestration & Pipeline Modules
|
||||
**Examples:** `auto_task/orchestrator.rs`, `auto_task/agent_executor.rs`
|
||||
|
||||
**INFO Level:** Show the story arc
|
||||
```rust
|
||||
info!("Pipeline starting - task: {}, intent: {}", task_id, intent);
|
||||
info!("Stage 1 PLAN starting - Agent #1 analyzing request");
|
||||
info!("Stage 1 PLAN complete - {} nodes planned", node_count);
|
||||
info!("Stage 2 BUILD complete - {} resources, url: {}", count, url);
|
||||
info!("Pipeline complete - task: {}, nodes: {}, resources: {}", task_id, nodes, resources);
|
||||
```
|
||||
|
||||
**DEBUG Level:** Show decision points
|
||||
```rust
|
||||
debug!("Classified intent as: {:?}", classification);
|
||||
debug!("Selected app template: {}", template_name);
|
||||
debug!("Skipping stage 3 - no resources to review");
|
||||
```
|
||||
|
||||
**TRACE Level:** Show execution flow
|
||||
```rust
|
||||
trace!("Broadcasting thought to UI: {}", thought);
|
||||
trace!("Updating agent {} status: {} -> {}", id, old, new);
|
||||
trace!("Sub-task generated: {}", task_name);
|
||||
```
|
||||
|
||||
### 2. File Monitoring & Compilation
|
||||
**Examples:** `drive/local_file_monitor.rs`, `drive/drive_monitor/mod.rs`
|
||||
|
||||
**INFO Level:** Key file operations
|
||||
```rust
|
||||
info!("Local file monitor started - watching /opt/gbo/data/*.gbai");
|
||||
info!("Compiling bot: {} ({} files)", bot_name, file_count);
|
||||
info!("Bot {} compiled successfully - {} tools generated", bot_name, tool_count);
|
||||
```
|
||||
|
||||
**DEBUG Level:** File processing details
|
||||
```rust
|
||||
debug!("Detected change in: {:?}", path);
|
||||
debug!("Recompiling {} - modification detected", bot_name);
|
||||
debug!("Skipping {} - no changes detected", bot_name);
|
||||
```
|
||||
|
||||
**TRACE Level:** File system operations
|
||||
```rust
|
||||
trace!("Scanning directory: {:?}", dir);
|
||||
trace!("File modified: {:?} at {:?}", path, time);
|
||||
trace!("Watching directory: {:?}", path);
|
||||
```
|
||||
|
||||
### 3. Security & Authentication
|
||||
**Examples:** `security/jwt.rs`, `security/api_keys.rs`, `security/sql_guard.rs`
|
||||
|
||||
**INFO Level:** Security events (always log these!)
|
||||
```rust
|
||||
info!("User {} logged in from {}", user_id, ip);
|
||||
info!("API key {} created for user {}", key_id, user_id);
|
||||
info!("Failed login attempt for user {} from {}", username, ip);
|
||||
info!("Rate limit exceeded for IP: {}", ip);
|
||||
```
|
||||
|
||||
**DEBUG Level:** Security checks
|
||||
```rust
|
||||
debug!("Validating JWT for user {}", user_id);
|
||||
debug!("Checking API key permissions: {:?}", permissions);
|
||||
debug!("SQL query sanitized: {}", safe_query);
|
||||
```
|
||||
|
||||
**TRACE Level:** Security internals
|
||||
```rust
|
||||
trace!("Token expiry check: {} seconds remaining", remaining);
|
||||
trace!("Permission check: {} -> {}", resource, allowed);
|
||||
trace!("Hashing password with cost factor: {}", cost);
|
||||
```
|
||||
|
||||
### 4. API Handlers
|
||||
**Examples:** HTTP endpoint handlers in `core/`, `drive/`, etc.
|
||||
|
||||
**INFO Level:** Request lifecycle
|
||||
```rust
|
||||
info!("Request started: {} {} from {}", method, path, ip);
|
||||
info!("Request completed: {} {} -> {} ({}ms)", method, path, status, duration);
|
||||
info!("User {} created resource: {}", user_id, resource_id);
|
||||
```
|
||||
|
||||
**DEBUG Level:** Request details
|
||||
```rust
|
||||
debug!("Request headers: {:?}", headers);
|
||||
debug!("Request body: {:?}", body);
|
||||
debug!("Response payload: {} bytes", size);
|
||||
```
|
||||
|
||||
**TRACE Level:** Request processing
|
||||
```rust
|
||||
trace!("Parsing JSON body");
|
||||
trace!("Validating request parameters");
|
||||
trace!("Serializing response");
|
||||
```
|
||||
|
||||
### 5. Database Operations
|
||||
**Examples:** `core/shared/models/`, Diesel queries
|
||||
|
||||
**INFO Level:** Database lifecycle
|
||||
```rust
|
||||
info!("Database connection pool initialized ({} connections)", pool_size);
|
||||
info!("Migration completed - {} tables updated", count);
|
||||
info!("Database backup created: {}", backup_path);
|
||||
```
|
||||
|
||||
**DEBUG Level:** Query information
|
||||
```rust
|
||||
debug!("Executing query: {}", query);
|
||||
debug!("Query returned {} rows in {}ms", count, duration);
|
||||
debug!("Cache miss for key: {}", key);
|
||||
```
|
||||
|
||||
**TRACE Level:** Query details
|
||||
```rust
|
||||
trace!("Preparing statement: {}", sql);
|
||||
trace!("Binding parameter {}: {:?}", index, value);
|
||||
trace!("Fetching next row");
|
||||
```
|
||||
|
||||
### 6. LLM & AI Operations
|
||||
**Examples:** `llm/`, `core/kb/`
|
||||
|
||||
**INFO Level:** LLM operations
|
||||
```rust
|
||||
info!("LLM request started - model: {}, tokens: {}", model, estimated_tokens);
|
||||
info!("LLM response received - {} tokens, {}ms", tokens, duration);
|
||||
info!("Embedding generated - {} dimensions", dimensions);
|
||||
info!("Knowledge base indexed - {} documents", doc_count);
|
||||
```
|
||||
|
||||
**DEBUG Level:** LLM details
|
||||
```rust
|
||||
debug!("LLM prompt: {}", prompt_preview);
|
||||
debug!("Using temperature: {}, max_tokens: {}", temp, max);
|
||||
debug!("Selected model variant: {}", variant);
|
||||
```
|
||||
|
||||
**TRACE Level:** LLM internals
|
||||
```rust
|
||||
trace!("Sending request to LLM API: {}", url);
|
||||
trace!("Streaming token: {}", token);
|
||||
trace!("Parsing LLM response chunk");
|
||||
```
|
||||
|
||||
### 7. Startup & Initialization
|
||||
**Examples:** `main.rs`, `main_module/bootstrap.rs`
|
||||
|
||||
**INFO Level:** Startup milestones
|
||||
```rust
|
||||
info!("Server starting on port {}", port);
|
||||
info!("Database initialized - PostgreSQL connected");
|
||||
info!("Cache initialized - Valkey connected");
|
||||
info!("Secrets loaded from Vault");
|
||||
info!("BotServer ready - {} bots loaded", bot_count);
|
||||
```
|
||||
|
||||
**DEBUG Level:** Configuration details
|
||||
```rust
|
||||
debug!("Using config: {:?}", config);
|
||||
debug!("Environment: {}", env);
|
||||
debug!("Feature flags: {:?}", features);
|
||||
```
|
||||
|
||||
**TRACE Level:** Initialization steps
|
||||
```rust
|
||||
trace!("Loading .env file");
|
||||
trace!("Setting up signal handlers");
|
||||
trace!("Initializing thread registry");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 The Cinema Viewer Experience
|
||||
|
||||
### Level 1: Watching the Movie (INFO)
|
||||
```bash
|
||||
RUST_LOG=botserver=info
|
||||
```
|
||||
|
||||
**What you see:**
|
||||
```
|
||||
INFO botserver: Server starting on port 8080
|
||||
INFO botserver: Database initialized - PostgreSQL connected
|
||||
INFO botserver: User alice@example.com logged in from 192.168.1.100
|
||||
INFO botserver::auto_task::orchestrator: Pipeline starting - task: abc123, intent: Create CRM
|
||||
INFO botserver::auto_task::orchestrator: Stage 1 PLAN complete - 5 nodes planned
|
||||
INFO botserver::auto_task::orchestrator: Stage 2 BUILD complete - 12 resources, url: /apps/crm
|
||||
INFO botserver::auto_task::orchestrator: Pipeline complete - task: abc123, nodes: 5, resources: 12
|
||||
INFO botserver: User alice@example.com logged out
|
||||
```
|
||||
|
||||
**Perfect for:** Production monitoring, understanding system flow
|
||||
|
||||
### Level 2: Director's Commentary (DEBUG)
|
||||
```bash
|
||||
RUST_LOG=botserver=debug
|
||||
```
|
||||
|
||||
**What you see:** Everything from INFO plus:
|
||||
```
|
||||
DEBUG botserver::auto_task::orchestrator: Classified intent as: AppGeneration
|
||||
DEBUG botserver::auto_task::orchestrator: Selected app template: crm
|
||||
DEBUG botserver::security::jwt: Validating JWT for user alice@example.com
|
||||
DEBUG botserver::drive::local_file_monitor: Detected change in: /opt/gbo/data/crm.gbai
|
||||
DEBUG botserver::llm: Using temperature: 0.7, max_tokens: 2000
|
||||
```
|
||||
|
||||
**Perfect for:** Troubleshooting issues, understanding decisions
|
||||
|
||||
### Level 3: Raw Footage (TRACE)
|
||||
```bash
|
||||
RUST_LOG=botserver=trace
|
||||
```
|
||||
|
||||
**What you see:** Everything from DEBUG plus:
|
||||
```
|
||||
TRACE botserver::drive::local_file_monitor: Scanning directory: /opt/gbo/data
|
||||
TRACE botserver::auto_task::orchestrator: Broadcasting thought to UI: Analyzing...
|
||||
TRACE botserver::llm: Streaming token: Create
|
||||
TRACE botserver::llm: Streaming token: a
|
||||
TRACE botserver::llm: Streaming token: CRM
|
||||
TRACE botserver::core::db: Preparing statement: SELECT * FROM bots
|
||||
```
|
||||
|
||||
**Perfect for:** Deep debugging, performance analysis, finding bugs
|
||||
|
||||
---
|
||||
|
||||
## ✨ Best Practices
|
||||
|
||||
### 1. Tell a Story
|
||||
```rust
|
||||
// ✅ GOOD - Shows the narrative
|
||||
info!("Pipeline starting - task: {}", task_id);
|
||||
info!("Stage 1 PLAN complete - {} nodes planned", nodes);
|
||||
info!("Stage 2 BUILD complete - {} resources", resources);
|
||||
info!("Pipeline complete - app deployed at {}", url);
|
||||
|
||||
// ❌ BAD - Just data points
|
||||
info!("Task started");
|
||||
info!("Nodes: {}", nodes);
|
||||
info!("Resources: {}", resources);
|
||||
info!("Done");
|
||||
```
|
||||
|
||||
### 2. Use Structured Data
|
||||
```rust
|
||||
// ✅ GOOD - Easy to parse and filter
|
||||
info!("User {} logged in from {}", user_id, ip);
|
||||
info!("Request completed: {} {} -> {} ({}ms)", method, path, status, duration);
|
||||
|
||||
// ❌ BAD - Hard to parse
|
||||
info!("User login happened");
|
||||
info!("Request finished successfully");
|
||||
```
|
||||
|
||||
### 3. Include Context
|
||||
```rust
|
||||
// ✅ GOOD - Provides context
|
||||
error!("Database connection failed for bot {}: {}", bot_id, e);
|
||||
warn!("Rate limit approaching for user {}: {}/{} requests", user_id, count, limit);
|
||||
|
||||
// ❌ BAD - No context
|
||||
error!("Connection failed: {}", e);
|
||||
warn!("Rate limit warning");
|
||||
```
|
||||
|
||||
### 4. Use Appropriate Levels
|
||||
```rust
|
||||
// ✅ GOOD - Right level for right information
|
||||
info!("Server started on port {}", port); // Key event
|
||||
debug!("Using config: {:?}", config); // Troubleshooting
|
||||
trace!("Listening on socket {:?}", socket); // Deep detail
|
||||
|
||||
// ❌ BAD - Wrong levels
|
||||
trace!("Server started"); // Too important for trace
|
||||
info!("Loop iteration {}", i); // Too verbose for info
|
||||
error!("Variable is null"); // Not an error
|
||||
```
|
||||
|
||||
### 5. Avoid Noise
|
||||
```rust
|
||||
// ✅ GOOD - Meaningful information
|
||||
debug!("Retry attempt {} of {} for API call", attempt, max);
|
||||
|
||||
// ❌ BAD - Just noise
|
||||
debug!("Entering function");
|
||||
debug!("Exiting function");
|
||||
debug!("Variable assigned");
|
||||
```
|
||||
|
||||
### 6. Log State Changes
|
||||
```rust
|
||||
// ✅ GOOD - Shows what changed
|
||||
info!("User {} role changed: {} -> {}", user_id, old_role, new_role);
|
||||
info!("Bot {} status: {} -> {}", bot_id, old_status, new_status);
|
||||
|
||||
// ❌ BAD - No before/after
|
||||
info!("User role updated");
|
||||
info!("Bot status changed");
|
||||
```
|
||||
|
||||
### 7. Include Timings for Operations
|
||||
```rust
|
||||
// ✅ GOOD - Performance visibility
|
||||
info!("Database migration completed in {}ms", duration);
|
||||
info!("LLM response received - {} tokens, {}ms", tokens, duration);
|
||||
debug!("Query executed in {}ms", duration);
|
||||
|
||||
// ❌ BAD - No performance data
|
||||
info!("Migration completed");
|
||||
info!("LLM response received");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Implementation Guide
|
||||
|
||||
### Step 1: Audit Current Logging
|
||||
```bash
|
||||
# Find all logging statements
|
||||
find botserver/src -name "*.rs" -exec grep -n "info!\|debug!\|trace!\|warn!\|error!" {} +
|
||||
|
||||
# Count by level
|
||||
grep -r "info!" botserver/src | wc -l
|
||||
grep -r "debug!" botserver/src | wc -l
|
||||
grep -r "trace!" botserver/src | wc -l
|
||||
```
|
||||
|
||||
### Step 2: Categorize by Module
|
||||
Create a spreadsheet or document listing:
|
||||
- Module name
|
||||
- Current log levels used
|
||||
- Purpose of the module
|
||||
- What story should it tell
|
||||
|
||||
### Step 3: Refactor Module by Module
|
||||
Start with critical path modules:
|
||||
1. **auto_task/orchestrator.rs** - Already done! ✅
|
||||
2. **drive/local_file_monitor.rs** - File operations
|
||||
3. **security/jwt.rs** - Authentication events
|
||||
4. **main.rs** - Startup sequence
|
||||
5. **core/bot/** - Bot lifecycle
|
||||
|
||||
### Step 4: Test Different Verbosity Levels
|
||||
```bash
|
||||
# Test INFO level (production)
|
||||
RUST_LOG=botserver=info cargo run
|
||||
|
||||
# Test DEBUG level (troubleshooting)
|
||||
RUST_LOG=botserver=debug cargo run
|
||||
|
||||
# Test TRACE level (development)
|
||||
RUST_LOG=botserver=trace cargo run
|
||||
```
|
||||
|
||||
### Step 5: Document Module-Specific Patterns
|
||||
For each module, document:
|
||||
- What story does it tell at INFO level?
|
||||
- What troubleshooting info at DEBUG level?
|
||||
- What raw details at TRACE level?
|
||||
|
||||
---
|
||||
|
||||
## 📋 Quick Reference Card
|
||||
|
||||
### Log Level Decision Tree
|
||||
|
||||
```
|
||||
Is this a failure that stops execution?
|
||||
└─ YES → ERROR
|
||||
└─ NO → Is this unexpected but recoverable?
|
||||
└─ YES → WARN
|
||||
└─ NO → Is this a key business event?
|
||||
└─ YES → INFO
|
||||
└─ NO → Is this useful for troubleshooting?
|
||||
└─ YES → DEBUG
|
||||
└─ NO → Is this step-by-step execution detail?
|
||||
└─ YES → TRACE
|
||||
└─ NO → Don't log it!
|
||||
```
|
||||
|
||||
### Module-Specific Cheat Sheet
|
||||
|
||||
| Module Type | INFO | DEBUG | TRACE |
|
||||
|-------------|------|-------|-------|
|
||||
| **Orchestration** | Stage start/complete, pipeline milestones | Decision points, classifications | UI broadcasts, state changes |
|
||||
| **File Monitoring** | Monitor start, bot compiled | Changes detected, recompiles | File scans, timestamps |
|
||||
| **Security** | Logins, key events, failures | Validations, permission checks | Token details, hash operations |
|
||||
| **API Handlers** | Request start/end, resource changes | Headers, payloads | JSON parsing, serialization |
|
||||
| **Database** | Connections, migrations | Queries, row counts | Statement prep, row fetching |
|
||||
| **LLM** | Requests, responses, indexing | Prompts, parameters | Token streaming, chunking |
|
||||
| **Startup** | Service ready, milestones | Config, environment | Init steps, signal handlers |
|
||||
|
||||
---
|
||||
|
||||
## 🎬 Example: Complete Pipeline Logging
|
||||
|
||||
Here's how a complete auto-task pipeline looks at different levels:
|
||||
|
||||
### INFO Level (The Movie)
|
||||
```
|
||||
INFO Pipeline starting - task: task-123, intent: Create a CRM system
|
||||
INFO Stage 1 PLAN starting - Agent #1 analyzing request
|
||||
INFO Stage 1 PLAN complete - 5 nodes planned
|
||||
INFO Stage 2 BUILD starting - Agent #2 generating code
|
||||
INFO Stage 2 BUILD complete - 12 resources, url: /apps/crm-system
|
||||
INFO Stage 3 REVIEW starting - Agent #3 checking code quality
|
||||
INFO Stage 3 REVIEW complete - all checks passed
|
||||
INFO Stage 4 DEPLOY starting - Agent #4 deploying to /apps/crm-system
|
||||
INFO Stage 4 DEPLOY complete - app live at /apps/crm-system
|
||||
INFO Stage 5 MONITOR starting - Agent #1 setting up monitoring
|
||||
INFO Stage 5 MONITOR complete - monitoring active
|
||||
INFO Pipeline complete - task: task-123, nodes: 5, resources: 12, url: /apps/crm-system
|
||||
```
|
||||
|
||||
### DEBUG Level (Director's Commentary)
|
||||
```
|
||||
INFO Pipeline starting - task: task-123, intent: Create a CRM system
|
||||
DEBUG Classified intent as: AppGeneration
|
||||
DEBUG Selected app template: crm_standard
|
||||
INFO Stage 1 PLAN starting - Agent #1 analyzing request
|
||||
DEBUG Generated 5 sub-tasks from intent
|
||||
INFO Stage 1 PLAN complete - 5 nodes planned
|
||||
INFO Stage 2 BUILD starting - Agent #2 generating code
|
||||
DEBUG Using database schema: contacts, deals, activities
|
||||
DEBUG Generated 3 tables, 8 pages, 1 tool
|
||||
INFO Stage 2 BUILD complete - 12 resources, url: /apps/crm-system
|
||||
...
|
||||
```
|
||||
|
||||
### TRACE Level (Raw Footage)
|
||||
```
|
||||
INFO Pipeline starting - task: task-123, intent: Create a CRM system
|
||||
DEBUG Classified intent as: AppGeneration
|
||||
TRACE Extracting entities from: "Create a CRM system"
|
||||
TRACE Found entity: CRM
|
||||
TRACE Found entity: system
|
||||
DEBUG Selected app template: crm_standard
|
||||
INFO Stage 1 PLAN starting - Agent #1 analyzing request
|
||||
TRACE Broadcasting thought to UI: Analyzing request...
|
||||
TRACE Deriving plan sub-tasks
|
||||
TRACE Sub-task 1: Create database schema
|
||||
TRACE Sub-task 2: Generate list page
|
||||
TRACE Sub-task 3: Generate form pages
|
||||
TRACE Sub-task 4: Create BASIC tools
|
||||
TRACE Sub-task 5: Setup navigation
|
||||
DEBUG Generated 5 sub-tasks from intent
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Goals & Metrics
|
||||
|
||||
### Success Criteria
|
||||
1. **INFO logs tell a complete story** - Can understand system flow without DEBUG/TRACE
|
||||
2. **DEBUG logs enable troubleshooting** - Can diagnose issues with context
|
||||
3. **TRACE logs show execution details** - Can see step-by-step for deep debugging
|
||||
4. **No log spam** - Production logs are concise and meaningful
|
||||
5. **Consistent patterns** - Similar modules log similarly
|
||||
|
||||
### Metrics to Track
|
||||
- Lines of logs per request at INFO level: < 20
|
||||
- Lines of logs per request at DEBUG level: < 100
|
||||
- Lines of logs per request at TRACE level: unlimited
|
||||
- Error logs include context: 100%
|
||||
- WARN logs explain impact: 100%
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Next Steps
|
||||
|
||||
1. **Audit** current logging in all 341 files
|
||||
2. **Prioritize** modules by criticality
|
||||
3. **Refactor** module by module following this plan
|
||||
4. **Test** at each log level
|
||||
5. **Document** module-specific patterns
|
||||
6. **Train** team on logging standards
|
||||
7. **Monitor** log volume and usefulness
|
||||
8. **Iterate** based on feedback
|
||||
|
||||
---
|
||||
|
||||
## 📚 References
|
||||
|
||||
- [Rust log crate documentation](https://docs.rs/log/)
|
||||
- [env_logger documentation](https://docs.rs/env_logger/)
|
||||
- [Structured logging best practices](https://www.honeycomb.io/blog/structured-logging/)
|
||||
- [The Log: What every software engineer should know](https://blog.codinghorror.com/the-log-everything-manifesto/)
|
||||
|
||||
---
|
||||
|
||||
**Remember:** Good logging is like good cinematography - it should be invisible when done right, but tell a compelling story when you pay attention to it. 🎬
|
||||
|
|
@ -229,13 +229,16 @@ impl Orchestrator {
|
|||
) -> Result<OrchestrationResult, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let intent_preview = &classification.original_text
|
||||
[..classification.original_text.len().min(80)];
|
||||
info!("Pipeline starting - task: {}, intent: {}", self.task_id, intent_preview);
|
||||
info!(
|
||||
"Orchestrator: starting pipeline task={} intent={}",
|
||||
self.task_id, intent_preview
|
||||
);
|
||||
|
||||
self.broadcast_pipeline_start();
|
||||
|
||||
// ── Stage 1: PLAN ──────────────────────────────────────────────────
|
||||
if let Err(e) = self.execute_plan_stage(classification).await {
|
||||
error!("Stage 1 PLAN failed: {}", e);
|
||||
error!("Plan stage failed: {e}");
|
||||
return Ok(self.failure_result(0, &format!("Planning failed: {e}")));
|
||||
}
|
||||
|
||||
|
|
@ -246,7 +249,7 @@ impl Orchestrator {
|
|||
{
|
||||
Ok(pair) => pair,
|
||||
Err(e) => {
|
||||
error!("Stage 2 BUILD failed: {}", e);
|
||||
error!("Build stage failed: {e}");
|
||||
return Ok(self.failure_result(1, &format!("Build failed: {e}")));
|
||||
}
|
||||
};
|
||||
|
|
@ -272,10 +275,6 @@ impl Orchestrator {
|
|||
.map(|r| format!("✓ {} table created", r.name))
|
||||
.collect();
|
||||
|
||||
// Log final summary
|
||||
info!("Pipeline complete - task: {}, nodes: {}, resources: {}, url: {}",
|
||||
self.task_id, node_count, resources.len(), app_url);
|
||||
|
||||
let message = format!(
|
||||
"Got it. Here's the plan: I broke it down in **{node_count} nodes**.\n\n{}\n\nApp deployed at **{app_url}**",
|
||||
if resource_summary.is_empty() {
|
||||
|
|
@ -318,8 +317,6 @@ impl Orchestrator {
|
|||
&mut self,
|
||||
classification: &ClassifiedIntent,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
info!("Stage 1 PLAN starting - Agent #1 analyzing request");
|
||||
|
||||
self.update_stage(0, StageStatus::Running);
|
||||
self.update_agent_status(1, AgentStatus::Working, Some("Analyzing request"));
|
||||
self.broadcast_thought(
|
||||
|
|
@ -356,7 +353,6 @@ impl Orchestrator {
|
|||
|
||||
self.update_stage(0, StageStatus::Completed);
|
||||
self.update_agent_status(1, AgentStatus::Evolved, None);
|
||||
info!("Stage 1 PLAN complete - {} nodes planned", node_count);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -369,8 +365,6 @@ impl Orchestrator {
|
|||
classification: &ClassifiedIntent,
|
||||
session: &UserSession,
|
||||
) -> Result<(String, Vec<CreatedResource>), Box<dyn std::error::Error + Send + Sync>> {
|
||||
info!("Stage 2 BUILD starting - Agent #2 generating code");
|
||||
|
||||
self.update_stage(1, StageStatus::Running);
|
||||
self.update_agent_status(2, AgentStatus::Bred, Some("Preparing build"));
|
||||
self.broadcast_thought(2, "Builder agent bred. Starting code generation...");
|
||||
|
|
@ -443,12 +437,9 @@ impl Orchestrator {
|
|||
|
||||
self.update_stage(1, StageStatus::Completed);
|
||||
self.update_agent_status(2, AgentStatus::Evolved, None);
|
||||
info!("Stage 2 BUILD complete - {} resources, url: {}",
|
||||
resources.len(), app_url);
|
||||
Ok((app_url, resources))
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Stage 2 BUILD failed: {}", e);
|
||||
self.update_stage(1, StageStatus::Failed);
|
||||
self.update_agent_status(2, AgentStatus::Failed, Some("Build failed"));
|
||||
Err(e)
|
||||
|
|
@ -461,8 +452,6 @@ impl Orchestrator {
|
|||
// =========================================================================
|
||||
|
||||
async fn execute_review_stage(&mut self, resources: &[CreatedResource]) {
|
||||
info!("Stage 3 REVIEW starting - Agent #3 checking code quality");
|
||||
|
||||
self.update_stage(2, StageStatus::Running);
|
||||
self.update_agent_status(3, AgentStatus::Bred, Some("Starting review"));
|
||||
self.broadcast_thought(
|
||||
|
|
@ -497,7 +486,6 @@ impl Orchestrator {
|
|||
|
||||
self.update_stage(2, StageStatus::Completed);
|
||||
self.update_agent_status(3, AgentStatus::Evolved, None);
|
||||
info!("Stage 3 REVIEW complete - all checks passed");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
|
|
@ -505,8 +493,6 @@ impl Orchestrator {
|
|||
// =========================================================================
|
||||
|
||||
async fn execute_deploy_stage(&mut self, app_url: &str) {
|
||||
info!("Stage 4 DEPLOY starting - Agent #4 deploying to {}", app_url);
|
||||
|
||||
self.update_stage(3, StageStatus::Running);
|
||||
self.update_agent_status(4, AgentStatus::Bred, Some("Preparing deploy"));
|
||||
self.broadcast_thought(
|
||||
|
|
@ -532,7 +518,6 @@ impl Orchestrator {
|
|||
|
||||
self.update_stage(3, StageStatus::Completed);
|
||||
self.update_agent_status(4, AgentStatus::Evolved, None);
|
||||
info!("Stage 4 DEPLOY complete - app live at {}", app_url);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
|
|
@ -540,8 +525,6 @@ impl Orchestrator {
|
|||
// =========================================================================
|
||||
|
||||
async fn execute_monitor_stage(&mut self, app_url: &str) {
|
||||
info!("Stage 5 MONITOR starting - Agent #1 setting up monitoring");
|
||||
|
||||
self.update_stage(4, StageStatus::Running);
|
||||
self.broadcast_thought(
|
||||
1,
|
||||
|
|
@ -561,7 +544,6 @@ impl Orchestrator {
|
|||
self.broadcast_activity(1, "monitor_complete", &activity);
|
||||
|
||||
self.update_stage(4, StageStatus::Completed);
|
||||
info!("Stage 5 MONITOR complete - monitoring active");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use log::trace;
|
||||
use log::{info, trace};
|
||||
|
||||
pub fn convert_mail_line_with_substitution(line: &str) -> String {
|
||||
let mut result = String::new();
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ pub mod talk;
|
|||
pub use mail::convert_mail_block;
|
||||
pub use talk::convert_talk_block;
|
||||
|
||||
use log::trace;
|
||||
use log::{info, trace};
|
||||
|
||||
pub fn convert_begin_blocks(script: &str) -> String {
|
||||
let mut result = String::new();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use log::trace;
|
||||
use log::{info, trace};
|
||||
|
||||
pub fn convert_talk_line_with_substitution(line: &str) -> String {
|
||||
let mut result = String::new();
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use crate::basic::keywords::switch_case::switch_keyword;
|
|||
use crate::core::shared::models::UserSession;
|
||||
use crate::core::shared::state::AppState;
|
||||
use diesel::prelude::*;
|
||||
use log::trace;
|
||||
use log::{info, trace};
|
||||
use rhai::{Dynamic, Engine, EvalAltResult, Scope};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -1400,7 +1400,7 @@ impl ScriptService {
|
|||
log::trace!("IF/THEN conversion complete, output has {} lines", result.lines().count());
|
||||
|
||||
// Convert BASIC <> (not equal) to Rhai != globally
|
||||
|
||||
|
||||
|
||||
result.replace(" <> ", " != ")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -146,9 +146,9 @@ impl BootstrapManager {
|
|||
}
|
||||
}
|
||||
|
||||
if pm.is_installed("drive") {
|
||||
if pm.is_installed("minio") {
|
||||
info!("Starting MinIO...");
|
||||
match pm.start("drive") {
|
||||
match pm.start("minio") {
|
||||
Ok(_child) => {
|
||||
info!("MinIO started");
|
||||
}
|
||||
|
|
@ -178,18 +178,13 @@ impl BootstrapManager {
|
|||
info!("Zitadel/Directory service is already running");
|
||||
|
||||
// Create OAuth client if config doesn't exist (even when already running)
|
||||
// Check both Vault and file system for existing config
|
||||
let config_path = self.stack_dir("conf/system/directory_config.json");
|
||||
let has_config = config_path.exists();
|
||||
|
||||
if !has_config {
|
||||
if !config_path.exists() {
|
||||
info!("Creating OAuth client for Directory service...");
|
||||
match crate::core::package_manager::setup_directory().await {
|
||||
Ok(_) => info!("OAuth client created successfully"),
|
||||
Err(e) => warn!("Failed to create OAuth client: {}", e),
|
||||
}
|
||||
} else {
|
||||
info!("Directory config already exists, skipping OAuth setup");
|
||||
}
|
||||
} else {
|
||||
info!("Starting Zitadel/Directory service...");
|
||||
|
|
|
|||
|
|
@ -208,7 +208,7 @@ pub enum BotExistsResult {
|
|||
pub fn zitadel_health_check() -> bool {
|
||||
// Check if Zitadel is responding on port 8300
|
||||
if let Ok(output) = Command::new("curl")
|
||||
.args(["-f", "-s", "--connect-timeout", "2", "http://localhost:8300/debug/healthz"])
|
||||
.args(["-f", "-s", "--connect-timeout", "2", "http://localhost:8300/debug/ready"])
|
||||
.output()
|
||||
{
|
||||
if output.status.success() {
|
||||
|
|
|
|||
|
|
@ -273,11 +273,8 @@ impl BotOrchestrator {
|
|||
}
|
||||
|
||||
info!(
|
||||
"BotServer ready - {} bots loaded",
|
||||
bots_mounted
|
||||
);
|
||||
log::debug!(
|
||||
"Bot mounting details: {} created, {} already existed",
|
||||
"Bot mounting complete: {} bots processed ({} created, {} already existed)",
|
||||
bots_mounted,
|
||||
bots_created,
|
||||
bots_mounted - bots_created
|
||||
);
|
||||
|
|
@ -396,7 +393,7 @@ impl BotOrchestrator {
|
|||
.execute(&mut conn)
|
||||
.map_err(|e| format!("Failed to create bot: {e}"))?;
|
||||
|
||||
info!("User system created resource: bot {}", bot_id);
|
||||
info!("Created bot '{}' with ID '{}'", bot_name, bot_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -455,62 +455,16 @@ impl PackageManager {
|
|||
pre_install_cmds_linux: vec![
|
||||
"mkdir -p {{CONF_PATH}}/directory".to_string(),
|
||||
"mkdir -p {{LOGS_PATH}}".to_string(),
|
||||
// Create Zitadel steps YAML: configures a machine user (service account)
|
||||
// with IAM_OWNER role and writes a PAT file for API bootstrap
|
||||
concat!(
|
||||
"cat > {{CONF_PATH}}/directory/zitadel-init-steps.yaml << 'STEPSEOF'\n",
|
||||
"FirstInstance:\n",
|
||||
" Org:\n",
|
||||
" Machine:\n",
|
||||
" Machine:\n",
|
||||
" Username: gb-service-account\n",
|
||||
" Name: General Bots Service Account\n",
|
||||
" MachineKey:\n",
|
||||
" Type: 1\n",
|
||||
" Pat:\n",
|
||||
" ExpirationDate: '2099-01-01T00:00:00Z'\n",
|
||||
" PatPath: {{CONF_PATH}}/directory/admin-pat.txt\n",
|
||||
" MachineKeyPath: {{CONF_PATH}}/directory/machine-key.json\n",
|
||||
"STEPSEOF",
|
||||
).to_string(),
|
||||
],
|
||||
post_install_cmds_linux: vec![
|
||||
// Create zitadel DB user before start-from-init
|
||||
"PGPASSWORD='{{DB_PASSWORD}}' {{STACK_PATH}}/bin/tables/bin/psql -h localhost -p 5432 -U gbuser -d postgres -c \"CREATE ROLE zitadel WITH LOGIN PASSWORD 'zitadel'\" 2>&1 | grep -v 'already exists' || true".to_string(),
|
||||
"PGPASSWORD='{{DB_PASSWORD}}' {{STACK_PATH}}/bin/tables/bin/psql -h localhost -p 5432 -U gbuser -d postgres -c \"CREATE DATABASE zitadel WITH OWNER zitadel\" 2>&1 | grep -v 'already exists' || true".to_string(),
|
||||
"PGPASSWORD='{{DB_PASSWORD}}' {{STACK_PATH}}/bin/tables/bin/psql -h localhost -p 5432 -U gbuser -d postgres -c \"GRANT ALL PRIVILEGES ON DATABASE zitadel TO zitadel\" 2>&1 || true".to_string(),
|
||||
// Start Zitadel with --steps pointing to our init file (creates machine user + PAT)
|
||||
concat!(
|
||||
"ZITADEL_PORT=8300 ",
|
||||
"ZITADEL_DATABASE_POSTGRES_HOST=localhost ",
|
||||
"ZITADEL_DATABASE_POSTGRES_PORT=5432 ",
|
||||
"ZITADEL_DATABASE_POSTGRES_DATABASE=zitadel ",
|
||||
"ZITADEL_DATABASE_POSTGRES_USER_USERNAME=zitadel ",
|
||||
"ZITADEL_DATABASE_POSTGRES_USER_PASSWORD=zitadel ",
|
||||
"ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE=disable ",
|
||||
"ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME=gbuser ",
|
||||
"ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD={{DB_PASSWORD}} ",
|
||||
"ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE=disable ",
|
||||
"ZITADEL_EXTERNALSECURE=false ",
|
||||
"ZITADEL_EXTERNALDOMAIN=localhost ",
|
||||
"ZITADEL_EXTERNALPORT=8300 ",
|
||||
"ZITADEL_TLS_ENABLED=false ",
|
||||
"nohup {{BIN_PATH}}/zitadel start-from-init ",
|
||||
"--masterkey MasterkeyNeedsToHave32Characters ",
|
||||
"--tlsMode disabled ",
|
||||
"--steps {{CONF_PATH}}/directory/zitadel-init-steps.yaml ",
|
||||
"> {{LOGS_PATH}}/zitadel.log 2>&1 &",
|
||||
).to_string(),
|
||||
// Wait for Zitadel to be ready
|
||||
"for i in $(seq 1 120); do curl -sf http://localhost:8300/debug/healthz && echo 'Zitadel is ready!' && break || sleep 2; done".to_string(),
|
||||
// Wait for PAT token to be written to logs with retry loop
|
||||
// Zitadel may take several seconds to write the PAT after health check passes
|
||||
"echo 'Waiting for PAT token in logs...'; for i in $(seq 1 30); do sync; if grep -q -E '^[A-Za-z0-9_-]{40,}$' {{LOGS_PATH}}/zitadel.log 2>/dev/null; then echo \"PAT token found in logs after $((i*2)) seconds\"; break; fi; sleep 2; done".to_string(),
|
||||
// Extract PAT token from logs if Zitadel printed it to stdout instead of file
|
||||
// The PAT appears as a standalone line (alphanumeric with hyphens/underscores) after machine key JSON
|
||||
"if [ ! -f '{{CONF_PATH}}/directory/admin-pat.txt' ]; then grep -E '^[A-Za-z0-9_-]{40,}$' {{LOGS_PATH}}/zitadel.log 2>/dev/null | head -1 > {{CONF_PATH}}/directory/admin-pat.txt && echo 'PAT extracted from logs' || echo 'Could not extract PAT from logs'; fi".to_string(),
|
||||
// Verify PAT file was created and is not empty
|
||||
"sync; sleep 1; if [ -f '{{CONF_PATH}}/directory/admin-pat.txt' ] && [ -s '{{CONF_PATH}}/directory/admin-pat.txt' ]; then echo 'PAT token created successfully'; cat {{CONF_PATH}}/directory/admin-pat.txt; else echo 'WARNING: PAT file not found or empty'; fi".to_string(),
|
||||
"cat > {{CONF_PATH}}/directory/steps.yaml << 'EOF'\n---\nDatabase:\n postgres:\n Host: localhost\n Port: 5432\n Database: zitadel\n User:\n Username: zitadel\n Password: zitadel\n SSL:\n Mode: disable\n Admin:\n Username: gbuser\n Password: {{DB_PASSWORD}}\n SSL:\n Mode: disable\nEOF".to_string(),
|
||||
"cat > {{CONF_PATH}}/directory/zitadel.yaml << 'EOF'\nLog:\n Level: info\n\nDatabase:\n postgres:\n Host: localhost\n Port: 5432\n Database: zitadel\n User:\n Username: zitadel\n Password: zitadel\n SSL:\n Mode: disable\n Admin:\n Username: gbuser\n Password: {{DB_PASSWORD}}\n SSL:\n Mode: disable\n\nMachine:\n Identification:\n Hostname: localhost\n WebhookAddress: http://localhost:8080\n\nPort: 8300\nExternalDomain: localhost\nExternalPort: 8300\nExternalSecure: false\n\nTLS:\n Enabled: false\nEOF".to_string(),
|
||||
|
||||
|
||||
|
||||
"ZITADEL_MASTERKEY=$(VAULT_ADDR=https://localhost:8200 VAULT_CACERT={{CONF_PATH}}/system/certificates/ca/ca.crt vault kv get -field=masterkey secret/gbo/directory 2>/dev/null || echo 'MasterkeyNeedsToHave32Characters') nohup {{BIN_PATH}}/zitadel start-from-init --config {{CONF_PATH}}/directory/zitadel.yaml --masterkeyFromEnv --tlsMode disabled --steps {{CONF_PATH}}/directory/steps.yaml > {{LOGS_PATH}}/zitadel.log 2>&1 &".to_string(),
|
||||
|
||||
"for i in $(seq 1 90); do curl -sf http://localhost:8300/debug/ready && break || sleep 1; done".to_string(),
|
||||
],
|
||||
pre_install_cmds_macos: vec![
|
||||
"mkdir -p {{CONF_PATH}}/directory".to_string(),
|
||||
|
|
@ -519,34 +473,14 @@ impl PackageManager {
|
|||
pre_install_cmds_windows: vec![],
|
||||
post_install_cmds_windows: vec![],
|
||||
env_vars: HashMap::from([
|
||||
("ZITADEL_PORT".to_string(), "8300".to_string()),
|
||||
("ZITADEL_EXTERNALSECURE".to_string(), "false".to_string()),
|
||||
("ZITADEL_EXTERNALDOMAIN".to_string(), "localhost".to_string()),
|
||||
("ZITADEL_EXTERNALPORT".to_string(), "8300".to_string()),
|
||||
("ZITADEL_TLS_ENABLED".to_string(), "false".to_string()),
|
||||
]),
|
||||
data_download_list: Vec::new(),
|
||||
exec_cmd: concat!(
|
||||
"ZITADEL_PORT=8300 ",
|
||||
"ZITADEL_DATABASE_POSTGRES_HOST=localhost ",
|
||||
"ZITADEL_DATABASE_POSTGRES_PORT=5432 ",
|
||||
"ZITADEL_DATABASE_POSTGRES_DATABASE=zitadel ",
|
||||
"ZITADEL_DATABASE_POSTGRES_USER_USERNAME=zitadel ",
|
||||
"ZITADEL_DATABASE_POSTGRES_USER_PASSWORD=zitadel ",
|
||||
"ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE=disable ",
|
||||
"ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME=gbuser ",
|
||||
"ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD={{DB_PASSWORD}} ",
|
||||
"ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE=disable ",
|
||||
"ZITADEL_EXTERNALSECURE=false ",
|
||||
"ZITADEL_EXTERNALDOMAIN=localhost ",
|
||||
"ZITADEL_EXTERNALPORT=8300 ",
|
||||
"ZITADEL_TLS_ENABLED=false ",
|
||||
"nohup {{BIN_PATH}}/zitadel start ",
|
||||
"--masterkey MasterkeyNeedsToHave32Characters ",
|
||||
"--tlsMode disabled ",
|
||||
"> {{LOGS_PATH}}/zitadel.log 2>&1 &",
|
||||
).to_string(),
|
||||
check_cmd: "curl -f --connect-timeout 2 -m 5 http://localhost:8300/debug/healthz >/dev/null 2>&1".to_string(),
|
||||
exec_cmd: "ZITADEL_MASTERKEY=$(VAULT_ADDR=https://localhost:8200 VAULT_CACERT={{CONF_PATH}}/system/certificates/ca/ca.crt vault kv get -field=masterkey secret/gbo/directory 2>/dev/null || echo 'MasterkeyNeedsToHave32Characters') nohup {{BIN_PATH}}/zitadel start --config {{CONF_PATH}}/directory/zitadel.yaml --masterkeyFromEnv --tlsMode disabled > {{LOGS_PATH}}/zitadel.log 2>&1 &".to_string(),
|
||||
check_cmd: "curl -f --connect-timeout 2 -m 5 http://localhost:8300/healthz >/dev/null 2>&1".to_string(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,16 +45,108 @@ pub fn get_all_components() -> Vec<ComponentInfo> {
|
|||
]
|
||||
}
|
||||
|
||||
/// Parse Zitadel log file to extract initial admin credentials
|
||||
#[cfg(feature = "directory")]
|
||||
fn extract_initial_admin_from_log(log_path: &std::path::Path) -> Option<(String, String)> {
|
||||
use std::fs;
|
||||
|
||||
let log_content = fs::read_to_string(log_path).ok()?;
|
||||
|
||||
// Try different log formats from Zitadel
|
||||
// Format 1: "initial admin user created. email: admin@<domain> password: <password>"
|
||||
for line in log_content.lines() {
|
||||
let line_lower = line.to_lowercase();
|
||||
if line_lower.contains("initial admin") || line_lower.contains("admin credentials") {
|
||||
// Try to extract email and password
|
||||
let email = if let Some(email_start) = line.find("email:") {
|
||||
let rest = &line[email_start + 6..];
|
||||
rest.trim()
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.map(|s| s.trim_end_matches(',').to_string())
|
||||
} else if let Some(email_start) = line.find("Email:") {
|
||||
let rest = &line[email_start + 6..];
|
||||
rest.trim()
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.map(|s| s.trim_end_matches(',').to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let password = if let Some(pwd_start) = line.find("password:") {
|
||||
let rest = &line[pwd_start + 9..];
|
||||
rest.trim()
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.map(|s| s.trim_end_matches(',').to_string())
|
||||
} else if let Some(pwd_start) = line.find("Password:") {
|
||||
let rest = &line[pwd_start + 9..];
|
||||
rest.trim()
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.map(|s| s.trim_end_matches(',').to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let (Some(email), Some(password)) = (email, password) {
|
||||
if !email.is_empty() && !password.is_empty() {
|
||||
log::info!("Extracted initial admin credentials from log: {}", email);
|
||||
return Some((email, password));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try multiline format
|
||||
// Admin credentials:
|
||||
// Email: admin@localhost
|
||||
// Password: xxxxx
|
||||
let lines: Vec<&str> = log_content.lines().collect();
|
||||
for i in 0..lines.len().saturating_sub(2) {
|
||||
if lines[i].to_lowercase().contains("admin credentials") {
|
||||
let mut email = None;
|
||||
let mut password = None;
|
||||
|
||||
for j in (i + 1)..std::cmp::min(i + 5, lines.len()) {
|
||||
let line = lines[j];
|
||||
if line.contains("Email:") {
|
||||
email = line.split("Email:")
|
||||
.nth(1)
|
||||
.map(|s| s.trim().to_string());
|
||||
}
|
||||
if line.contains("Password:") {
|
||||
password = line.split("Password:")
|
||||
.nth(1)
|
||||
.map(|s| s.trim().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if let (Some(e), Some(p)) = (email, password) {
|
||||
if !e.is_empty() && !p.is_empty() {
|
||||
log::info!("Extracted initial admin credentials from multiline log: {}", e);
|
||||
return Some((e, p));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Admin credentials structure
|
||||
#[cfg(feature = "directory")]
|
||||
struct AdminCredentials {
|
||||
email: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
/// Initialize Directory (Zitadel) with default admin user and OAuth application
|
||||
/// This should be called after Zitadel has started and is responding
|
||||
#[cfg(feature = "directory")]
|
||||
pub async fn setup_directory() -> anyhow::Result<crate::core::package_manager::setup::DirectoryConfig> {
|
||||
use std::path::PathBuf;
|
||||
use std::collections::HashMap;
|
||||
|
||||
let stack_path = std::env::var("BOTSERVER_STACK_PATH")
|
||||
.unwrap_or_else(|_| "./botserver-stack".to_string());
|
||||
|
|
@ -62,51 +154,11 @@ pub async fn setup_directory() -> anyhow::Result<crate::core::package_manager::s
|
|||
let base_url = "http://localhost:8300".to_string();
|
||||
let config_path = PathBuf::from(&stack_path).join("conf/system/directory_config.json");
|
||||
|
||||
// Check if config already exists in Vault first
|
||||
if let Ok(secrets_manager) = crate::core::secrets::SecretsManager::from_env() {
|
||||
if secrets_manager.is_enabled() {
|
||||
if let Ok(secrets) = secrets_manager.get_secret(crate::core::secrets::SecretPaths::DIRECTORY).await {
|
||||
if let (Some(client_id), Some(client_secret)) = (secrets.get("client_id"), secrets.get("client_secret")) {
|
||||
// Validate that credentials are real, not placeholders
|
||||
let is_valid = !client_id.is_empty()
|
||||
&& !client_secret.is_empty()
|
||||
&& client_secret != "..."
|
||||
&& client_id.contains('@') // OAuth client IDs contain @
|
||||
&& client_secret.len() > 10; // Real secrets are longer than placeholders
|
||||
|
||||
if is_valid {
|
||||
log::info!("Directory already configured with OAuth client in Vault");
|
||||
// Reconstruct config from Vault
|
||||
let config = crate::core::package_manager::setup::DirectoryConfig {
|
||||
base_url: base_url.clone(),
|
||||
issuer_url: secrets.get("issuer_url").cloned().unwrap_or_else(|| base_url.clone()),
|
||||
issuer: secrets.get("issuer").cloned().unwrap_or_else(|| base_url.clone()),
|
||||
client_id: client_id.clone(),
|
||||
client_secret: client_secret.clone(),
|
||||
redirect_uri: secrets.get("redirect_uri").cloned().unwrap_or_else(|| "http://localhost:3000/auth/callback".to_string()),
|
||||
project_id: secrets.get("project_id").cloned().unwrap_or_default(),
|
||||
api_url: secrets.get("api_url").cloned().unwrap_or_else(|| base_url.clone()),
|
||||
service_account_key: secrets.get("service_account_key").cloned(),
|
||||
};
|
||||
return Ok(config);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if config already exists with valid OAuth client in file
|
||||
// Check if config already exists with valid OAuth client
|
||||
if config_path.exists() {
|
||||
if let Ok(content) = std::fs::read_to_string(&config_path) {
|
||||
if let Ok(config) = serde_json::from_str::<crate::core::package_manager::setup::DirectoryConfig>(&content) {
|
||||
// Validate that credentials are real, not placeholders
|
||||
let is_valid = !config.client_id.is_empty()
|
||||
&& !config.client_secret.is_empty()
|
||||
&& config.client_secret != "..."
|
||||
&& config.client_id.contains('@')
|
||||
&& config.client_secret.len() > 10;
|
||||
|
||||
if is_valid {
|
||||
if !config.client_id.is_empty() && !config.client_secret.is_empty() {
|
||||
log::info!("Directory already configured with OAuth client");
|
||||
return Ok(config);
|
||||
}
|
||||
|
|
@ -114,33 +166,96 @@ pub async fn setup_directory() -> anyhow::Result<crate::core::package_manager::s
|
|||
}
|
||||
}
|
||||
|
||||
// Initialize directory with default credentials
|
||||
let mut directory_setup = crate::core::package_manager::setup::DirectorySetup::new(base_url.clone(), config_path.clone());
|
||||
let config = directory_setup.initialize().await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to initialize directory: {}", e))?;
|
||||
// Try to get credentials from multiple sources
|
||||
let credentials = get_admin_credentials(&stack_path).await?;
|
||||
|
||||
// Store credentials in Vault
|
||||
if let Ok(secrets_manager) = crate::core::secrets::SecretsManager::from_env() {
|
||||
if secrets_manager.is_enabled() {
|
||||
let mut secrets = HashMap::new();
|
||||
secrets.insert("url".to_string(), config.base_url.clone());
|
||||
secrets.insert("issuer_url".to_string(), config.issuer_url.clone());
|
||||
secrets.insert("issuer".to_string(), config.issuer.clone());
|
||||
secrets.insert("client_id".to_string(), config.client_id.clone());
|
||||
secrets.insert("client_secret".to_string(), config.client_secret.clone());
|
||||
secrets.insert("redirect_uri".to_string(), config.redirect_uri.clone());
|
||||
secrets.insert("project_id".to_string(), config.project_id.clone());
|
||||
secrets.insert("api_url".to_string(), config.api_url.clone());
|
||||
if let Some(key) = &config.service_account_key {
|
||||
secrets.insert("service_account_key".to_string(), key.clone());
|
||||
}
|
||||
let mut directory_setup = crate::core::package_manager::setup::DirectorySetup::with_admin_credentials(
|
||||
base_url,
|
||||
config_path.clone(),
|
||||
credentials.email,
|
||||
credentials.password,
|
||||
);
|
||||
|
||||
match secrets_manager.put_secret(crate::core::secrets::SecretPaths::DIRECTORY, secrets).await {
|
||||
Ok(_) => log::info!("Directory credentials stored in Vault"),
|
||||
Err(e) => log::warn!("Failed to store directory credentials in Vault: {}", e),
|
||||
}
|
||||
directory_setup.initialize().await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to initialize directory: {}", e))
|
||||
}
|
||||
|
||||
/// Get admin credentials from multiple sources
|
||||
#[cfg(feature = "directory")]
|
||||
async fn get_admin_credentials(stack_path: &str) -> anyhow::Result<AdminCredentials> {
|
||||
// Approach 1: Read from ~/.gb-setup-credentials (most reliable - from first bootstrap)
|
||||
if let Some(creds) = read_saved_credentials() {
|
||||
log::info!("Using credentials from ~/.gb-setup-credentials");
|
||||
return Ok(creds);
|
||||
}
|
||||
|
||||
// Approach 2: Try to extract from Zitadel logs (fallback)
|
||||
let log_path = std::path::PathBuf::from(stack_path).join("logs/directory/zitadel.log");
|
||||
if let Some((email, password)) = extract_initial_admin_from_log(&log_path) {
|
||||
log::info!("Using credentials extracted from Zitadel log");
|
||||
return Ok(AdminCredentials { email, password });
|
||||
}
|
||||
|
||||
// This should not be reached - initialize() will handle authentication errors
|
||||
// If we get here, it means credentials were found but authentication failed
|
||||
log::error!("═══════════════════════════════════════════════════════════════");
|
||||
log::error!("❌ ZITADEL AUTHENTICATION FAILED");
|
||||
log::error!("═══════════════════════════════════════════════════════════════");
|
||||
log::error!("Credentials were found but authentication failed.");
|
||||
log::error!("This usually means:");
|
||||
log::error!(" • Credentials are from a previous Zitadel installation");
|
||||
log::error!(" • User account is locked or disabled");
|
||||
log::error!(" • Password has been changed");
|
||||
log::error!("");
|
||||
log::error!("SOLUTION: Reset and create fresh credentials:");
|
||||
log::error!(" 1. Delete: rm ~/.gb-setup-credentials");
|
||||
log::error!(" 2. Delete: rm .env");
|
||||
log::error!(" 3. Delete: rm botserver-stack/conf/system/.bootstrap_completed");
|
||||
log::error!(" 4. Run: ./reset.sh");
|
||||
log::error!(" 5. New admin credentials will be displayed and saved");
|
||||
log::error!("═══════════════════════════════════════════════════════════════");
|
||||
|
||||
anyhow::bail!("Authentication failed. Reset bootstrap to create fresh credentials.")
|
||||
}
|
||||
|
||||
/// Read credentials from ~/.gb-setup-credentials file
|
||||
#[cfg(feature = "directory")]
|
||||
fn read_saved_credentials() -> Option<AdminCredentials> {
|
||||
let home = std::env::var("HOME").ok()?;
|
||||
let creds_path = std::path::PathBuf::from(&home).join(".gb-setup-credentials");
|
||||
|
||||
if !creds_path.exists() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let content = std::fs::read_to_string(&creds_path).ok()?;
|
||||
|
||||
// Parse credentials from file
|
||||
let mut username = None;
|
||||
let mut password = None;
|
||||
let mut email = None;
|
||||
|
||||
for line in content.lines() {
|
||||
if line.contains("Username:") {
|
||||
username = line.split("Username:")
|
||||
.nth(1)
|
||||
.map(|s| s.trim().to_string());
|
||||
}
|
||||
if line.contains("Password:") {
|
||||
password = line.split("Password:")
|
||||
.nth(1)
|
||||
.map(|s| s.trim().to_string());
|
||||
}
|
||||
if line.contains("Email:") {
|
||||
email = line.split("Email:")
|
||||
.nth(1)
|
||||
.map(|s| s.trim().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(config)
|
||||
if let (Some(_username), Some(password), Some(email)) = (username, password, email) {
|
||||
Some(AdminCredentials { email, password })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,338 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use log::{info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use reqwest::Client;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DirectoryConfig {
|
||||
pub base_url: String,
|
||||
pub issuer_url: String,
|
||||
pub issuer: String,
|
||||
pub client_id: String,
|
||||
pub client_secret: String,
|
||||
pub redirect_uri: String,
|
||||
pub project_id: String,
|
||||
pub api_url: String,
|
||||
pub service_account_key: Option<String>,
|
||||
}
|
||||
|
||||
pub struct DirectorySetup {
|
||||
base_url: String,
|
||||
config_path: PathBuf,
|
||||
http_client: Client,
|
||||
pat_token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct CreateProjectRequest {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ProjectResponse {
|
||||
id: String,
|
||||
}
|
||||
|
||||
impl DirectorySetup {
|
||||
pub fn new(base_url: String, config_path: PathBuf) -> Self {
|
||||
let http_client = Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.build()
|
||||
.unwrap_or_else(|_| Client::new());
|
||||
|
||||
Self {
|
||||
base_url,
|
||||
config_path,
|
||||
http_client,
|
||||
pat_token: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn initialize(&mut self) -> Result<DirectoryConfig> {
|
||||
info!("Initializing Directory (Zitadel) OAuth client...");
|
||||
|
||||
// Step 1: Wait for Zitadel to be ready
|
||||
self.wait_for_zitadel().await?;
|
||||
|
||||
// Step 2: Load PAT token from file (created by start-from-init --steps)
|
||||
info!("Loading PAT token from Zitadel init steps...");
|
||||
self.load_pat_token()?;
|
||||
|
||||
// Step 3: Get or create project
|
||||
let project_id = self.get_or_create_project().await?;
|
||||
info!("Using project ID: {project_id}");
|
||||
|
||||
// Step 4: Create OAuth application
|
||||
let (client_id, client_secret) = self.create_oauth_application(&project_id).await?;
|
||||
info!("Created OAuth application with client_id: {client_id}");
|
||||
|
||||
// Step 5: Create config
|
||||
let config = DirectoryConfig {
|
||||
base_url: self.base_url.clone(),
|
||||
issuer_url: self.base_url.clone(),
|
||||
issuer: self.base_url.clone(),
|
||||
client_id,
|
||||
client_secret,
|
||||
redirect_uri: "http://localhost:3000/auth/callback".to_string(),
|
||||
project_id,
|
||||
api_url: self.base_url.clone(),
|
||||
service_account_key: None,
|
||||
};
|
||||
|
||||
// Step 6: Save config
|
||||
self.save_config(&config)?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
async fn wait_for_zitadel(&self) -> Result<()> {
|
||||
info!("Waiting for Zitadel to be ready...");
|
||||
|
||||
for i in 1..=120 {
|
||||
match self.http_client
|
||||
.get(format!("{}/debug/healthz", self.base_url))
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(response) if response.status().is_success() => {
|
||||
info!("Zitadel is ready (healthz OK)");
|
||||
return Ok(());
|
||||
}
|
||||
_ => {
|
||||
if i % 15 == 0 {
|
||||
info!("Still waiting for Zitadel... (attempt {i}/120)");
|
||||
}
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow::anyhow!("Zitadel did not become ready within 240 seconds"))
|
||||
}
|
||||
|
||||
/// Load the PAT token from the file generated by Zitadel's start-from-init --steps
|
||||
/// The steps YAML configures FirstInstance.Org.PatPath which tells Zitadel to
|
||||
/// create a machine user with IAM_OWNER role and write its PAT to disk
|
||||
fn load_pat_token(&mut self) -> Result<()> {
|
||||
let stack_path = std::env::var("BOTSERVER_STACK_PATH")
|
||||
.unwrap_or_else(|_| "./botserver-stack".to_string());
|
||||
|
||||
let pat_path = PathBuf::from(&stack_path).join("conf/directory/admin-pat.txt");
|
||||
|
||||
if pat_path.exists() {
|
||||
let pat_token = std::fs::read_to_string(&pat_path)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to read PAT file {}: {}", pat_path.display(), e))?
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
if pat_token.is_empty() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"PAT file exists at {} but is empty. Zitadel start-from-init may have failed.",
|
||||
pat_path.display()
|
||||
));
|
||||
}
|
||||
|
||||
info!("Loaded PAT token from: {} (len={})", pat_path.display(), pat_token.len());
|
||||
self.pat_token = Some(pat_token);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Also check the legacy location
|
||||
let legacy_pat_path = std::path::Path::new("./botserver-stack/conf/directory/admin-pat.txt");
|
||||
if legacy_pat_path.exists() {
|
||||
let pat_token = std::fs::read_to_string(legacy_pat_path)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to read PAT file: {e}"))?
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
if !pat_token.is_empty() {
|
||||
info!("Loaded PAT token from legacy path");
|
||||
self.pat_token = Some(pat_token);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow::anyhow!(
|
||||
"No PAT token file found at {}. \
|
||||
Zitadel must be started with 'start-from-init --steps <steps.yaml>' \
|
||||
where steps.yaml has FirstInstance.Org.PatPath configured.",
|
||||
pat_path.display()
|
||||
))
|
||||
}
|
||||
|
||||
async fn get_or_create_project(&self) -> Result<String> {
|
||||
info!("Getting or creating Zitadel project...");
|
||||
|
||||
let auth_header = self.get_auth_header()?;
|
||||
|
||||
// Try to list existing projects via management API v1
|
||||
let list_response = self.http_client
|
||||
.post(format!("{}/management/v1/projects/_search", self.base_url))
|
||||
.header("Authorization", &auth_header)
|
||||
.json(&serde_json::json!({}))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if list_response.status().is_success() {
|
||||
let projects: serde_json::Value = list_response.json().await?;
|
||||
|
||||
if let Some(result) = projects.get("result").and_then(|r| r.as_array()) {
|
||||
for project in result {
|
||||
if project.get("name")
|
||||
.and_then(|n| n.as_str())
|
||||
.map(|n| n == "General Bots")
|
||||
.unwrap_or(false)
|
||||
{
|
||||
if let Some(id) = project.get("id").and_then(|i| i.as_str()) {
|
||||
info!("Found existing 'General Bots' project: {id}");
|
||||
return Ok(id.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create new project
|
||||
info!("Creating new 'General Bots' project...");
|
||||
let create_request = CreateProjectRequest {
|
||||
name: "General Bots".to_string(),
|
||||
};
|
||||
|
||||
let create_response = self.http_client
|
||||
.post(format!("{}/management/v1/projects", self.base_url))
|
||||
.header("Authorization", self.get_auth_header()?)
|
||||
.json(&create_request)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !create_response.status().is_success() {
|
||||
let error_text = create_response.text().await.unwrap_or_default();
|
||||
return Err(anyhow::anyhow!("Failed to create project: {error_text}"));
|
||||
}
|
||||
|
||||
let project: ProjectResponse = create_response.json().await?;
|
||||
info!("Created project with ID: {}", project.id);
|
||||
|
||||
Ok(project.id)
|
||||
}
|
||||
|
||||
async fn create_oauth_application(&self, project_id: &str) -> Result<(String, String)> {
|
||||
info!("Creating OAuth/OIDC application for BotServer...");
|
||||
|
||||
let auth_header = self.get_auth_header()?;
|
||||
|
||||
// Use the management v1 OIDC app creation endpoint which returns
|
||||
// client_id and client_secret in the response directly
|
||||
let app_body = serde_json::json!({
|
||||
"name": "BotServer",
|
||||
"redirectUris": [
|
||||
"http://localhost:3000/auth/callback",
|
||||
"http://localhost:8080/auth/callback"
|
||||
],
|
||||
"responseTypes": ["OIDC_RESPONSE_TYPE_CODE"],
|
||||
"grantTypes": [
|
||||
"OIDC_GRANT_TYPE_AUTHORIZATION_CODE",
|
||||
"OIDC_GRANT_TYPE_REFRESH_TOKEN"
|
||||
],
|
||||
"appType": "OIDC_APP_TYPE_WEB",
|
||||
"authMethodType": "OIDC_AUTH_METHOD_TYPE_POST",
|
||||
"postLogoutRedirectUris": ["http://localhost:3000"],
|
||||
"devMode": true
|
||||
});
|
||||
|
||||
let app_response = self.http_client
|
||||
.post(format!("{}/management/v1/projects/{project_id}/apps/oidc", self.base_url))
|
||||
.header("Authorization", &auth_header)
|
||||
.json(&app_body)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !app_response.status().is_success() {
|
||||
let error_text = app_response.text().await.unwrap_or_default();
|
||||
return Err(anyhow::anyhow!("Failed to create OIDC app: {error_text}"));
|
||||
}
|
||||
|
||||
let app_data: serde_json::Value = app_response.json().await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to parse OIDC app response: {e}"))?;
|
||||
|
||||
info!("OIDC app creation response: {}", serde_json::to_string_pretty(&app_data).unwrap_or_default());
|
||||
|
||||
// The response contains clientId and clientSecret directly
|
||||
let client_id = app_data
|
||||
.get("clientId")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("No clientId in OIDC app response: {app_data}"))?
|
||||
.to_string();
|
||||
|
||||
let client_secret = app_data
|
||||
.get("clientSecret")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
|
||||
if client_secret.is_empty() {
|
||||
warn!("No clientSecret returned — app may use PKCE only");
|
||||
}
|
||||
|
||||
info!("Retrieved OAuth client credentials (client_id: {client_id})");
|
||||
Ok((client_id, client_secret))
|
||||
}
|
||||
|
||||
fn get_auth_header(&self) -> Result<String> {
|
||||
match &self.pat_token {
|
||||
Some(token) => Ok(format!("Bearer {token}")),
|
||||
None => Err(anyhow::anyhow!(
|
||||
"No PAT token available. Cannot authenticate with Zitadel API."
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn save_config(&self, config: &DirectoryConfig) -> Result<()> {
|
||||
if let Some(parent) = self.config_path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
let content = serde_json::to_string_pretty(config)?;
|
||||
std::fs::write(&self.config_path, content)?;
|
||||
|
||||
info!("Saved Directory config to: {}", self.config_path.display());
|
||||
|
||||
println!();
|
||||
println!("╔════════════════════════════════════════════════════════════╗");
|
||||
println!("║ ZITADEL OAUTH CLIENT CONFIGURED ║");
|
||||
println!("╠════════════════════════════════════════════════════════════╣");
|
||||
println!("║ Project ID: {:<43}║", config.project_id);
|
||||
println!("║ Client ID: {:<43}║", config.client_id);
|
||||
println!("║ Redirect URI: {:<43}║", config.redirect_uri);
|
||||
println!("║ Config saved: {:<43}║", self.config_path.display().to_string().chars().take(43).collect::<String>());
|
||||
println!("╚════════════════════════════════════════════════════════════╝");
|
||||
println!();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_directory_config_serialization() {
|
||||
let config = DirectoryConfig {
|
||||
base_url: "http://localhost:8300".to_string(),
|
||||
issuer_url: "http://localhost:8300".to_string(),
|
||||
issuer: "http://localhost:8300".to_string(),
|
||||
client_id: "test_client".to_string(),
|
||||
client_secret: "test_secret".to_string(),
|
||||
redirect_uri: "http://localhost:3000/callback".to_string(),
|
||||
project_id: "12345".to_string(),
|
||||
api_url: "http://localhost:8300".to_string(),
|
||||
service_account_key: None,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&config).unwrap();
|
||||
assert!(json.contains("test_client"));
|
||||
assert!(json.contains("test_secret"));
|
||||
}
|
||||
}
|
||||
631
src/core/package_manager/setup/directory_setup.rs
Normal file
631
src/core/package_manager/setup/directory_setup.rs
Normal file
|
|
@ -0,0 +1,631 @@
|
|||
use anyhow::Result;
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use tokio::fs;
|
||||
use tokio::time::sleep;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DirectorySetup {
|
||||
base_url: String,
|
||||
client: Client,
|
||||
admin_token: Option<String>,
|
||||
/// Admin credentials for password grant authentication (used during initial setup)
|
||||
admin_credentials: Option<(String, String)>,
|
||||
config_path: PathBuf,
|
||||
}
|
||||
|
||||
impl DirectorySetup {
|
||||
pub fn set_admin_token(&mut self, token: String) {
|
||||
self.admin_token = Some(token);
|
||||
}
|
||||
|
||||
/// Set admin credentials for password grant authentication
|
||||
pub fn set_admin_credentials(&mut self, username: String, password: String) {
|
||||
self.admin_credentials = Some((username, password));
|
||||
}
|
||||
|
||||
/// Get an access token using either PAT or password grant
|
||||
async fn get_admin_access_token(&self) -> Result<String> {
|
||||
// If we have a PAT token, use it directly
|
||||
if let Some(ref token) = self.admin_token {
|
||||
return Ok(token.clone());
|
||||
}
|
||||
|
||||
// If we have admin credentials, use password grant
|
||||
if let Some((username, password)) = &self.admin_credentials {
|
||||
let token_url = format!("{}/oauth/v2/token", self.base_url);
|
||||
let params = [
|
||||
("grant_type", "password".to_string()),
|
||||
("username", username.clone()),
|
||||
("password", password.clone()),
|
||||
("scope", "openid profile email urn:zitadel:iam:org:project:id:zitadel:aud".to_string()),
|
||||
];
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.post(&token_url)
|
||||
.form(¶ms)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to get access token: {}", e))?;
|
||||
|
||||
let token_data: serde_json::Value = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to parse token response: {}", e))?;
|
||||
|
||||
let access_token = token_data
|
||||
.get("access_token")
|
||||
.and_then(|t| t.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("No access token in response"))?
|
||||
.to_string();
|
||||
|
||||
log::info!("Obtained access token via password grant");
|
||||
return Ok(access_token);
|
||||
}
|
||||
|
||||
Err(anyhow::anyhow!("No admin token or credentials configured"))
|
||||
}
|
||||
|
||||
pub async fn ensure_admin_token(&mut self) -> Result<()> {
|
||||
if self.admin_token.is_none() && self.admin_credentials.is_none() {
|
||||
return Err(anyhow::anyhow!("Admin token or credentials must be configured"));
|
||||
}
|
||||
|
||||
// If we have credentials but no token, authenticate and get the token
|
||||
if self.admin_token.is_none() && self.admin_credentials.is_some() {
|
||||
let token = self.get_admin_access_token().await?;
|
||||
self.admin_token = Some(token);
|
||||
log::info!("Obtained admin access token from credentials");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_secure_password() -> String {
|
||||
use rand::distr::Alphanumeric;
|
||||
use rand::Rng;
|
||||
let mut rng = rand::rng();
|
||||
(0..16)
|
||||
.map(|_| {
|
||||
let byte = rng.sample(Alphanumeric);
|
||||
char::from(byte)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct DefaultOrganization {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub domain: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct DefaultUser {
|
||||
pub id: String,
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
}
|
||||
|
||||
pub struct CreateUserParams<'a> {
|
||||
pub org_id: &'a str,
|
||||
pub username: &'a str,
|
||||
pub email: &'a str,
|
||||
pub password: &'a str,
|
||||
pub first_name: &'a str,
|
||||
pub last_name: &'a str,
|
||||
pub is_admin: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct DirectoryConfig {
|
||||
pub base_url: String,
|
||||
pub default_org: DefaultOrganization,
|
||||
pub default_user: DefaultUser,
|
||||
pub admin_token: String,
|
||||
pub project_id: String,
|
||||
pub client_id: String,
|
||||
pub client_secret: String,
|
||||
}
|
||||
|
||||
impl DirectorySetup {
|
||||
pub fn new(base_url: String, config_path: PathBuf) -> Self {
|
||||
Self {
|
||||
base_url,
|
||||
client: Client::builder()
|
||||
.timeout(Duration::from_secs(30))
|
||||
.build()
|
||||
.unwrap_or_else(|e| {
|
||||
log::warn!("Failed to create HTTP client with timeout: {}, using default", e);
|
||||
Client::new()
|
||||
}),
|
||||
admin_token: None,
|
||||
admin_credentials: None,
|
||||
config_path,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a DirectorySetup with initial admin credentials for password grant
|
||||
pub fn with_admin_credentials(base_url: String, config_path: PathBuf, username: String, password: String) -> Self {
|
||||
Self {
|
||||
base_url,
|
||||
client: Client::builder()
|
||||
.timeout(Duration::from_secs(30))
|
||||
.build()
|
||||
.unwrap_or_else(|e| {
|
||||
log::warn!("Failed to create HTTP client with timeout: {}, using default", e);
|
||||
Client::new()
|
||||
}),
|
||||
admin_token: None,
|
||||
admin_credentials: Some((username, password)),
|
||||
config_path,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn wait_for_ready(&self, max_attempts: u32) -> Result<()> {
|
||||
log::info!("Waiting for Directory service to be ready...");
|
||||
|
||||
for attempt in 1..=max_attempts {
|
||||
match self
|
||||
.client
|
||||
.get(format!("{}/debug/ready", self.base_url))
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(response) if response.status().is_success() => {
|
||||
log::info!("Directory service is ready!");
|
||||
return Ok(());
|
||||
}
|
||||
_ => {
|
||||
log::debug!(
|
||||
"Directory not ready yet (attempt {}/{})",
|
||||
attempt,
|
||||
max_attempts
|
||||
);
|
||||
sleep(Duration::from_secs(3)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::bail!("Directory service did not become ready in time")
|
||||
}
|
||||
|
||||
pub async fn initialize(&mut self) -> Result<DirectoryConfig> {
|
||||
log::info!(" Initializing Directory (Zitadel) with defaults...");
|
||||
|
||||
if let Ok(existing_config) = self.load_existing_config().await {
|
||||
log::info!("Directory already initialized, using existing config");
|
||||
return Ok(existing_config);
|
||||
}
|
||||
|
||||
self.wait_for_ready(30).await?;
|
||||
|
||||
// Wait additional time for Zitadel API to be fully ready
|
||||
log::info!("Waiting for Zitadel API to be fully initialized...");
|
||||
sleep(Duration::from_secs(10)).await;
|
||||
|
||||
self.ensure_admin_token().await?;
|
||||
|
||||
let org = self.create_default_organization().await?;
|
||||
log::info!(" Created default organization: {}", org.name);
|
||||
|
||||
let user = self.create_default_user(&org.id).await?;
|
||||
log::info!(" Created default user: {}", user.username);
|
||||
|
||||
// Retry OAuth client creation up to 3 times with delays
|
||||
let (project_id, client_id, client_secret) = {
|
||||
let mut last_error = None;
|
||||
let mut result = None;
|
||||
|
||||
for attempt in 1..=3 {
|
||||
match self.create_oauth_application(&org.id).await {
|
||||
Ok(credentials) => {
|
||||
result = Some(credentials);
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!(
|
||||
"OAuth client creation attempt {}/3 failed: {}",
|
||||
attempt,
|
||||
e
|
||||
);
|
||||
last_error = Some(e);
|
||||
if attempt < 3 {
|
||||
log::info!("Retrying in 5 seconds...");
|
||||
sleep(Duration::from_secs(5)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"Failed to create OAuth client after 3 attempts: {}",
|
||||
last_error.unwrap_or_else(|| anyhow::anyhow!("Unknown error"))
|
||||
)
|
||||
})?
|
||||
};
|
||||
log::info!(" Created OAuth2 application");
|
||||
|
||||
self.grant_user_permissions(&org.id, &user.id).await?;
|
||||
log::info!(" Granted admin permissions to default user");
|
||||
|
||||
let config = DirectoryConfig {
|
||||
base_url: self.base_url.clone(),
|
||||
default_org: org,
|
||||
default_user: user,
|
||||
admin_token: self.admin_token.clone().unwrap_or_default(),
|
||||
project_id,
|
||||
client_id,
|
||||
client_secret,
|
||||
};
|
||||
|
||||
self.save_config_internal(&config).await?;
|
||||
log::info!(" Saved Directory configuration");
|
||||
|
||||
log::info!(" Directory initialization complete!");
|
||||
log::info!("");
|
||||
log::info!("╔══════════════════════════════════════════════════════════════╗");
|
||||
log::info!("║ DEFAULT CREDENTIALS ║");
|
||||
log::info!("╠══════════════════════════════════════════════════════════════╣");
|
||||
log::info!("║ Email: {:<50}║", config.default_user.email);
|
||||
log::info!("║ Password: {:<50}║", config.default_user.password);
|
||||
log::info!("╠══════════════════════════════════════════════════════════════╣");
|
||||
log::info!("║ Login at: {:<50}║", self.base_url);
|
||||
log::info!("╚══════════════════════════════════════════════════════════════╝");
|
||||
log::info!("");
|
||||
log::info!(">>> COPY THESE CREDENTIALS NOW - Press ENTER to continue <<<");
|
||||
|
||||
let mut input = String::new();
|
||||
let _ = std::io::stdin().read_line(&mut input);
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub async fn create_organization(&mut self, name: &str, description: &str) -> Result<String> {
|
||||
self.ensure_admin_token().await?;
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.post(format!("{}/management/v1/orgs", self.base_url))
|
||||
.bearer_auth(self.admin_token.as_ref().unwrap_or(&String::new()))
|
||||
.json(&json!({
|
||||
"name": name,
|
||||
"description": description,
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let error_text = response.text().await?;
|
||||
anyhow::bail!("Failed to create organization: {}", error_text);
|
||||
}
|
||||
|
||||
let result: serde_json::Value = response.json().await?;
|
||||
Ok(result["id"].as_str().unwrap_or("").to_string())
|
||||
}
|
||||
|
||||
async fn create_default_organization(&self) -> Result<DefaultOrganization> {
|
||||
let org_name = "BotServer".to_string();
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.post(format!("{}/management/v1/orgs", self.base_url))
|
||||
.bearer_auth(self.admin_token.as_ref().unwrap_or(&String::new()))
|
||||
.json(&json!({
|
||||
"name": org_name,
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let error_text = response.text().await?;
|
||||
anyhow::bail!("Failed to create organization: {}", error_text);
|
||||
}
|
||||
|
||||
let result: serde_json::Value = response.json().await?;
|
||||
|
||||
Ok(DefaultOrganization {
|
||||
id: result["id"].as_str().unwrap_or("").to_string(),
|
||||
name: org_name.clone(),
|
||||
domain: format!("{}.localhost", org_name.to_lowercase()),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn create_user(
|
||||
&mut self,
|
||||
params: CreateUserParams<'_>,
|
||||
) -> Result<DefaultUser> {
|
||||
self.ensure_admin_token().await?;
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.post(format!("{}/management/v1/users/human", self.base_url))
|
||||
.bearer_auth(self.admin_token.as_ref().unwrap_or(&String::new()))
|
||||
.json(&json!({
|
||||
"userName": params.username,
|
||||
"profile": {
|
||||
"firstName": params.first_name,
|
||||
"lastName": params.last_name,
|
||||
"displayName": format!("{} {}", params.first_name, params.last_name)
|
||||
},
|
||||
"email": {
|
||||
"email": params.email,
|
||||
"isEmailVerified": true
|
||||
},
|
||||
"password": params.password,
|
||||
"organisation": {
|
||||
"orgId": params.org_id
|
||||
}
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let error_text = response.text().await?;
|
||||
anyhow::bail!("Failed to create user: {}", error_text);
|
||||
}
|
||||
|
||||
let result: serde_json::Value = response.json().await?;
|
||||
|
||||
let user = DefaultUser {
|
||||
id: result["userId"].as_str().unwrap_or("").to_string(),
|
||||
username: params.username.to_string(),
|
||||
email: params.email.to_string(),
|
||||
password: params.password.to_string(),
|
||||
first_name: params.first_name.to_string(),
|
||||
last_name: params.last_name.to_string(),
|
||||
};
|
||||
|
||||
if params.is_admin {
|
||||
self.grant_user_permissions(params.org_id, &user.id).await?;
|
||||
}
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
async fn create_default_user(&self, org_id: &str) -> Result<DefaultUser> {
|
||||
let username = format!(
|
||||
"admin_{}",
|
||||
uuid::Uuid::new_v4()
|
||||
.to_string()
|
||||
.chars()
|
||||
.take(8)
|
||||
.collect::<String>()
|
||||
);
|
||||
let email = format!("{}@botserver.local", username);
|
||||
let password = Self::generate_secure_password();
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.post(format!("{}/management/v1/users/human", self.base_url))
|
||||
.bearer_auth(self.admin_token.as_ref().unwrap_or(&String::new()))
|
||||
.json(&json!({
|
||||
"userName": username,
|
||||
"profile": {
|
||||
"firstName": "Admin",
|
||||
"lastName": "User",
|
||||
"displayName": "Administrator"
|
||||
},
|
||||
"email": {
|
||||
"email": email,
|
||||
"isEmailVerified": true
|
||||
},
|
||||
"password": password,
|
||||
"organisation": {
|
||||
"orgId": org_id
|
||||
}
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let error_text = response.text().await?;
|
||||
anyhow::bail!("Failed to create user: {}", error_text);
|
||||
}
|
||||
|
||||
let result: serde_json::Value = response.json().await?;
|
||||
|
||||
Ok(DefaultUser {
|
||||
id: result["userId"].as_str().unwrap_or("").to_string(),
|
||||
username: username.clone(),
|
||||
email: email.clone(),
|
||||
password: password.clone(),
|
||||
first_name: "Admin".to_string(),
|
||||
last_name: "User".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn create_oauth_application(
|
||||
&self,
|
||||
_org_id: &str,
|
||||
) -> Result<(String, String, String)> {
|
||||
let app_name = "BotServer";
|
||||
let redirect_uri = "http://localhost:8080/auth/callback".to_string();
|
||||
|
||||
// Get access token using either PAT or password grant
|
||||
let access_token = self.get_admin_access_token().await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to get admin access token: {}", e))?;
|
||||
|
||||
let project_response = self
|
||||
.client
|
||||
.post(format!("{}/management/v1/projects", self.base_url))
|
||||
.bearer_auth(&access_token)
|
||||
.json(&json!({
|
||||
"name": app_name,
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !project_response.status().is_success() {
|
||||
let error_text = project_response.text().await.unwrap_or_default();
|
||||
return Err(anyhow::anyhow!("Failed to create project: {}", error_text));
|
||||
}
|
||||
|
||||
let project_result: serde_json::Value = project_response.json().await?;
|
||||
let project_id = project_result["id"].as_str().unwrap_or("").to_string();
|
||||
|
||||
if project_id.is_empty() {
|
||||
return Err(anyhow::anyhow!("Project ID is empty in response"));
|
||||
}
|
||||
|
||||
let app_response = self.client
|
||||
.post(format!("{}/management/v1/projects/{}/apps/oidc", self.base_url, project_id))
|
||||
.bearer_auth(&access_token)
|
||||
.json(&json!({
|
||||
"name": app_name,
|
||||
"redirectUris": [redirect_uri, "http://localhost:3000/auth/callback", "http://localhost:8080/auth/callback", "http://localhost:9000/auth/callback"],
|
||||
"responseTypes": ["OIDC_RESPONSE_TYPE_CODE"],
|
||||
"grantTypes": ["OIDC_GRANT_TYPE_AUTHORIZATION_CODE", "OIDC_GRANT_TYPE_REFRESH_TOKEN", "OIDC_GRANT_TYPE_PASSWORD"],
|
||||
"appType": "OIDC_APP_TYPE_WEB",
|
||||
"authMethodType": "OIDC_AUTH_METHOD_TYPE_POST",
|
||||
"postLogoutRedirectUris": ["http://localhost:8080", "http://localhost:3000", "http://localhost:9000"],
|
||||
"accessTokenType": "OIDC_TOKEN_TYPE_BEARER",
|
||||
"devMode": true,
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !app_response.status().is_success() {
|
||||
let error_text = app_response.text().await.unwrap_or_default();
|
||||
return Err(anyhow::anyhow!("Failed to create OAuth application: {}", error_text));
|
||||
}
|
||||
|
||||
let app_result: serde_json::Value = app_response.json().await?;
|
||||
let client_id = app_result["clientId"].as_str().unwrap_or("").to_string();
|
||||
let client_secret = app_result["clientSecret"]
|
||||
.as_str()
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
|
||||
if client_id.is_empty() {
|
||||
return Err(anyhow::anyhow!("Client ID is empty in response"));
|
||||
}
|
||||
|
||||
log::info!("Created OAuth application with client_id: {}", client_id);
|
||||
Ok((project_id, client_id, client_secret))
|
||||
}
|
||||
|
||||
pub async fn grant_user_permissions(&self, org_id: &str, user_id: &str) -> Result<()> {
|
||||
let _response = self
|
||||
.client
|
||||
.post(format!(
|
||||
"{}/management/v1/orgs/{}/members",
|
||||
self.base_url, org_id
|
||||
))
|
||||
.bearer_auth(self.admin_token.as_ref().unwrap_or(&String::new()))
|
||||
.json(&json!({
|
||||
"userId": user_id,
|
||||
"roles": ["ORG_OWNER"]
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn save_config(
|
||||
&mut self,
|
||||
org_id: String,
|
||||
org_name: String,
|
||||
admin_user: DefaultUser,
|
||||
client_id: String,
|
||||
client_secret: String,
|
||||
) -> Result<DirectoryConfig> {
|
||||
self.ensure_admin_token().await?;
|
||||
|
||||
let config = DirectoryConfig {
|
||||
base_url: self.base_url.clone(),
|
||||
default_org: DefaultOrganization {
|
||||
id: org_id,
|
||||
name: org_name.clone(),
|
||||
domain: format!("{}.localhost", org_name.to_lowercase()),
|
||||
},
|
||||
default_user: admin_user,
|
||||
admin_token: self.admin_token.clone().unwrap_or_default(),
|
||||
project_id: String::new(),
|
||||
client_id,
|
||||
client_secret,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string_pretty(&config)?;
|
||||
fs::write(&self.config_path, json).await?;
|
||||
|
||||
log::info!(
|
||||
"Saved Directory configuration to {}",
|
||||
self.config_path.display()
|
||||
);
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
async fn save_config_internal(&self, config: &DirectoryConfig) -> Result<()> {
|
||||
// Ensure parent directory exists
|
||||
if let Some(parent) = self.config_path.parent() {
|
||||
if !parent.exists() {
|
||||
fs::create_dir_all(parent).await.map_err(|e| {
|
||||
anyhow::anyhow!("Failed to create config directory {}: {}", parent.display(), e)
|
||||
})?;
|
||||
log::info!("Created config directory: {}", parent.display());
|
||||
}
|
||||
}
|
||||
|
||||
let json = serde_json::to_string_pretty(config)?;
|
||||
fs::write(&self.config_path, json).await.map_err(|e| {
|
||||
anyhow::anyhow!("Failed to write config to {}: {}", self.config_path.display(), e)
|
||||
})?;
|
||||
log::info!("Saved Directory configuration to {}", self.config_path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn load_existing_config(&self) -> Result<DirectoryConfig> {
|
||||
let content = fs::read_to_string(&self.config_path).await?;
|
||||
let config: DirectoryConfig = serde_json::from_str(&content)?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub async fn get_config(&self) -> Result<DirectoryConfig> {
|
||||
self.load_existing_config().await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn generate_directory_config(config_path: PathBuf, _db_path: PathBuf) -> Result<()> {
|
||||
let yaml_config = r"
|
||||
Log:
|
||||
Level: info
|
||||
|
||||
Database:
|
||||
Postgres:
|
||||
Host: localhost
|
||||
Port: 5432
|
||||
Database: zitadel
|
||||
User: zitadel
|
||||
Password: zitadel
|
||||
SSL:
|
||||
Mode: disable
|
||||
|
||||
Machine:
|
||||
Identification:
|
||||
Hostname: localhost
|
||||
WebhookAddress: http://localhost:8080
|
||||
|
||||
Port: 9000
|
||||
ExternalDomain: localhost
|
||||
ExternalPort: 9000
|
||||
ExternalSecure: false
|
||||
|
||||
TLS:
|
||||
Enabled: false
|
||||
"
|
||||
.to_string();
|
||||
|
||||
fs::write(config_path, yaml_config).await?;
|
||||
Ok(())
|
||||
}
|
||||
342
src/core/package_manager/setup/email_setup.rs
Normal file
342
src/core/package_manager/setup/email_setup.rs
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use tokio::fs;
|
||||
use tokio::time::sleep;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EmailSetup {
|
||||
base_url: String,
|
||||
admin_user: String,
|
||||
admin_pass: String,
|
||||
config_path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct EmailConfig {
|
||||
pub base_url: String,
|
||||
pub smtp_host: String,
|
||||
pub smtp_port: u16,
|
||||
pub imap_host: String,
|
||||
pub imap_port: u16,
|
||||
pub admin_user: String,
|
||||
pub admin_pass: String,
|
||||
pub directory_integration: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct EmailDomain {
|
||||
pub domain: String,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
impl EmailSetup {
|
||||
pub fn new(base_url: String, config_path: PathBuf) -> Self {
|
||||
let admin_user = format!(
|
||||
"admin_{}@botserver.local",
|
||||
uuid::Uuid::new_v4()
|
||||
.to_string()
|
||||
.chars()
|
||||
.take(8)
|
||||
.collect::<String>()
|
||||
);
|
||||
let admin_pass = Self::generate_secure_password();
|
||||
|
||||
Self {
|
||||
base_url,
|
||||
admin_user,
|
||||
admin_pass,
|
||||
config_path,
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_secure_password() -> String {
|
||||
use rand::distr::Alphanumeric;
|
||||
use rand::Rng;
|
||||
let mut rng = rand::rng();
|
||||
(0..16)
|
||||
.map(|_| {
|
||||
let byte = rng.sample(Alphanumeric);
|
||||
char::from(byte)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn wait_for_ready(&self, max_attempts: u32) -> Result<()> {
|
||||
log::info!("Waiting for Email service to be ready...");
|
||||
|
||||
for attempt in 1..=max_attempts {
|
||||
if tokio::net::TcpStream::connect("127.0.0.1:25").await.is_ok() {
|
||||
log::info!("Email service is ready!");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
log::debug!(
|
||||
"Email service not ready yet (attempt {}/{})",
|
||||
attempt,
|
||||
max_attempts
|
||||
);
|
||||
sleep(Duration::from_secs(3)).await;
|
||||
}
|
||||
|
||||
anyhow::bail!("Email service did not become ready in time")
|
||||
}
|
||||
|
||||
pub async fn initialize(
|
||||
&mut self,
|
||||
directory_config_path: Option<PathBuf>,
|
||||
) -> Result<EmailConfig> {
|
||||
log::info!(" Initializing Email (Stalwart) server...");
|
||||
|
||||
if let Ok(existing_config) = self.load_existing_config().await {
|
||||
log::info!("Email already initialized, using existing config");
|
||||
return Ok(existing_config);
|
||||
}
|
||||
|
||||
self.wait_for_ready(30).await?;
|
||||
|
||||
self.create_default_domain()?;
|
||||
log::info!(" Created default email domain: localhost");
|
||||
|
||||
let directory_integration = if let Some(dir_config_path) = directory_config_path {
|
||||
match self.setup_directory_integration(&dir_config_path) {
|
||||
Ok(_) => {
|
||||
log::info!(" Integrated with Directory for authentication");
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!(" Directory integration failed: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
self.create_admin_account().await?;
|
||||
log::info!(" Created admin email account: {}", self.admin_user);
|
||||
|
||||
let config = EmailConfig {
|
||||
base_url: self.base_url.clone(),
|
||||
smtp_host: "localhost".to_string(),
|
||||
smtp_port: 25,
|
||||
imap_host: "localhost".to_string(),
|
||||
imap_port: 143,
|
||||
admin_user: self.admin_user.clone(),
|
||||
admin_pass: self.admin_pass.clone(),
|
||||
directory_integration,
|
||||
};
|
||||
|
||||
self.save_config(&config).await?;
|
||||
log::info!(" Saved Email configuration");
|
||||
|
||||
log::info!(" Email initialization complete!");
|
||||
log::info!("📧 SMTP: localhost:25 (587 for TLS)");
|
||||
log::info!("📬 IMAP: localhost:143 (993 for TLS)");
|
||||
log::info!("👤 Admin: {} / {}", config.admin_user, config.admin_pass);
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn create_default_domain(&self) -> Result<()> {
|
||||
let _ = self;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_admin_account(&self) -> Result<()> {
|
||||
log::info!("Creating admin email account via Stalwart API...");
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(30))
|
||||
.build()?;
|
||||
|
||||
let api_url = format!("{}/api/account", self.base_url);
|
||||
|
||||
let account_data = serde_json::json!({
|
||||
"name": self.admin_user,
|
||||
"secret": self.admin_pass,
|
||||
"description": "BotServer Admin Account",
|
||||
"quota": 1_073_741_824,
|
||||
"type": "individual",
|
||||
"emails": [self.admin_user.clone()],
|
||||
"memberOf": ["administrators"],
|
||||
"enabled": true
|
||||
});
|
||||
|
||||
let response = client
|
||||
.post(&api_url)
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&account_data)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
// All branches return Ok(()) - just log appropriate messages
|
||||
match response {
|
||||
Ok(resp) => {
|
||||
if resp.status().is_success() {
|
||||
log::info!(
|
||||
"Admin email account created successfully: {}",
|
||||
self.admin_user
|
||||
);
|
||||
} else if resp.status().as_u16() == 409 {
|
||||
log::info!("Admin email account already exists: {}", self.admin_user);
|
||||
} else {
|
||||
let status = resp.status();
|
||||
log::warn!("Failed to create admin account via API (status {})", status);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!(
|
||||
"Could not connect to Stalwart management API: {}. Account may need manual setup.",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn setup_directory_integration(&self, directory_config_path: &PathBuf) -> Result<()> {
|
||||
let _ = self;
|
||||
let content = std::fs::read_to_string(directory_config_path)?;
|
||||
let dir_config: serde_json::Value = serde_json::from_str(&content)?;
|
||||
|
||||
let issuer_url = dir_config["base_url"]
|
||||
.as_str()
|
||||
.unwrap_or("http://localhost:9000");
|
||||
|
||||
log::info!("Setting up OIDC authentication with Directory...");
|
||||
log::info!("Issuer URL: {}", issuer_url);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn save_config(&self, config: &EmailConfig) -> Result<()> {
|
||||
let json = serde_json::to_string_pretty(config)?;
|
||||
fs::write(&self.config_path, json).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn load_existing_config(&self) -> Result<EmailConfig> {
|
||||
let content = fs::read_to_string(&self.config_path).await?;
|
||||
let config: EmailConfig = serde_json::from_str(&content)?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub async fn get_config(&self) -> Result<EmailConfig> {
|
||||
self.load_existing_config().await
|
||||
}
|
||||
|
||||
pub fn create_user_mailbox(&self, _username: &str, _password: &str, email: &str) -> Result<()> {
|
||||
let _ = self;
|
||||
log::info!("Creating mailbox for user: {}", email);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn sync_users_from_directory(&self, directory_config_path: &PathBuf) -> Result<()> {
|
||||
log::info!("Syncing users from Directory to Email...");
|
||||
|
||||
let content = fs::read_to_string(directory_config_path).await?;
|
||||
let dir_config: serde_json::Value = serde_json::from_str(&content)?;
|
||||
|
||||
if let Some(default_user) = dir_config.get("default_user") {
|
||||
let email = default_user["email"].as_str().unwrap_or("");
|
||||
let password = default_user["password"].as_str().unwrap_or("");
|
||||
let username = default_user["username"].as_str().unwrap_or("");
|
||||
|
||||
if !email.is_empty() {
|
||||
self.create_user_mailbox(username, password, email)?;
|
||||
log::info!(" Created mailbox for: {}", email);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn generate_email_config(
|
||||
config_path: PathBuf,
|
||||
data_path: PathBuf,
|
||||
directory_integration: bool,
|
||||
) -> Result<()> {
|
||||
let mut config = format!(
|
||||
r#"
|
||||
[server]
|
||||
hostname = "localhost"
|
||||
|
||||
[server.listener."smtp"]
|
||||
bind = ["0.0.0.0:25"]
|
||||
protocol = "smtp"
|
||||
|
||||
[server.listener."smtp-submission"]
|
||||
bind = ["0.0.0.0:587"]
|
||||
protocol = "smtp"
|
||||
tls.implicit = false
|
||||
|
||||
[server.listener."smtp-submissions"]
|
||||
bind = ["0.0.0.0:465"]
|
||||
protocol = "smtp"
|
||||
tls.implicit = true
|
||||
|
||||
[server.listener."imap"]
|
||||
bind = ["0.0.0.0:143"]
|
||||
protocol = "imap"
|
||||
|
||||
[server.listener."imaps"]
|
||||
bind = ["0.0.0.0:993"]
|
||||
protocol = "imap"
|
||||
tls.implicit = true
|
||||
|
||||
[server.listener."http"]
|
||||
bind = ["0.0.0.0:9000"]
|
||||
protocol = "http"
|
||||
|
||||
[storage]
|
||||
data = "sqlite"
|
||||
blob = "sqlite"
|
||||
lookup = "sqlite"
|
||||
fts = "sqlite"
|
||||
|
||||
[store."sqlite"]
|
||||
type = "sqlite"
|
||||
path = "{}/stalwart.db"
|
||||
|
||||
[directory."local"]
|
||||
type = "internal"
|
||||
store = "sqlite"
|
||||
|
||||
"#,
|
||||
data_path.display()
|
||||
);
|
||||
|
||||
if directory_integration {
|
||||
config.push_str(
|
||||
r#"
|
||||
[directory."oidc"]
|
||||
type = "oidc"
|
||||
issuer = "http://localhost:9000"
|
||||
client-id = "{{CLIENT_ID}}"
|
||||
client-secret = "{{CLIENT_SECRET}}"
|
||||
|
||||
[authentication]
|
||||
mechanisms = ["plain", "login"]
|
||||
directory = "oidc"
|
||||
fallback-directory = "local"
|
||||
|
||||
"#,
|
||||
);
|
||||
} else {
|
||||
config.push_str(
|
||||
r#"
|
||||
[authentication]
|
||||
mechanisms = ["plain", "login"]
|
||||
directory = "local"
|
||||
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
fs::write(config_path, config).await?;
|
||||
Ok(())
|
||||
}
|
||||
7
src/core/package_manager/setup/mod.rs
Normal file
7
src/core/package_manager/setup/mod.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
pub mod directory_setup;
|
||||
pub mod email_setup;
|
||||
pub mod vector_db_setup;
|
||||
|
||||
pub use directory_setup::{DirectorySetup, DirectoryConfig, DefaultUser, CreateUserParams};
|
||||
pub use email_setup::EmailSetup;
|
||||
pub use vector_db_setup::VectorDbSetup;
|
||||
93
src/core/package_manager/setup/vector_db_setup.rs
Normal file
93
src/core/package_manager/setup/vector_db_setup.rs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
use anyhow::Result;
|
||||
use std::path::PathBuf;
|
||||
use std::fs;
|
||||
use tracing::info;
|
||||
|
||||
pub struct VectorDbSetup;
|
||||
|
||||
impl VectorDbSetup {
|
||||
pub async fn setup(conf_path: PathBuf, data_path: PathBuf) -> Result<()> {
|
||||
let config_dir = conf_path.join("vector_db");
|
||||
fs::create_dir_all(&config_dir)?;
|
||||
|
||||
let data_dir = data_path.join("vector_db");
|
||||
fs::create_dir_all(&data_dir)?;
|
||||
|
||||
let cert_dir = conf_path.join("system/certificates/vectordb");
|
||||
|
||||
// Convert to absolute paths for Qdrant config
|
||||
let data_dir_abs = fs::canonicalize(&data_dir).unwrap_or(data_dir);
|
||||
let cert_dir_abs = fs::canonicalize(&cert_dir).unwrap_or(cert_dir);
|
||||
|
||||
let config_content = generate_qdrant_config(&data_dir_abs, &cert_dir_abs);
|
||||
|
||||
let config_path = config_dir.join("config.yaml");
|
||||
fs::write(&config_path, config_content)?;
|
||||
|
||||
info!("Qdrant vector_db configuration written to {:?}", config_path);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_qdrant_config(data_dir: &std::path::Path, cert_dir: &std::path::Path) -> String {
|
||||
let data_path = data_dir.to_string_lossy();
|
||||
let cert_path = cert_dir.join("server.crt").to_string_lossy().to_string();
|
||||
let key_path = cert_dir.join("server.key").to_string_lossy().to_string();
|
||||
let ca_path = cert_dir.join("ca.crt").to_string_lossy().to_string();
|
||||
|
||||
format!(
|
||||
r#"# Qdrant configuration with TLS enabled
|
||||
# Generated by BotServer bootstrap
|
||||
|
||||
log_level: INFO
|
||||
|
||||
storage:
|
||||
storage_path: {data_path}
|
||||
snapshots_path: {data_path}/snapshots
|
||||
on_disk_payload: true
|
||||
|
||||
service:
|
||||
host: 0.0.0.0
|
||||
http_port: 6333
|
||||
grpc_port: 6334
|
||||
enable_tls: true
|
||||
|
||||
tls:
|
||||
cert: {cert_path}
|
||||
key: {key_path}
|
||||
ca_cert: {ca_path}
|
||||
verify_https_client_certificate: false
|
||||
|
||||
cluster:
|
||||
enabled: false
|
||||
|
||||
telemetry_disabled: true
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn generate_vector_db_config(config_path: PathBuf, data_path: PathBuf) -> Result<()> {
|
||||
VectorDbSetup::setup(config_path, data_path).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn test_generate_qdrant_config() {
|
||||
let data_dir = PathBuf::from("/tmp/qdrant/data");
|
||||
let cert_dir = PathBuf::from("/tmp/qdrant/certs");
|
||||
|
||||
let config = generate_qdrant_config(&data_dir, &cert_dir);
|
||||
|
||||
assert!(config.contains("enable_tls: true"));
|
||||
assert!(config.contains("http_port: 6333"));
|
||||
assert!(config.contains("grpc_port: 6334"));
|
||||
assert!(config.contains("/tmp/qdrant/data"));
|
||||
assert!(config.contains("/tmp/qdrant/certs/server.crt"));
|
||||
assert!(config.contains("/tmp/qdrant/certs/server.key"));
|
||||
}
|
||||
}
|
||||
|
|
@ -188,9 +188,6 @@ impl SessionManager {
|
|||
error!("Failed to create session in database: {}", e);
|
||||
e
|
||||
})?;
|
||||
|
||||
log::info!("User {} created resource: session {}", verified_uid, inserted.id);
|
||||
|
||||
Ok(inserted)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -186,8 +186,6 @@ pub async fn write_file(
|
|||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": e.to_string()}))))?;
|
||||
|
||||
log::info!("User system created resource: drive_file {}", key);
|
||||
|
||||
Ok(Json(serde_json::json!({"success": true})))
|
||||
}
|
||||
|
||||
|
|
@ -209,10 +207,6 @@ pub async fn delete_file(
|
|||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": e.to_string()}))))?;
|
||||
|
||||
// Technically a deletion, but we could log it as an audit change or leave it out
|
||||
// The plan specifies resource creation: `info!("User {} created resource: {}", user_id, resource_id);`
|
||||
log::info!("User system deleted resource: drive_file {}", file_id);
|
||||
|
||||
Ok(Json(serde_json::json!({"success": true})))
|
||||
}
|
||||
|
||||
|
|
@ -245,8 +239,6 @@ pub async fn create_folder(
|
|||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": e.to_string()}))))?;
|
||||
|
||||
log::info!("User system created resource: drive_folder {}", key);
|
||||
|
||||
Ok(Json(serde_json::json!({"success": true})))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ impl LocalFileMonitor {
|
|||
}
|
||||
|
||||
pub async fn start_monitoring(&self) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
info!("Local file monitor started - watching /opt/gbo/data/*.gbai directories");
|
||||
trace!("Starting local file monitor for /opt/gbo/data/*.gbai directories");
|
||||
|
||||
// Create data directory if it doesn't exist
|
||||
if let Err(e) = tokio::fs::create_dir_all(&self.data_dir).await {
|
||||
|
|
@ -68,7 +68,7 @@ impl LocalFileMonitor {
|
|||
monitor.monitoring_loop().await;
|
||||
});
|
||||
|
||||
debug!("Local file monitor successfully initialized");
|
||||
trace!("Local file monitor started");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -116,7 +116,7 @@ impl LocalFileMonitor {
|
|||
EventKind::Create(_) | EventKind::Modify(_) | EventKind::Any => {
|
||||
for path in &event.paths {
|
||||
if self.is_gbdialog_file(path) {
|
||||
debug!("Detected change in: {:?}", path);
|
||||
trace!("Detected change: {:?}", path);
|
||||
if let Err(e) = self.compile_local_file(path).await {
|
||||
error!("Failed to compile {:?}: {}", path, e);
|
||||
}
|
||||
|
|
@ -126,7 +126,7 @@ impl LocalFileMonitor {
|
|||
EventKind::Remove(_) => {
|
||||
for path in &event.paths {
|
||||
if self.is_gbdialog_file(path) {
|
||||
debug!("File removed: {:?}", path);
|
||||
trace!("File removed: {:?}", path);
|
||||
self.remove_file_state(path).await;
|
||||
}
|
||||
}
|
||||
|
|
@ -167,7 +167,7 @@ impl LocalFileMonitor {
|
|||
}
|
||||
|
||||
async fn scan_and_compile_all(&self) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
trace!("Scanning directory: {:?}", self.data_dir);
|
||||
debug!("[LOCAL_MONITOR] Scanning /opt/gbo/data for .gbai directories");
|
||||
|
||||
let entries = match tokio::fs::read_dir(&self.data_dir).await {
|
||||
Ok(e) => e,
|
||||
|
|
@ -203,7 +203,7 @@ impl LocalFileMonitor {
|
|||
}
|
||||
|
||||
async fn compile_gbdialog(&self, bot_name: &str, gbdialog_path: &Path) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
info!("Compiling bot: {}", bot_name);
|
||||
debug!("[LOCAL_MONITOR] Processing .gbdialog for bot: {}", bot_name);
|
||||
|
||||
let entries = tokio::fs::read_dir(gbdialog_path).await?;
|
||||
let mut entries = entries;
|
||||
|
|
@ -231,7 +231,7 @@ impl LocalFileMonitor {
|
|||
};
|
||||
|
||||
if should_compile {
|
||||
debug!("Recompiling {:?} - modification detected", path);
|
||||
trace!("Compiling: {:?}", path);
|
||||
if let Err(e) = self.compile_local_file(&path).await {
|
||||
error!("Failed to compile {:?}: {}", path, e);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -184,7 +184,7 @@ async fn main() -> std::io::Result<()> {
|
|||
e
|
||||
);
|
||||
} else {
|
||||
info!("Secrets loaded from Vault");
|
||||
info!("SecretsManager initialized - fetching secrets from Vault");
|
||||
}
|
||||
} else {
|
||||
trace!("Bootstrap not complete - skipping early SecretsManager init");
|
||||
|
|
@ -195,7 +195,6 @@ async fn main() -> std::io::Result<()> {
|
|||
aws_smithy_runtime=off,aws_smithy_runtime_api=off,aws_sdk_s3=off,aws_config=off,\
|
||||
aws_credential_types=off,aws_http=off,aws_sig_auth=off,aws_types=off,\
|
||||
mio=off,tokio=off,tokio_util=off,tower=off,tower_http=off,\
|
||||
tokio_tungstenite=off,tungstenite=off,\
|
||||
reqwest=off,hyper=off,hyper_util=off,h2=off,\
|
||||
rustls=off,rustls_pemfile=off,tokio_rustls=off,\
|
||||
tracing=off,tracing_core=off,tracing_subscriber=off,\
|
||||
|
|
@ -291,7 +290,6 @@ async fn main() -> std::io::Result<()> {
|
|||
dotenv().ok();
|
||||
|
||||
let pool = init_database(&progress_tx).await?;
|
||||
info!("Database initialized - PostgreSQL connected");
|
||||
let refreshed_cfg = load_config(&pool).await?;
|
||||
let config = std::sync::Arc::new(refreshed_cfg.clone());
|
||||
|
||||
|
|
@ -381,7 +379,7 @@ async fn main() -> std::io::Result<()> {
|
|||
trace!("Initial data setup task spawned");
|
||||
trace!("All system threads started, starting HTTP server...");
|
||||
|
||||
info!("Server started on port {}", config.server.port);
|
||||
info!("Starting HTTP server on port {}...", config.server.port);
|
||||
if let Err(e) = run_axum_server(app_state, config.server.port, worker_count).await {
|
||||
error!("Failed to start HTTP server: {}", e);
|
||||
std::process::exit(1);
|
||||
|
|
@ -394,3 +392,5 @@ async fn main() -> std::io::Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -302,7 +302,7 @@ pub async fn init_redis() -> Option<Arc<redis::Client>> {
|
|||
// Test with PING
|
||||
match redis::cmd("PING").query::<String>(&mut conn) {
|
||||
Ok(response) if response == "PONG" => {
|
||||
log::info!("Cache initialized - Valkey connected");
|
||||
log::info!("Cache connection verified: PONG");
|
||||
Ok(Some(Arc::new(client)))
|
||||
}
|
||||
Ok(response) => {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
|
|||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::info;
|
||||
use tracing::{debug, info};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
@ -490,14 +490,14 @@ impl JwtManager {
|
|||
claims.session_id,
|
||||
)?;
|
||||
|
||||
info!("Tokens refreshed for user {}", user_id);
|
||||
debug!("Refreshed tokens for user {user_id}");
|
||||
Ok(new_pair)
|
||||
}
|
||||
|
||||
pub async fn revoke_token(&self, jti: &str) -> Result<()> {
|
||||
let mut blacklist = self.blacklist.write().await;
|
||||
blacklist.insert(jti.to_string());
|
||||
info!("Token revoked: {}", jti);
|
||||
debug!("Revoked token {jti}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -189,11 +189,11 @@ error: Option<String>,
|
|||
#[cfg(feature = "mail")]
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct SmtpTestRequest {
|
||||
host: String,
|
||||
port: i32,
|
||||
username: Option<String>,
|
||||
password: Option<String>,
|
||||
_use_tls: Option<bool>,
|
||||
host: String,
|
||||
port: i32,
|
||||
username: Option<String>,
|
||||
password: Option<String>,
|
||||
use_tls: Option<bool>,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "mail"))]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue