- Created SelfFreezoneClient in Self components
- Wraps SDK FreezoneScriptClient for Self-specific operations
- Implements send_verification_email method
- Uses Rhai script template for email verification
- Includes template variable substitution
- Added serde-wasm-bindgen dependency
Usage:
let client = SelfFreezoneClient::builder()
.supervisor_url("http://localhost:8080")
.secret("my-secret")
.build()?;
client.send_verification_email(
"user@example.com",
"123456",
"https://verify.com/abc"
).await?;
17 KiB
17 KiB
Development Guide
Overview
This guide covers setting up a development environment, contributing to the Self project, and understanding the development workflow.
Development Environment Setup
Prerequisites
Required Tools
- Rust: Latest stable version (1.75+)
- Node.js: 18+ (for frontend tooling)
- Trunk: WASM build tool
- PostgreSQL: 12+ (for database development)
- Docker: For containerized development
- Git: Version control
Installation Commands
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.cargo/env
# Add WASM target
rustup target add wasm32-unknown-unknown
# Install Trunk
cargo install trunk
# Install Node.js (using nvm)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
nvm install 18
nvm use 18
# Install PostgreSQL (Ubuntu/Debian)
sudo apt-get install postgresql postgresql-contrib
# Install Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
Project Setup
Clone and Build
# Clone repository
git clone https://github.com/your-org/self.git
cd self
# Install dependencies and build
cargo build
# Build WASM components
cd app
trunk build
# Start development server
cd ../server
cargo run
# In another terminal, serve frontend
cd ../app
trunk serve
Development Database Setup
# Start PostgreSQL
sudo systemctl start postgresql
# Create development database
sudo -u postgres createdb selfdb_dev
sudo -u postgres createuser selfuser
sudo -u postgres psql -c "ALTER USER selfuser WITH PASSWORD 'devpassword';"
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE selfdb_dev TO selfuser;"
# Set environment variable
export DATABASE_URL="postgresql://selfuser:devpassword@localhost/selfdb_dev"
# Run migrations
cargo install sqlx-cli
sqlx migrate run
Development Configuration
Environment Variables
# .env.development
DATABASE_URL=postgresql://selfuser:devpassword@localhost/selfdb_dev
JWT_SECRET=dev-secret-key-not-for-production
SMTP_HOST=localhost
SMTP_PORT=1025
RUST_LOG=debug
BASE_URL=http://localhost:8080
VS Code Configuration
// .vscode/settings.json
{
"rust-analyzer.cargo.features": ["dev"],
"rust-analyzer.checkOnSave.command": "clippy",
"rust-analyzer.checkOnSave.extraArgs": ["--", "-W", "clippy::all"],
"files.associations": {
"*.rs": "rust"
},
"editor.formatOnSave": true,
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer"
}
}
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug Server",
"cargo": {
"args": ["build", "--bin=server"],
"filter": {
"name": "server",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}/server"
}
]
}
Project Structure
Directory Layout
self/
├── components/ # Reusable Yew components
│ ├── src/
│ │ ├── crypto.rs # Cryptographic utilities
│ │ ├── vault.rs # Vault storage system
│ │ ├── vault_manager.rs # Vault management UI
│ │ ├── login.rs # Login component
│ │ ├── registration.rs # Registration component
│ │ ├── identity.rs # Identity management
│ │ ├── sign.rs # Digital signing
│ │ └── lib.rs # Component exports
│ ├── Cargo.toml
│ └── README.md
├── app/ # Reference application
│ ├── src/
│ │ └── lib.rs # Main application
│ ├── index.html # HTML template
│ ├── Trunk.toml # Trunk configuration
│ ├── serve.sh # Development server script
│ └── Cargo.toml
├── server/ # Backend server
│ ├── src/
│ │ └── main.rs # Server implementation
│ ├── migrations/ # Database migrations
│ └── Cargo.toml
├── docs/ # Documentation
├── tests/ # Integration tests
├── scripts/ # Development scripts
├── docker-compose.yml # Development containers
├── Cargo.toml # Workspace configuration
└── README.md
Component Architecture
// Component dependency graph
components/
├── crypto.rs // Core cryptographic functions
├── vault.rs // Low-level vault operations
├── vault_manager.rs // Vault UI component (depends on vault)
├── registration.rs // Registration flow (depends on crypto, vault)
├── login.rs // Login flow (depends on crypto, vault)
├── identity.rs // Identity display (depends on vault)
└── sign.rs // Signing operations (depends on crypto, vault)
Development Workflow
Git Workflow
Branch Strategy
# Main branches
main # Production-ready code
develop # Integration branch for features
# Feature branches
feature/vault-system # New feature development
bugfix/auth-issue # Bug fixes
hotfix/security-patch # Critical fixes for production
Commit Convention
# Commit message format
<type>(<scope>): <description>
# Types
feat: # New feature
fix: # Bug fix
docs: # Documentation changes
style: # Code style changes (formatting, etc.)
refactor: # Code refactoring
test: # Adding or updating tests
chore: # Maintenance tasks
# Examples
feat(vault): add multi-key storage support
fix(auth): resolve JWT token validation issue
docs(api): update authentication flow documentation
Development Commands
Common Tasks
# Format code
cargo fmt
# Run linter
cargo clippy
# Run tests
cargo test
# Run tests with coverage
cargo tarpaulin --out Html
# Check for security vulnerabilities
cargo audit
# Update dependencies
cargo update
# Build documentation
cargo doc --open
Frontend Development
# Start development server with hot reload
cd app
trunk serve --open
# Build for production
trunk build --release
# Run frontend tests
wasm-pack test --headless --firefox
Backend Development
# Start server with auto-reload
cd server
cargo watch -x run
# Run database migrations
sqlx migrate run
# Generate migration
sqlx migrate add create_users_table
# Check SQL queries at compile time
cargo sqlx prepare
Testing Strategy
Unit Tests
// Example unit test
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_key_generation() {
let keypair = generate_keypair().unwrap();
assert_eq!(keypair.private_key.len(), 64); // 32 bytes hex-encoded
assert_eq!(keypair.public_key.len(), 130); // 65 bytes hex-encoded
}
#[test]
fn test_encryption_roundtrip() {
let data = "test data";
let password = "test password";
let encrypted = encrypt_private_key(data, password).unwrap();
let decrypted = decrypt_private_key(&encrypted, password).unwrap();
assert_eq!(data, decrypted);
}
}
Integration Tests
// tests/integration_test.rs
use self_server::*;
use reqwest;
use tokio;
#[tokio::test]
async fn test_registration_flow() {
// Start test server
let server = start_test_server().await;
let client = reqwest::Client::new();
// Test email verification
let response = client
.post(&format!("{}/api/send-verification", server.url()))
.json(&json!({"email": "test@example.com"}))
.send()
.await
.unwrap();
assert_eq!(response.status(), 200);
// Test registration
let keypair = generate_keypair().unwrap();
let response = client
.post(&format!("{}/api/register", server.url()))
.json(&json!({
"email": "test@example.com",
"name": "Test User",
"public_key": keypair.public_key
}))
.send()
.await
.unwrap();
assert_eq!(response.status(), 200);
}
WASM Tests
// tests/wasm_tests.rs
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn test_crypto_in_browser() {
let keypair = generate_keypair().unwrap();
assert!(!keypair.private_key.is_empty());
assert!(!keypair.public_key.is_empty());
}
End-to-End Tests
// e2e/registration.spec.js
const { test, expect } = require('@playwright/test');
test('user registration flow', async ({ page }) => {
await page.goto('http://localhost:8000');
// Fill registration form
await page.fill('[data-testid="name-input"]', 'Test User');
await page.fill('[data-testid="email-input"]', 'test@example.com');
await page.click('[data-testid="send-verification"]');
// Wait for verification (in test environment)
await page.waitForSelector('[data-testid="email-verified"]');
// Generate keys
await page.click('[data-testid="generate-keys"]');
await page.fill('[data-testid="password-input"]', 'testpassword123');
await page.fill('[data-testid="confirm-password"]', 'testpassword123');
// Complete registration
await page.click('[data-testid="complete-registration"]');
await expect(page.locator('[data-testid="registration-success"]')).toBeVisible();
});
Code Style and Standards
Rust Style Guide
Naming Conventions
// Use snake_case for functions and variables
fn generate_keypair() -> Result<KeyPair, String> { }
let private_key = "...";
// Use PascalCase for types and traits
struct KeyPair { }
trait VaultStorage { }
// Use SCREAMING_SNAKE_CASE for constants
const DEFAULT_ITERATIONS: u32 = 10_000;
// Use descriptive names
fn encrypt_private_key_with_password() // Good
fn encrypt() // Too generic
Error Handling
// Use Result types for fallible operations
fn risky_operation() -> Result<String, MyError> {
// Implementation
}
// Use ? operator for error propagation
fn calling_function() -> Result<(), MyError> {
let result = risky_operation()?;
Ok(())
}
// Provide context for errors
fn with_context() -> Result<(), Box<dyn std::error::Error>> {
risky_operation()
.map_err(|e| format!("Failed to perform risky operation: {}", e))?;
Ok(())
}
Documentation
/// Generates a new secp256k1 key pair for cryptographic operations.
///
/// This function uses cryptographically secure random number generation
/// to create a private key, then derives the corresponding public key.
///
/// # Returns
///
/// Returns a `Result` containing a `KeyPair` on success, or a `String`
/// error message on failure.
///
/// # Errors
///
/// This function will return an error if:
/// - Random number generation fails
/// - Key validation fails
/// - Public key derivation fails
///
/// # Examples
///
/// ```
/// use self_components::generate_keypair;
///
/// let keypair = generate_keypair().expect("Failed to generate keypair");
/// println!("Public key: {}", keypair.public_key);
/// ```
pub fn generate_keypair() -> Result<KeyPair, String> {
// Implementation
}
Frontend Style Guide
Component Structure
// Component organization
pub struct MyComponent {
// State fields
loading: bool,
error_message: Option<String>,
data: Option<MyData>,
}
pub enum MyMsg {
// User actions
LoadData,
UpdateField(String),
Submit,
// Async responses
DataLoaded(MyData),
LoadFailed(String),
// UI events
ClearError,
}
impl Component for MyComponent {
type Message = MyMsg;
type Properties = MyProps;
fn create(ctx: &Context<Self>) -> Self {
// Initialize component
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
// Handle messages
}
fn view(&self, ctx: &Context<Self>) -> Html {
// Render component
}
}
HTML Structure
// Use semantic HTML and proper accessibility
fn view(&self, ctx: &Context<Self>) -> Html {
html! {
<main class="container" role="main">
<h1>{"Page Title"}</h1>
<form onsubmit={ctx.link().callback(|e: SubmitEvent| {
e.prevent_default();
MyMsg::Submit
})}>
<div class="form-group">
<label for="email-input">{"Email Address"}</label>
<input
id="email-input"
type="email"
class="form-control"
required=true
aria-describedby="email-help"
value={self.email.clone()}
oninput={ctx.link().callback(|e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
MyMsg::UpdateEmail(input.value())
})}
/>
<small id="email-help" class="form-text text-muted">
{"We'll never share your email with anyone else."}
</small>
</div>
<button type="submit" class="btn btn-primary" disabled={self.loading}>
{if self.loading { "Loading..." } else { "Submit" }}
</button>
</form>
</main>
}
}
Debugging and Troubleshooting
Common Issues
WASM Build Failures
# Clear trunk cache
trunk clean
# Rebuild with verbose output
trunk build --verbose
# Check for missing dependencies
cargo check --target wasm32-unknown-unknown
Database Connection Issues
# Check PostgreSQL status
sudo systemctl status postgresql
# Test connection
psql -h localhost -U selfuser -d selfdb_dev
# Reset database
dropdb selfdb_dev && createdb selfdb_dev
sqlx migrate run
CORS Issues
// Add CORS middleware in development
use tower_http::cors::{CorsLayer, Any};
let app = Router::new()
.layer(
CorsLayer::new()
.allow_origin(Any)
.allow_methods(Any)
.allow_headers(Any)
);
Debugging Tools
Browser DevTools
// Enable debug logging in browser
localStorage.setItem('debug', 'self:*');
// View WASM memory usage
console.log(performance.memory);
// Inspect localStorage
console.log(localStorage.getItem('self_vault'));
Server Debugging
// Add debug logging
use tracing::{debug, info, warn, error};
debug!("Processing request: {:?}", request);
info!("User authenticated: {}", user_id);
warn!("Rate limit approaching: {}", current_rate);
error!("Database connection failed: {}", error);
Contributing Guidelines
Pull Request Process
-
Fork and Branch
git checkout -b feature/my-new-feature -
Make Changes
- Follow code style guidelines
- Add tests for new functionality
- Update documentation
-
Test Changes
cargo test cargo clippy cargo fmt --check -
Submit PR
- Write clear commit messages
- Include description of changes
- Reference related issues
Code Review Checklist
Functionality
- Code works as intended
- Edge cases are handled
- Error conditions are properly managed
- Performance is acceptable
Security
- Input validation is present
- No secrets in code
- Cryptographic operations are correct
- Authentication/authorization is proper
Code Quality
- Code is readable and well-documented
- Tests are comprehensive
- No code duplication
- Follows project conventions
Documentation
- Public APIs are documented
- README is updated if needed
- Breaking changes are noted
- Examples are provided
Release Process
Version Numbering
# Semantic versioning: MAJOR.MINOR.PATCH
1.0.0 # Initial release
1.0.1 # Bug fix
1.1.0 # New feature
2.0.0 # Breaking change
Release Checklist
-
Pre-release
- All tests pass
- Documentation updated
- CHANGELOG.md updated
- Version numbers bumped
-
Release
- Create release tag
- Build release artifacts
- Deploy to staging
- Run integration tests
-
Post-release
- Deploy to production
- Monitor for issues
- Update documentation site
- Announce release
This development guide provides everything needed to contribute to the Self project effectively, from initial setup through the release process.