diff --git a/CRITICAL_SECURITY_AUDIT_REPORT.md b/CRITICAL_SECURITY_AUDIT_REPORT.md index 89dd2ac..adc91ad 100644 --- a/CRITICAL_SECURITY_AUDIT_REPORT.md +++ b/CRITICAL_SECURITY_AUDIT_REPORT.md @@ -73,4 +73,4 @@ This document outlines the comprehensive security measures implemented in the Au All critical security vulnerabilities have been addressed and secured. The AuthFramework implements industry best practices for authentication and authorization security. ## Last Updated -Generated automatically as part of comprehensive security testing suite. \ No newline at end of file +Generated automatically as part of comprehensive security testing suite. diff --git a/SDK_REPOSITORY_SPLIT_GUIDE.md b/SDK_REPOSITORY_SPLIT_GUIDE.md new file mode 100644 index 0000000..031ba8e --- /dev/null +++ b/SDK_REPOSITORY_SPLIT_GUIDE.md @@ -0,0 +1,261 @@ +# SDK Repository Split Guide + +This document outlines the process for splitting the Python and JavaScript SDKs from the main AuthFramework repository into their own independent repositories. + +## Overview + +The SDKs are being split to provide: +- **Focused Development**: Each SDK can have its own release cycle and versioning +- **Smaller Downloads**: Users only clone the SDK they need +- **Independent CI/CD**: Separate testing and deployment pipelines +- **Better Collaboration**: SDK-specific contributors don't need the full monorepo +- **Package Management**: Direct publishing to PyPI and npm without monorepo complexity + +## Repository Structure + +### Python SDK Repository: `ciresnave/authframework-python` + +``` +authframework-python/ +├── .github/ +│ └── workflows/ +│ ├── ci.yml +│ └── release.yml +├── .vscode/ +│ └── settings.json +├── src/ +│ └── authframework/ +│ ├── __init__.py +│ ├── client.py +│ ├── _auth.py +│ ├── _admin.py +│ ├── _base.py +│ ├── _tokens.py +│ ├── exceptions.py +│ ├── models/ +│ └── integrations/ +├── tests/ +├── examples/ +├── docs/ +├── pyproject.toml +├── pyrightconfig.json +├── README.md +├── LICENSE +├── CHANGELOG.md +├── CONTRIBUTING.md +└── authframework-python-sdk.code-workspace +``` + +### JavaScript SDK Repository: `ciresnave/authframework-js` + +``` +authframework-js/ +├── .github/ +│ └── workflows/ +│ ├── ci.yml +│ └── release.yml +├── src/ +│ ├── auth/ +│ ├── admin/ +│ ├── tokens/ +│ ├── types/ +│ ├── errors/ +│ ├── utils/ +│ └── index.ts +├── dist/ +├── tests/ +├── examples/ +├── docs/ +├── package.json +├── tsconfig.json +├── rollup.config.js +├── jest.config.js +├── README.md +├── LICENSE +├── CHANGELOG.md +└── CONTRIBUTING.md +``` + +## Migration Steps + +### 1. Create New Repositories + +```bash +# Create Python SDK repository +gh repo create ciresnave/authframework-python --public --description "Official Python SDK for AuthFramework" + +# Create JavaScript SDK repository +gh repo create ciresnave/authframework-js --public --description "Official JavaScript/TypeScript SDK for AuthFramework" +``` + +### 2. Prepare Python SDK + +```bash +# Navigate to Python SDK directory +cd /path/to/AuthFramework/sdks/python + +# Initialize git repository +git init +git add . +git commit -m "feat: initial Python SDK repository setup" + +# Add remote and push +git remote add origin https://github.com/ciresnave/authframework-python.git +git branch -M main +git push -u origin main +``` + +### 3. Prepare JavaScript SDK + +```bash +# Navigate to JavaScript SDK directory +cd /path/to/AuthFramework/sdks/javascript + +# Initialize git repository +git init +git add . +git commit -m "feat: initial JavaScript SDK repository setup" + +# Add remote and push +git remote add origin https://github.com/ciresnave/authframework-js.git +git branch -M main +git push -u origin main +``` + +### 4. Update Package Registries + +#### Python SDK (PyPI) +- Package name: `authframework` +- Repository: `https://github.com/ciresnave/authframework-python` +- Update `pyproject.toml` URLs +- Configure GitHub Actions for PyPI publishing + +#### JavaScript SDK (npm) +- Package name: `@authframework/js-sdk` +- Repository: `https://github.com/ciresnave/authframework-js` +- Update `package.json` URLs +- Configure GitHub Actions for npm publishing + +### 5. GitHub Repository Settings + +#### Python SDK Repository Settings +- **Secrets**: Add `PYPI_API_TOKEN` for automated publishing +- **Branch Protection**: Require PR reviews for main branch +- **Issues**: Enable with templates +- **Discussions**: Enable for community support +- **Wiki**: Enable for extended documentation +- **Topics**: `python`, `sdk`, `authentication`, `authorization`, `jwt` + +#### JavaScript SDK Repository Settings +- **Secrets**: Add `NPM_TOKEN` for automated publishing +- **Branch Protection**: Require PR reviews for main branch +- **Issues**: Enable with templates +- **Discussions**: Enable for community support +- **Wiki**: Enable for extended documentation +- **Topics**: `javascript`, `typescript`, `sdk`, `authentication`, `authorization`, `jwt` + +### 6. Documentation Updates + +#### Update Main Repository README +Remove SDK documentation and add links to new repositories: + +```markdown +## SDKs + +AuthFramework provides official SDKs for multiple programming languages: + +- **Python**: [authframework/authframework-python](https://github.com/ciresnave/authframework-python) +- **JavaScript/TypeScript**: [authframework/authframework-js](https://github.com/ciresnave/authframework-js) +``` + +#### Update SDK Documentation +- Create comprehensive README files for each SDK +- Set up documentation websites (ReadTheDocs for Python, GitHub Pages for JS) +- Update API documentation links +- Create migration guides for existing users + +### 7. CI/CD Pipeline Setup + +#### Python SDK Pipeline +- **Testing**: pytest with coverage on multiple Python versions (3.9-3.12) +- **Linting**: black, flake8, isort, mypy +- **Security**: bandit, safety +- **Publishing**: Automatic PyPI releases on git tags +- **Documentation**: Automatic docs building and deployment + +#### JavaScript SDK Pipeline +- **Testing**: Jest with coverage on multiple Node.js versions (16, 18, 20) +- **Linting**: ESLint, Prettier +- **Type Checking**: TypeScript compiler +- **Building**: Rollup for ESM and CommonJS builds +- **Publishing**: Automatic npm releases on git tags +- **Documentation**: Automatic docs building and deployment + +### 8. Migration Timeline + +1. **Week 1**: Repository setup and basic file migration +2. **Week 2**: CI/CD pipeline configuration and testing +3. **Week 3**: Package registry setup and initial releases +4. **Week 4**: Documentation updates and community communication +5. **Ongoing**: Monitor for issues and gather feedback + +## Benefits After Split + +### For Users +- **Faster Setup**: Only download the SDK they need +- **Clear Documentation**: SDK-specific docs without monorepo complexity +- **Better Support**: Dedicated issue tracking per SDK +- **Framework Focus**: Each SDK optimized for its language ecosystem + +### For Maintainers +- **Independent Releases**: SDK versions not tied to main project +- **Focused PRs**: Changes specific to each SDK +- **Specialized CI**: Testing pipelines optimized for each language +- **Clear Ownership**: Dedicated maintainers per SDK + +### For the Main Project +- **Reduced Complexity**: Main repo focuses on core Rust implementation +- **Faster CI**: No need to test all SDKs on core changes +- **Modular Architecture**: Clear separation of concerns +- **Easier Onboarding**: New contributors can focus on specific areas + +## Backwards Compatibility + +### Existing Package Names +- Python: `authframework` package name remains the same +- JavaScript: `@authframework/js-sdk` package name remains the same + +### Import Statements +No changes required in user code: + +```python +# Python - remains the same +from authframework import AuthFrameworkClient +``` + +```javascript +// JavaScript - remains the same +import { AuthFrameworkClient } from '@authframework/js-sdk'; +``` + +### Migration Communication +- Deprecation notices in old repository locations +- Clear migration guides in documentation +- Community announcements on GitHub Discussions +- Blog post explaining the benefits of the split + +## Maintenance Strategy + +### Ongoing Responsibilities +- **Core Team**: Maintain Rust implementation and coordinate SDK updates +- **Python Team**: Maintain Python SDK, respond to Python-specific issues +- **JavaScript Team**: Maintain JS SDK, respond to JS-specific issues +- **Community**: Contribute to all repositories based on expertise + +### Coordination +- Regular sync meetings between SDK maintainers +- Shared issues for cross-SDK concerns +- Consistent API design across SDKs +- Coordinated security updates + +This split provides a foundation for long-term sustainable development of the AuthFramework ecosystem while maintaining backwards compatibility and improving the developer experience. \ No newline at end of file diff --git a/SECURITY_AUDIT.md b/SECURITY_AUDIT.md index 7ea90db..d3bf405 100644 --- a/SECURITY_AUDIT.md +++ b/SECURITY_AUDIT.md @@ -6,10 +6,10 @@ This document explains the security advisories that are currently allowed in the ### RUSTSEC-2023-0071: RSA Marvin Attack (Medium Severity) -**Status**: Temporarily Allowed -**Affected Crate**: `rsa 0.9.8` -**Used By**: `sqlx-mysql`, `openidconnect` -**Issue**: Potential key recovery through timing sidechannels +**Status**: Temporarily Allowed +**Affected Crate**: `rsa 0.9.8` +**Used By**: `sqlx-mysql`, `openidconnect` +**Issue**: Potential key recovery through timing sidechannels **Risk Assessment**: **LOW** - AuthFramework does not directly expose RSA operations to untrusted input @@ -26,12 +26,12 @@ This document explains the security advisories that are currently allowed in the ### RUSTSEC-2024-0436: Paste Crate Unmaintained -**Status**: Temporarily Allowed -**Affected Crate**: `paste 1.0.15` -**Used By**: `ratatui` → `tui-input` (TUI features only) -**Issue**: Crate is no longer maintained +**Status**: Temporarily Allowed +**Affected Crate**: `paste 1.0.15` +**Used By**: `ratatui` → `tui-input` (TUI features only) +**Issue**: Crate is no longer maintained -**Risk Assessment**: **VERY LOW** +**Risk Assessment**: **VERY LOW** - Used only in optional TUI admin interface features - `paste` is a macro-only crate with minimal security surface - Functionality is stable and well-tested @@ -45,7 +45,7 @@ This document explains the security advisories that are currently allowed in the ## Security Policy 1. **Regular Reviews**: Security exceptions are reviewed monthly -2. **Automatic Updates**: Dependencies are updated automatically when fixes become available +2. **Automatic Updates**: Dependencies are updated automatically when fixes become available 3. **Monitoring**: We actively monitor RustSec advisory database for new issues 4. **Escalation**: High or critical severity issues require immediate attention @@ -53,4 +53,4 @@ This document explains the security advisories that are currently allowed in the For security concerns, please see our [Security Policy](SECURITY.md) or contact the maintainers directly. -Last Updated: September 28, 2025 \ No newline at end of file +Last Updated: September 28, 2025 diff --git a/deny.toml b/deny.toml index 17c366c..3350248 100644 --- a/deny.toml +++ b/deny.toml @@ -39,7 +39,7 @@ wildcards = "allow" # Allow wildcard dependencies [sources] - # Source repository settings + # Source repository settings allow-git = [] allow-registry = ["https://github.com/rust-lang/crates.io-index"] unknown-git = "warn" # Warn about unknown git sources diff --git a/docs/api/README.md b/docs/api/README.md index 271f3a4..835add3 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -186,10 +186,19 @@ Configurable Cross-Origin Resource Sharing support for web applications. ## Client SDKs +AuthFramework provides professional, production-ready SDKs in separate repositories: + ### JavaScript/TypeScript SDK +📦 **Repository**: https://github.com/ciresnave/authframework-js +📚 **Documentation**: See repository README for full documentation + +```bash +npm install @authframework/client +``` + ```typescript -import { AuthFrameworkClient } from '@authframework/js-sdk'; +import { AuthFrameworkClient } from '@authframework/client'; const client = new AuthFrameworkClient({ baseUrl: 'http://localhost:8080', @@ -211,6 +220,10 @@ const profile = await client.users.getProfile(); ### Python SDK +📦 **Repository**: https://github.com/ciresnave/authframework-python +📊 **PyPI**: `pip install authframework` +📚 **Documentation**: See repository README for full documentation + ```python from authframework import AuthFrameworkClient @@ -234,32 +247,32 @@ profile = client.users.get_profile() ## Error Codes -| Code | Description | -|------|-------------| -| `INVALID_CREDENTIALS` | Username or password is incorrect | -| `TOKEN_EXPIRED` | Access token has expired | -| `TOKEN_INVALID` | Access token is malformed or invalid | -| `INSUFFICIENT_PERMISSIONS` | User lacks required permissions | -| `RATE_LIMIT_EXCEEDED` | Too many requests in time window | -| `MFA_REQUIRED` | Multi-factor authentication required | -| `MFA_INVALID_CODE` | Invalid MFA verification code | -| `USER_NOT_FOUND` | Requested user does not exist | -| `EMAIL_ALREADY_EXISTS` | Email address already registered | -| `VALIDATION_ERROR` | Request validation failed | -| `INTERNAL_ERROR` | Internal server error | +| Code | Description | +| -------------------------- | ------------------------------------ | +| `INVALID_CREDENTIALS` | Username or password is incorrect | +| `TOKEN_EXPIRED` | Access token has expired | +| `TOKEN_INVALID` | Access token is malformed or invalid | +| `INSUFFICIENT_PERMISSIONS` | User lacks required permissions | +| `RATE_LIMIT_EXCEEDED` | Too many requests in time window | +| `MFA_REQUIRED` | Multi-factor authentication required | +| `MFA_INVALID_CODE` | Invalid MFA verification code | +| `USER_NOT_FOUND` | Requested user does not exist | +| `EMAIL_ALREADY_EXISTS` | Email address already registered | +| `VALIDATION_ERROR` | Request validation failed | +| `INTERNAL_ERROR` | Internal server error | ## Configuration ### Environment Variables -| Variable | Description | Default | -|----------|-------------|---------| -| `AUTH_API_HOST` | API server host | `127.0.0.1` | -| `AUTH_API_PORT` | API server port | `8080` | -| `AUTH_API_CORS_ENABLED` | Enable CORS | `true` | -| `AUTH_API_MAX_BODY_SIZE` | Max request body size | `1048576` (1MB) | -| `AUTH_JWT_SECRET` | JWT signing secret | *(required)* | -| `AUTH_TOKEN_EXPIRY` | Access token lifetime | `3600` (1 hour) | +| Variable | Description | Default | +| --------------------------- | ---------------------- | ----------------- | +| `AUTH_API_HOST` | API server host | `127.0.0.1` | +| `AUTH_API_PORT` | API server port | `8080` | +| `AUTH_API_CORS_ENABLED` | Enable CORS | `true` | +| `AUTH_API_MAX_BODY_SIZE` | Max request body size | `1048576` (1MB) | +| `AUTH_JWT_SECRET` | JWT signing secret | *(required)* | +| `AUTH_TOKEN_EXPIRY` | Access token lifetime | `3600` (1 hour) | | `AUTH_REFRESH_TOKEN_EXPIRY` | Refresh token lifetime | `604800` (7 days) | ### Programmatic Configuration @@ -774,15 +787,20 @@ X-RateLimit-Reset: 1640995200 ## SDKs and Libraries -### JavaScript/Node.js +### JavaScript/TypeScript SDK + +**Repository**: [authframework-js](https://github.com/ciresnave/authframework-js) + +```bash +npm install @authframework/client +``` ```javascript -import { AuthFrameworkClient } from '@auth-framework/client'; +import { AuthFrameworkClient } from '@authframework/client'; const client = new AuthFrameworkClient({ baseUrl: 'https://api.yourdomain.com', - clientId: 'your_client_id', - clientSecret: 'your_client_secret' + apiKey: 'your_api_key' }); // Login @@ -792,18 +810,23 @@ const tokens = await client.auth.login({ }); // Get profile -const profile = await client.users.getProfile(tokens.access_token); +const profile = await client.users.getProfile(); ``` -### Python +### Python SDK + +**Repository**: [authframework-python](https://github.com/ciresnave/authframework-python) + +```bash +pip install authframework +``` ```python -from auth_framework import AuthFrameworkClient +from authframework import AuthFrameworkClient client = AuthFrameworkClient( base_url='https://api.yourdomain.com', - client_id='your_client_id', - client_secret='your_client_secret' + api_key='your_api_key' ) # Login @@ -813,28 +836,22 @@ tokens = client.auth.login( ) # Get profile -profile = client.users.get_profile(tokens['access_token']) +profile = client.users.get_profile() ``` -### Rust +### Rust (Core Library) + +AuthFramework provides the complete server-side implementation in Rust: ```rust -use auth_framework_client::AuthFrameworkClient; +use auth_framework::{AuthFramework, AuthConfig}; -let client = AuthFrameworkClient::new( - "https://api.yourdomain.com", - "your_client_id", - "your_client_secret" -); +let config = AuthConfig::new() + .secret("your-jwt-secret".to_string()); -// Login -let tokens = client.auth().login( - "user@example.com", - "password", - None -).await?; +let auth = AuthFramework::new(config); -// Get profile +// Full server implementation - see main documentation let profile = client.users().get_profile(&tokens.access_token).await?; ``` diff --git a/docs/guides/custom-storage-implementation.md b/docs/guides/custom-storage-implementation.md new file mode 100644 index 0000000..c292f36 --- /dev/null +++ b/docs/guides/custom-storage-implementation.md @@ -0,0 +1,756 @@ +# Custom Storage Backend Implementation Guide + +This guide shows you how to create a custom storage backend for AuthFramework, using SurrealDB as an example. This follows the Dependency Inversion Principle (DIP) by depending on the `AuthStorage` abstraction. + +## Overview + +AuthFramework uses the `AuthStorage` trait to abstract storage operations. Any storage backend that implements this trait can be used with the framework, providing maximum flexibility while maintaining type safety. + +## Step 1: Understand the AuthStorage Trait + +The core trait you must implement: + +```rust +#[async_trait] +pub trait AuthStorage: Send + Sync { + // Token operations + async fn store_token(&self, token: &AuthToken) -> Result<()>; + async fn get_token(&self, token_id: &str) -> Result>; + async fn get_token_by_access_token(&self, access_token: &str) -> Result>; + async fn update_token(&self, token: &AuthToken) -> Result<()>; + async fn delete_token(&self, token_id: &str) -> Result<()>; + async fn list_user_tokens(&self, user_id: &str) -> Result>; + + // Session operations + async fn store_session(&self, session_id: &str, data: &SessionData) -> Result<()>; + async fn get_session(&self, session_id: &str) -> Result>; + async fn delete_session(&self, session_id: &str) -> Result<()>; + async fn list_user_sessions(&self, user_id: &str) -> Result>; + async fn count_active_sessions(&self) -> Result; + + // Key-value operations + async fn store_kv(&self, key: &str, value: &[u8], ttl: Option) -> Result<()>; + async fn get_kv(&self, key: &str) -> Result>>; + async fn delete_kv(&self, key: &str) -> Result<()>; + + // Cleanup operations + async fn cleanup_expired(&self) -> Result<()>; + + // Bulk operations (optional with default implementations) + async fn store_tokens_bulk(&self, tokens: &[AuthToken]) -> Result<()> { + for token in tokens { + self.store_token(token).await?; + } + Ok(()) + } + + async fn delete_tokens_bulk(&self, token_ids: &[String]) -> Result<()> { + for token_id in token_ids { + self.delete_token(token_id).await?; + } + Ok(()) + } + + async fn store_sessions_bulk(&self, sessions: &[(String, SessionData)]) -> Result<()> { + for (session_id, data) in sessions { + self.store_session(session_id, data).await?; + } + Ok(()) + } + + async fn delete_sessions_bulk(&self, session_ids: &[String]) -> Result<()> { + for session_id in session_ids { + self.delete_session(session_id).await?; + } + Ok(()) + } +} +``` + +## Step 2: Create Your Storage Implementation + +Here's a complete SurrealDB implementation example: + +```rust +use auth_framework::{ + errors::{AuthError, Result}, + storage::{AuthStorage, SessionData}, + tokens::AuthToken, +}; +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use surrealdb::{Surreal, engine::remote::ws::{Client, Ws}}; + +/// SurrealDB storage backend for AuthFramework +#[derive(Clone)] +pub struct SurrealStorage { + db: Surreal, + namespace: String, + database: String, +} + +/// Configuration for SurrealDB storage +#[derive(Debug, Clone)] +pub struct SurrealConfig { + pub url: String, + pub namespace: String, + pub database: String, + pub username: Option, + pub password: Option, +} + +impl Default for SurrealConfig { + fn default() -> Self { + Self { + url: "ws://localhost:8000".to_string(), + namespace: "authframework".to_string(), + database: "auth".to_string(), + username: None, + password: None, + } + } +} + +// Internal data structures for SurrealDB +#[derive(Debug, Serialize, Deserialize)] +struct TokenRecord { + id: String, + user_id: String, + access_token: String, + refresh_token: Option, + token_type: String, + expires_at: i64, + created_at: i64, + scopes: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +struct SessionRecord { + id: String, + user_id: String, + data: serde_json::Value, + created_at: i64, + last_accessed: i64, + expires_at: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +struct KvRecord { + id: String, + value: Vec, + expires_at: Option, + created_at: i64, +} + +impl SurrealStorage { + /// Create a new SurrealDB storage instance + pub async fn new(config: SurrealConfig) -> Result { + // Connect to SurrealDB + let db = Surreal::new::(&config.url) + .await + .map_err(|e| AuthError::internal(format!("SurrealDB connection failed: {}", e)))?; + + // Authenticate if credentials provided + if let (Some(username), Some(password)) = (&config.username, &config.password) { + db.signin(surrealdb::opt::auth::Root { + username, + password, + }) + .await + .map_err(|e| AuthError::internal(format!("SurrealDB auth failed: {}", e)))?; + } + + // Use namespace and database + db.use_ns(&config.namespace) + .use_db(&config.database) + .await + .map_err(|e| AuthError::internal(format!("Failed to use namespace/database: {}", e)))?; + + let storage = Self { + db, + namespace: config.namespace, + database: config.database, + }; + + // Initialize schema + storage.initialize_schema().await?; + + Ok(storage) + } + + /// Convenience constructor with default configuration + pub async fn connect(url: &str) -> Result { + let config = SurrealConfig { + url: url.to_string(), + ..Default::default() + }; + Self::new(config).await + } + + /// Initialize database schema + async fn initialize_schema(&self) -> Result<()> { + // Define tables and indexes + let schema_queries = vec![ + // Tokens table + "DEFINE TABLE tokens SCHEMAFULL;", + "DEFINE FIELD user_id ON TABLE tokens TYPE string;", + "DEFINE FIELD access_token ON TABLE tokens TYPE string;", + "DEFINE FIELD refresh_token ON TABLE tokens TYPE option;", + "DEFINE FIELD token_type ON TABLE tokens TYPE string;", + "DEFINE FIELD expires_at ON TABLE tokens TYPE int;", + "DEFINE FIELD created_at ON TABLE tokens TYPE int;", + "DEFINE FIELD scopes ON TABLE tokens TYPE array;", + "DEFINE INDEX idx_tokens_access_token ON TABLE tokens COLUMNS access_token UNIQUE;", + "DEFINE INDEX idx_tokens_user_id ON TABLE tokens COLUMNS user_id;", + "DEFINE INDEX idx_tokens_expires_at ON TABLE tokens COLUMNS expires_at;", + + // Sessions table + "DEFINE TABLE sessions SCHEMAFULL;", + "DEFINE FIELD user_id ON TABLE sessions TYPE string;", + "DEFINE FIELD data ON TABLE sessions TYPE object;", + "DEFINE FIELD created_at ON TABLE sessions TYPE int;", + "DEFINE FIELD last_accessed ON TABLE sessions TYPE int;", + "DEFINE FIELD expires_at ON TABLE sessions TYPE option;", + "DEFINE INDEX idx_sessions_user_id ON TABLE sessions COLUMNS user_id;", + "DEFINE INDEX idx_sessions_expires_at ON TABLE sessions COLUMNS expires_at;", + + // Key-value table + "DEFINE TABLE kv SCHEMAFULL;", + "DEFINE FIELD value ON TABLE kv TYPE bytes;", + "DEFINE FIELD expires_at ON TABLE kv TYPE option;", + "DEFINE FIELD created_at ON TABLE kv TYPE int;", + "DEFINE INDEX idx_kv_expires_at ON TABLE kv COLUMNS expires_at;", + ]; + + for query in schema_queries { + self.db + .query(query) + .await + .map_err(|e| AuthError::internal(format!("Schema creation failed: {}", e)))?; + } + + Ok(()) + } + + /// Convert AuthToken to TokenRecord + fn token_to_record(token: &AuthToken) -> TokenRecord { + TokenRecord { + id: format!("tokens:{}", token.token_id), + user_id: token.user_id.clone(), + access_token: token.access_token.clone(), + refresh_token: token.refresh_token.clone(), + token_type: token.token_type.clone(), + expires_at: token.expires_at.timestamp(), + created_at: token.created_at.timestamp(), + scopes: token.scopes.clone(), + } + } + + /// Convert TokenRecord to AuthToken + fn record_to_token(record: TokenRecord) -> Result { + use chrono::{DateTime, Utc}; + + Ok(AuthToken { + token_id: record.id.strip_prefix("tokens:").unwrap_or(&record.id).to_string(), + user_id: record.user_id, + access_token: record.access_token, + refresh_token: record.refresh_token, + token_type: record.token_type, + expires_at: DateTime::from_timestamp(record.expires_at, 0) + .ok_or_else(|| AuthError::internal("Invalid expires_at timestamp".to_string()))?, + created_at: DateTime::from_timestamp(record.created_at, 0) + .ok_or_else(|| AuthError::internal("Invalid created_at timestamp".to_string()))?, + scopes: record.scopes, + }) + } + + /// Get current timestamp + fn current_timestamp() -> i64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs() as i64 + } +} + +#[async_trait] +impl AuthStorage for SurrealStorage { + async fn store_token(&self, token: &AuthToken) -> Result<()> { + let record = Self::token_to_record(token); + + self.db + .create(("tokens", &token.token_id)) + .content(record) + .await + .map_err(|e| AuthError::internal(format!("Failed to store token: {}", e)))?; + + Ok(()) + } + + async fn get_token(&self, token_id: &str) -> Result> { + let record: Option = self.db + .select(("tokens", token_id)) + .await + .map_err(|e| AuthError::internal(format!("Failed to get token: {}", e)))?; + + match record { + Some(record) => { + // Check if token is expired + let now = Self::current_timestamp(); + if record.expires_at <= now { + // Token is expired, delete it and return None + let _ = self.delete_token(token_id).await; + return Ok(None); + } + Ok(Some(Self::record_to_token(record)?)) + } + None => Ok(None), + } + } + + async fn get_token_by_access_token(&self, access_token: &str) -> Result> { + let mut response = self.db + .query("SELECT * FROM tokens WHERE access_token = $access_token LIMIT 1") + .bind(("access_token", access_token)) + .await + .map_err(|e| AuthError::internal(format!("Failed to query token: {}", e)))?; + + let records: Vec = response + .take(0) + .map_err(|e| AuthError::internal(format!("Failed to parse query result: {}", e)))?; + + match records.into_iter().next() { + Some(record) => { + // Check if token is expired + let now = Self::current_timestamp(); + if record.expires_at <= now { + // Token is expired, delete it and return None + let token_id = record.id.strip_prefix("tokens:").unwrap_or(&record.id); + let _ = self.delete_token(token_id).await; + return Ok(None); + } + Ok(Some(Self::record_to_token(record)?)) + } + None => Ok(None), + } + } + + async fn update_token(&self, token: &AuthToken) -> Result<()> { + let record = Self::token_to_record(token); + + self.db + .update(("tokens", &token.token_id)) + .content(record) + .await + .map_err(|e| AuthError::internal(format!("Failed to update token: {}", e)))?; + + Ok(()) + } + + async fn delete_token(&self, token_id: &str) -> Result<()> { + self.db + .delete(("tokens", token_id)) + .await + .map_err(|e| AuthError::internal(format!("Failed to delete token: {}", e)))?; + + Ok(()) + } + + async fn list_user_tokens(&self, user_id: &str) -> Result> { + let mut response = self.db + .query("SELECT * FROM tokens WHERE user_id = $user_id AND expires_at > $now") + .bind(("user_id", user_id)) + .bind(("now", Self::current_timestamp())) + .await + .map_err(|e| AuthError::internal(format!("Failed to list user tokens: {}", e)))?; + + let records: Vec = response + .take(0) + .map_err(|e| AuthError::internal(format!("Failed to parse query result: {}", e)))?; + + records + .into_iter() + .map(Self::record_to_token) + .collect() + } + + async fn store_session(&self, session_id: &str, data: &SessionData) -> Result<()> { + let record = SessionRecord { + id: format!("sessions:{}", session_id), + user_id: data.user_id.clone(), + data: data.data.clone(), + created_at: data.created_at.timestamp(), + last_accessed: data.last_accessed.timestamp(), + expires_at: None, // SurrealDB doesn't have built-in TTL, manage manually + }; + + self.db + .create(("sessions", session_id)) + .content(record) + .await + .map_err(|e| AuthError::internal(format!("Failed to store session: {}", e)))?; + + Ok(()) + } + + async fn get_session(&self, session_id: &str) -> Result> { + let record: Option = self.db + .select(("sessions", session_id)) + .await + .map_err(|e| AuthError::internal(format!("Failed to get session: {}", e)))?; + + match record { + Some(record) => { + use chrono::{DateTime, Utc}; + + Ok(Some(SessionData { + user_id: record.user_id, + data: record.data, + created_at: DateTime::from_timestamp(record.created_at, 0) + .ok_or_else(|| AuthError::internal("Invalid created_at timestamp".to_string()))?, + last_accessed: DateTime::from_timestamp(record.last_accessed, 0) + .ok_or_else(|| AuthError::internal("Invalid last_accessed timestamp".to_string()))?, + })) + } + None => Ok(None), + } + } + + async fn delete_session(&self, session_id: &str) -> Result<()> { + self.db + .delete(("sessions", session_id)) + .await + .map_err(|e| AuthError::internal(format!("Failed to delete session: {}", e)))?; + + Ok(()) + } + + async fn list_user_sessions(&self, user_id: &str) -> Result> { + let mut response = self.db + .query("SELECT * FROM sessions WHERE user_id = $user_id") + .bind(("user_id", user_id)) + .await + .map_err(|e| AuthError::internal(format!("Failed to list user sessions: {}", e)))?; + + let records: Vec = response + .take(0) + .map_err(|e| AuthError::internal(format!("Failed to parse query result: {}", e)))?; + + let mut sessions = Vec::new(); + for record in records { + use chrono::{DateTime, Utc}; + + sessions.push(SessionData { + user_id: record.user_id, + data: record.data, + created_at: DateTime::from_timestamp(record.created_at, 0) + .ok_or_else(|| AuthError::internal("Invalid created_at timestamp".to_string()))?, + last_accessed: DateTime::from_timestamp(record.last_accessed, 0) + .ok_or_else(|| AuthError::internal("Invalid last_accessed timestamp".to_string()))?, + }); + } + + Ok(sessions) + } + + async fn count_active_sessions(&self) -> Result { + let mut response = self.db + .query("SELECT count() FROM sessions WHERE expires_at IS NONE OR expires_at > $now") + .bind(("now", Self::current_timestamp())) + .await + .map_err(|e| AuthError::internal(format!("Failed to count active sessions: {}", e)))?; + + let count: Option = response + .take(0) + .map_err(|e| AuthError::internal(format!("Failed to parse count result: {}", e)))?; + + Ok(count.unwrap_or(0)) + } + + async fn store_kv(&self, key: &str, value: &[u8], ttl: Option) -> Result<()> { + let expires_at = ttl.map(|duration| { + Self::current_timestamp() + duration.as_secs() as i64 + }); + + let record = KvRecord { + id: format!("kv:{}", key), + value: value.to_vec(), + expires_at, + created_at: Self::current_timestamp(), + }; + + self.db + .create(("kv", key)) + .content(record) + .await + .map_err(|e| AuthError::internal(format!("Failed to store key-value: {}", e)))?; + + Ok(()) + } + + async fn get_kv(&self, key: &str) -> Result>> { + let record: Option = self.db + .select(("kv", key)) + .await + .map_err(|e| AuthError::internal(format!("Failed to get key-value: {}", e)))?; + + match record { + Some(record) => { + // Check if expired + if let Some(expires_at) = record.expires_at { + let now = Self::current_timestamp(); + if expires_at <= now { + // Expired, delete and return None + let _ = self.delete_kv(key).await; + return Ok(None); + } + } + Ok(Some(record.value)) + } + None => Ok(None), + } + } + + async fn delete_kv(&self, key: &str) -> Result<()> { + self.db + .delete(("kv", key)) + .await + .map_err(|e| AuthError::internal(format!("Failed to delete key-value: {}", e)))?; + + Ok(()) + } + + async fn cleanup_expired(&self) -> Result<()> { + let now = Self::current_timestamp(); + + // Clean up expired tokens + let _ = self.db + .query("DELETE FROM tokens WHERE expires_at <= $now") + .bind(("now", now)) + .await + .map_err(|e| AuthError::internal(format!("Failed to cleanup expired tokens: {}", e)))?; + + // Clean up expired key-value pairs + let _ = self.db + .query("DELETE FROM kv WHERE expires_at IS NOT NONE AND expires_at <= $now") + .bind(("now", now)) + .await + .map_err(|e| AuthError::internal(format!("Failed to cleanup expired kv: {}", e)))?; + + Ok(()) + } +} +``` + +## Step 3: Add Feature Gating (Recommended) + +Add to your `Cargo.toml`: + +```toml +[features] +default = [] +surrealdb-storage = ["surrealdb", "serde_json"] + +[dependencies] +surrealdb = { version = "1.0", optional = true } +serde_json = { version = "1.0", optional = true } +auth-framework = "0.4.2" +async-trait = "0.1" +serde = { version = "1.0", features = ["derive"] } +chrono = { version = "0.4", features = ["serde"] } +tokio = { version = "1.0", features = ["full"] } +``` + +Gate your implementation: + +```rust +#[cfg(feature = "surrealdb-storage")] +pub mod surrealdb; + +#[cfg(feature = "surrealdb-storage")] +pub use surrealdb::SurrealStorage; +``` + +## Step 4: Error Handling Best Practices + +Implement proper error conversion: + +```rust +impl From for auth_framework::errors::AuthError { + fn from(err: surrealdb::Error) -> Self { + auth_framework::errors::AuthError::internal(format!( + "SurrealDB error: {}", err + )) + } +} +``` + +## Step 5: Testing Your Implementation + +Create comprehensive tests: + +```rust +#[cfg(test)] +mod tests { + use super::*; + use auth_framework::testing::helpers; + use std::sync::Arc; + + async fn setup_test_storage() -> Arc { + let config = SurrealConfig { + url: "memory".to_string(), // Use in-memory for tests + ..Default::default() + }; + Arc::new(SurrealStorage::new(config).await.expect("Failed to create test storage")) + } + + #[tokio::test] + async fn test_token_operations() { + let storage = setup_test_storage().await; + + // Create a test token + let token = helpers::create_test_token("user123", "test-token"); + + // Store token + storage.store_token(&token).await.unwrap(); + + // Retrieve token + let retrieved = storage.get_token(&token.token_id).await.unwrap(); + assert!(retrieved.is_some()); + assert_eq!(retrieved.unwrap().user_id, "user123"); + + // Delete token + storage.delete_token(&token.token_id).await.unwrap(); + + // Verify deletion + let deleted = storage.get_token(&token.token_id).await.unwrap(); + assert!(deleted.is_none()); + } + + #[tokio::test] + async fn test_session_operations() { + let storage = setup_test_storage().await; + + let session_data = helpers::create_test_session_data("user123"); + let session_id = "test-session-id"; + + // Store session + storage.store_session(session_id, &session_data).await.unwrap(); + + // Retrieve session + let retrieved = storage.get_session(session_id).await.unwrap(); + assert!(retrieved.is_some()); + assert_eq!(retrieved.unwrap().user_id, "user123"); + + // Delete session + storage.delete_session(session_id).await.unwrap(); + + // Verify deletion + let deleted = storage.get_session(session_id).await.unwrap(); + assert!(deleted.is_none()); + } + + #[tokio::test] + async fn test_kv_operations() { + let storage = setup_test_storage().await; + + let key = "test-key"; + let value = b"test-value"; + + // Store key-value + storage.store_kv(key, value, None).await.unwrap(); + + // Retrieve value + let retrieved = storage.get_kv(key).await.unwrap(); + assert!(retrieved.is_some()); + assert_eq!(retrieved.unwrap(), value); + + // Delete key + storage.delete_kv(key).await.unwrap(); + + // Verify deletion + let deleted = storage.get_kv(key).await.unwrap(); + assert!(deleted.is_none()); + } + + #[tokio::test] + async fn test_ttl_expiration() { + let storage = setup_test_storage().await; + + let key = "ttl-key"; + let value = b"ttl-value"; + let short_ttl = Duration::from_millis(100); + + // Store with short TTL + storage.store_kv(key, value, Some(short_ttl)).await.unwrap(); + + // Should be available immediately + let retrieved = storage.get_kv(key).await.unwrap(); + assert!(retrieved.is_some()); + + // Wait for expiration + tokio::time::sleep(Duration::from_millis(150)).await; + + // Should be expired now + let expired = storage.get_kv(key).await.unwrap(); + assert!(expired.is_none()); + } +} +``` + +## Step 6: Integration with AuthFramework + +Your storage is now ready to use with AuthFramework: + +```rust +use auth_framework::{AuthFramework, AuthConfig}; +use std::sync::Arc; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Create your custom storage + let storage = Arc::new(SurrealStorage::connect("ws://localhost:8000").await?); + + // Create AuthFramework with your custom storage + let mut config = AuthConfig::default(); + config.security.secret_key = Some("your-jwt-secret-key-32-chars-min".to_string()); + + let auth = AuthFramework::builder() + .customize(|c| { + c.secret = config.security.secret_key; + c + }) + .with_storage() + .custom(storage) + .done() + .build() + .await?; + + // Use the auth framework normally + // All storage operations will use your SurrealDB backend + + Ok(()) +} +``` + +## Best Practices Summary + +1. **Follow SOLID Principles**: Your storage implements the `AuthStorage` interface (DIP) +2. **Proper Error Handling**: Convert database errors to `AuthError` types +3. **Feature Gating**: Make your storage optional via Cargo features +4. **Comprehensive Testing**: Test all storage operations thoroughly +5. **Documentation**: Document configuration options and usage +6. **Security**: Handle sensitive data appropriately (tokens, sessions) +7. **Performance**: Use appropriate indexes and queries +8. **Connection Management**: Handle connection pooling and retries + +## Next Steps + +- Add connection pooling for better performance +- Implement proper logging and metrics +- Add backup and recovery procedures +- Consider implementing read replicas for scaling +- Add migration scripts for schema changes + +This implementation provides a solid foundation for integrating any database with AuthFramework while maintaining the framework's security and performance standards. diff --git a/docs/guides/third-party-storage-usage.md b/docs/guides/third-party-storage-usage.md new file mode 100644 index 0000000..1055cb3 --- /dev/null +++ b/docs/guides/third-party-storage-usage.md @@ -0,0 +1,733 @@ +# Third-Party Storage Backend Usage Guide + +This guide shows you how to use third-party storage backends with AuthFramework, including the integration patterns and best practices. + +## Overview + +AuthFramework's builder pattern makes it easy to integrate any storage backend that implements the `AuthStorage` trait. This guide covers the two primary integration methods: the builder API and convenience constructors. + +## Integration Methods + +### Method 1: Builder Pattern with Custom Storage (Recommended) + +The builder pattern provides the most flexibility and follows AuthFramework's fluent API design: + +```rust +use auth_framework::{AuthFramework, AuthConfig}; +use std::sync::Arc; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Step 1: Create your storage backend + let storage = Arc::new(YourCustomStorage::connect("connection-string").await?); + + // Step 2: Configure AuthFramework + let mut config = AuthConfig::default(); + config.security.secret_key = Some("your-jwt-secret-32-chars-or-more".to_string()); + + // Step 3: Build with custom storage + let auth = AuthFramework::builder() + .customize(|c| { + c.secret = config.security.secret_key; + c + }) + .with_storage() + .custom(storage) // Pass your storage here + .done() + .build() + .await?; + + // Step 4: Use normally - all operations will use your storage + println!("AuthFramework initialized with custom storage!"); + + Ok(()) +} +``` + +### Method 2: Convenience Constructor + +For simpler use cases, use the convenience constructor: + +```rust +use auth_framework::{AuthFramework, AuthConfig}; +use std::sync::Arc; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let storage = Arc::new(YourCustomStorage::connect("connection-string").await?); + let mut config = AuthConfig::default(); + config.security.secret_key = Some("your-jwt-secret-32-chars-or-more".to_string()); + + // This returns an initialized AuthFramework instance + let auth = AuthFramework::new_initialized_with_storage(config, storage).await?; + + println!("AuthFramework ready to use!"); + + Ok(()) +} +``` + +## Real-World Examples + +### Example 1: SurrealDB Integration + +```rust +use auth_framework::{AuthFramework, AuthConfig, errors::Result as AuthResult}; +use std::sync::Arc; +use std::time::Duration; + +// Assuming you have a SurrealDB storage implementation +use your_surreal_crate::SurrealStorage; + +#[derive(Clone)] +pub struct AuthService { + auth: Arc, +} + +impl AuthService { + pub async fn new() -> AuthResult { + // Configure SurrealDB connection + let storage_config = your_surreal_crate::SurrealConfig { + url: std::env::var("SURREAL_URL") + .unwrap_or_else(|_| "ws://localhost:8000".to_string()), + namespace: "production".to_string(), + database: "authframework".to_string(), + username: std::env::var("SURREAL_USER").ok(), + password: std::env::var("SURREAL_PASS").ok(), + }; + + // Create storage backend + let storage = Arc::new( + SurrealStorage::new(storage_config) + .await + .map_err(|e| auth_framework::errors::AuthError::config( + format!("Failed to initialize SurrealDB: {}", e) + ))? + ); + + // Configure authentication + let mut config = AuthConfig::default(); + config.security.secret_key = Some(std::env::var("JWT_SECRET").map_err(|_| { + auth_framework::errors::AuthError::config( + "JWT_SECRET environment variable is required" + ) + })?); + + // Build the authentication framework + let auth = Arc::new( + AuthFramework::builder() + .customize(|c| { + c.secret = config.security.secret_key.clone(); + c + }) + .with_storage() + .custom(storage) + .done() + .build() + .await? + ); + + Ok(Self { auth }) + } + + pub async fn authenticate_user( + &self, + email: &str, + password: &str, + ) -> AuthResult { + // Use the auth framework normally + let credential = auth_framework::authentication::credentials::Credential::Password { + username: email.to_string(), + password: password.to_string(), + }; + + // This will use your SurrealDB backend for all storage operations + match self.auth.authenticate("password", credential).await? { + auth_framework::authentication::AuthResult::Success(token) => { + Ok(token.access_token) + } + auth_framework::authentication::AuthResult::Failed(reason) => { + Err(auth_framework::errors::AuthError::authentication_failed(reason)) + } + _ => Err(auth_framework::errors::AuthError::authentication_failed( + "Authentication method not configured".to_string() + )) + } + } +} +``` + +### Example 2: Web Application Integration + +```rust +use auth_framework::{AuthFramework, AuthConfig}; +use axum::{ + extract::State, + http::StatusCode, + response::Json, + routing::{get, post}, + Router, +}; +use std::sync::Arc; +use std::time::Duration; + +// Your custom storage backend +use your_storage_crate::CustomStorage; + +#[derive(Clone)] +struct AppState { + auth: Arc, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Initialize custom storage with connection pooling + let storage = Arc::new( + CustomStorage::builder() + .connection_string(&std::env::var("DATABASE_URL")?) + .pool_size(20) + .timeout(Duration::from_secs(30)) + .enable_ssl(true) + .build() + .await? + ); + + // Configure AuthFramework + let mut config = AuthConfig::default(); + config.security.secret_key = Some(std::env::var("JWT_SECRET")?); + + // Build with advanced configuration + let auth = Arc::new( + AuthFramework::builder() + .customize(|c| { + c.secret = config.security.secret_key.clone(); + c + }) + .with_storage() + .custom(storage) + .done() + .build() + .await? + ); + + let state = AppState { auth }; + + // Create Axum router + let app = Router::new() + .route("/login", post(login_handler)) + .route("/profile", get(profile_handler)) + .with_state(state); + + // Start server + println!("Server starting on http://localhost:3000"); + let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?; + axum::serve(listener, app).await?; + + Ok(()) +} + +async fn login_handler( + State(state): State, + Json(payload): Json, +) -> Result, StatusCode> { + let credential = auth_framework::authentication::credentials::Credential::Password { + username: payload.email, + password: payload.password, + }; + + match state.auth.authenticate("password", credential).await { + Ok(auth_framework::authentication::AuthResult::Success(token)) => { + Ok(Json(LoginResponse { + access_token: token.access_token, + refresh_token: token.refresh_token, + expires_in: 3600, + })) + } + Ok(_) => Err(StatusCode::UNAUTHORIZED), + Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR), + } +} + +async fn profile_handler( + State(state): State, + // Add auth middleware extraction here +) -> Result, StatusCode> { + // Profile handler implementation + Ok(Json(UserProfile { + id: "user123".to_string(), + email: "user@example.com".to_string(), + })) +} + +#[derive(serde::Deserialize)] +struct LoginRequest { + email: String, + password: String, +} + +#[derive(serde::Serialize)] +struct LoginResponse { + access_token: String, + refresh_token: Option, + expires_in: u64, +} + +#[derive(serde::Serialize)] +struct UserProfile { + id: String, + email: String, +} +``` + +### Example 3: Microservice Architecture + +```rust +use auth_framework::{AuthFramework, AuthConfig}; +use std::sync::Arc; +use tonic::{transport::Server, Request, Response, Status}; + +// Your distributed storage backend +use your_distributed_storage::DistributedStorage; + +pub struct AuthService { + auth: Arc, +} + +impl AuthService { + pub async fn new() -> Result> { + // Create distributed storage with service discovery + let storage_config = your_distributed_storage::Config { + consul_url: std::env::var("CONSUL_URL")?, + service_name: "auth-storage".to_string(), + datacenter: "dc1".to_string(), + replication_factor: 3, + }; + + let storage = Arc::new( + DistributedStorage::with_service_discovery(storage_config).await? + ); + + // Configure for microservice use + let mut config = AuthConfig::default(); + config.security.secret_key = Some(std::env::var("JWT_SECRET")?); + + let auth = Arc::new( + AuthFramework::builder() + .customize(|c| { + c.secret = config.security.secret_key.clone(); + c + }) + .with_storage() + .custom(storage) + .done() + .build() + .await? + ); + + Ok(Self { auth }) + } + + pub async fn validate_service_token(&self, token: &str) -> Result { + match self.auth.validate_token(token).await { + Ok(true) => Ok(true), + Ok(false) => Ok(false), + Err(e) => { + tracing::error!("Token validation error: {}", e); + Err(Status::internal("Validation failed")) + } + } + } +} +``` + +## Configuration Best Practices + +### Environment-Based Configuration + +```rust +use auth_framework::AuthConfig; +use std::env; +use std::time::Duration; + +pub fn create_auth_config() -> Result> { + let environment = env::var("ENVIRONMENT").unwrap_or_else(|_| "development".to_string()); + + let mut base_config = AuthConfig::default(); + base_config.security.secret_key = Some(env::var("JWT_SECRET")?); + + let config = match environment.as_str() { + "production" => { + // Production configuration + base_config + } + "staging" => { + // Staging configuration + base_config + } + _ => { + // Development defaults + base_config + } + }; + + Ok(config) +} +``` + +### Storage-Specific Configuration + +```rust +use your_storage_crate::{StorageConfig, ConnectionPool}; + +pub async fn create_storage_backend() -> Result, Box> { + let storage_type = std::env::var("STORAGE_TYPE").unwrap_or_else(|_| "memory".to_string()); + + match storage_type.as_str() { + "postgresql" => { + let config = StorageConfig::postgresql() + .connection_string(&std::env::var("DATABASE_URL")?) + .pool_config(ConnectionPool::new() + .max_connections(50) + .min_connections(5) + .connection_timeout(Duration::from_secs(30)) + ) + .enable_ssl(true) + .ssl_ca_cert_path(&std::env::var("SSL_CA_CERT")?); + + Ok(Arc::new(YourPostgresStorage::new(config).await?)) + } + "redis" => { + let config = StorageConfig::redis() + .cluster_urls(vec![ + std::env::var("REDIS_URL_1")?, + std::env::var("REDIS_URL_2")?, + std::env::var("REDIS_URL_3")?, + ]) + .enable_cluster_mode(true) + .connection_pool_size(20); + + Ok(Arc::new(YourRedisStorage::new(config).await?)) + } + "surrealdb" => { + let config = StorageConfig::surrealdb() + .url(&std::env::var("SURREAL_URL")?) + .namespace(&std::env::var("SURREAL_NAMESPACE")?) + .database(&std::env::var("SURREAL_DATABASE")?) + .credentials( + &std::env::var("SURREAL_USER")?, + &std::env::var("SURREAL_PASS")? + ); + + Ok(Arc::new(YourSurrealStorage::new(config).await?)) + } + _ => { + // Fallback to memory storage for development + Ok(Arc::new(auth_framework::storage::MemoryStorage::new())) + } + } +} +``` + +## Error Handling Patterns + +### Robust Error Handling + +```rust +use auth_framework::errors::{AuthError, Result as AuthResult}; + +pub async fn initialize_auth_service() -> AuthResult> { + let storage = create_custom_storage().await.map_err(|e| { + AuthError::config_with_help( + format!("Failed to initialize storage: {}", e), + "Check your storage configuration and connection parameters", + Some("Ensure your database is running and accessible".to_string()) + ) + })?; + + let config = create_auth_config().map_err(|e| { + AuthError::config_with_help( + format!("Invalid configuration: {}", e), + "Check all required environment variables are set", + Some("Run 'env | grep JWT_SECRET' to verify JWT secret is set".to_string()) + ) + })?; + + AuthFramework::builder() + .customize(|c| { + c.secret = config.security.secret_key.clone(); + c + }) + .with_storage() + .custom(storage) + .done() + .build() + .await + .map(Arc::new) +} + +async fn create_custom_storage() -> Result, Box> { + // Your storage creation logic with proper error handling + Ok(Arc::new(YourStorage::new().await?)) +} +``` + +### Graceful Degradation + +```rust +use auth_framework::storage::MemoryStorage; + +pub async fn create_resilient_storage() -> Arc { + // Try primary storage first + if let Ok(storage) = YourPrimaryStorage::connect(&primary_url).await { + tracing::info!("Connected to primary storage"); + return Arc::new(storage); + } + + // Fall back to secondary storage + if let Ok(storage) = YourSecondaryStorage::connect(&secondary_url).await { + tracing::warn!("Primary storage unavailable, using secondary"); + return Arc::new(storage); + } + + // Last resort: memory storage with warning + tracing::error!("All persistent storage backends unavailable, using memory storage"); + Arc::new(MemoryStorage::new()) +} +``` + +## Testing Your Integration + +### Integration Tests + +```rust +#[cfg(test)] +mod integration_tests { + use super::*; + use auth_framework::testing::helpers; + + async fn setup_test_auth() -> AuthFramework { + let storage = Arc::new(YourStorage::new_for_testing().await.unwrap()); + let mut config = AuthConfig::default(); + config.security.secret_key = Some("test-secret-32-characters-long!".to_string()); + + AuthFramework::builder() + .customize(|c| { + c.secret = config.security.secret_key.clone(); + c + }) + .with_storage() + .custom(storage) + .done() + .build() + .await + .unwrap() + } + + #[tokio::test] + async fn test_full_auth_flow() { + let auth = setup_test_auth().await; + + // Register authentication method + let jwt_method = auth_framework::methods::JwtMethod::new() + .secret_key("test-secret-32-characters-long!"); + + auth.register_method("jwt", + auth_framework::methods::AuthMethodEnum::Jwt(jwt_method) + ); + + // Test token creation and validation + let token = auth.create_auth_token( + "test-user", + vec!["read".to_string(), "write".to_string()], + "jwt", + None + ).await.unwrap(); + + assert!(!token.access_token.is_empty()); + + // Test token validation + let is_valid = auth.validate_token(&token.access_token).await.unwrap(); + assert!(is_valid); + } + + #[tokio::test] + async fn test_storage_persistence() { + let auth = setup_test_auth().await; + + // Create and store a token + let token = helpers::create_test_token("user123", "test-token-id"); + auth.storage.store_token(&token).await.unwrap(); + + // Verify it can be retrieved + let retrieved = auth.storage.get_token(&token.token_id).await.unwrap(); + assert!(retrieved.is_some()); + assert_eq!(retrieved.unwrap().user_id, "user123"); + } +} +``` + +## Production Deployment + +### Docker Configuration + +```dockerfile +FROM rust:1.70 as builder + +WORKDIR /app +COPY . . +RUN cargo build --release --features "your-storage-backend" + +FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /app/target/release/your-app /usr/local/bin/your-app +COPY --from=builder /app/config /config + +EXPOSE 8080 +CMD ["your-app"] +``` + +### Kubernetes Deployment + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: auth-service +spec: + replicas: 3 + selector: + matchLabels: + app: auth-service + template: + metadata: + labels: + app: auth-service + spec: + containers: + - name: auth-service + image: your-registry/auth-service:latest + ports: + - containerPort: 8080 + env: + - name: JWT_SECRET + valueFrom: + secretKeyRef: + name: auth-secrets + key: jwt-secret + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: auth-secrets + key: database-url + - name: STORAGE_TYPE + value: "postgresql" + resources: + requests: + memory: "256Mi" + cpu: "250m" + limits: + memory: "512Mi" + cpu: "500m" + livenessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /ready + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 5 +``` + +## Migration and Maintenance + +### Storage Migration + +```rust +use auth_framework::storage::StorageMigration; + +pub async fn migrate_storage() -> Result<(), Box> { + let old_storage = Arc::new(OldStorage::connect(&old_config).await?); + let new_storage = Arc::new(NewStorage::connect(&new_config).await?); + + let migration = StorageMigration::new(old_storage, new_storage) + .with_batch_size(1000) + .with_verify_data(true) + .with_preserve_ttl(true); + + println!("Starting storage migration..."); + + let result = migration.migrate_all().await?; + + println!("Migration completed: {} tokens, {} sessions migrated", + result.tokens_migrated, result.sessions_migrated); + + Ok(()) +} +``` + +## Troubleshooting + +### Common Issues and Solutions + +1. **Connection Failures** + + ```rust + // Implement connection retry logic + async fn connect_with_retry(connect_fn: impl Fn() -> Future>) -> Result { + let mut attempts = 0; + loop { + match connect_fn().await { + Ok(connection) => return Ok(connection), + Err(e) if attempts < 3 => { + attempts += 1; + tokio::time::sleep(Duration::from_secs(2_u64.pow(attempts))).await; + } + Err(e) => return Err(e), + } + } + } + ``` + +2. **Performance Issues** + + ```rust + // Enable connection pooling and optimize queries + let storage_config = YourStorageConfig::new() + .pool_size(50) + .enable_connection_pooling(true) + .query_timeout(Duration::from_secs(30)) + .enable_prepared_statements(true); + ``` + +3. **Memory Usage** + + ```rust + // Implement proper cleanup and monitoring + let cleanup_interval = Duration::from_secs(3600); + tokio::spawn(async move { + let mut interval = tokio::time::interval(cleanup_interval); + loop { + interval.tick().await; + if let Err(e) = storage.cleanup_expired().await { + tracing::error!("Cleanup failed: {}", e); + } + } + }); + ``` + +## Summary + +This guide covers the complete integration of third-party storage backends with AuthFramework. The builder pattern provides flexibility while maintaining type safety and following Rust best practices. The key points to remember: + +- Use `Arc` for your storage instances +- Prefer the builder pattern for complex configurations +- Implement proper error handling and fallback strategies +- Test thoroughly in both unit and integration scenarios +- Plan for production deployment and monitoring + +With these patterns, you can integrate any storage backend while maintaining AuthFramework's security, performance, and reliability standards. diff --git a/docs/storage-backends.md b/docs/storage-backends.md index 6ddb407..50a064a 100644 --- a/docs/storage-backends.md +++ b/docs/storage-backends.md @@ -77,14 +77,14 @@ async fn main() -> Result<(), Box> { let storage = InMemoryStorage::new(); let config = AuthConfig::default(); let auth = AuthFramework::new(storage, config).await?; - + // Register and authenticate auth.register_user("user123", "password").await?; let token = auth.authenticate("user123", "password").await?; - + // Token is stored in memory and will be automatically cleaned up println!("Token: {}", token.access_token); - + Ok(()) } ``` @@ -150,7 +150,7 @@ use auth_framework::storage::RedisConfig; let config = RedisConfig::new() .with_cluster_urls(vec![ "redis://node1:6379", - "redis://node2:6379", + "redis://node2:6379", "redis://node3:6379", ]) .with_cluster_mode(true) @@ -215,10 +215,10 @@ async fn main() -> Result<(), Box> { let storage = RedisStorage::new("redis://localhost:6379").await?; let config = AuthConfig::default(); let auth = Arc::new(AuthFramework::new(storage, config).await?); - + // Simulate concurrent operations let mut handles = vec![]; - + for i in 0..100 { let auth_clone = auth.clone(); let handle = tokio::spawn(async move { @@ -229,12 +229,12 @@ async fn main() -> Result<(), Box> { }); handles.push(handle); } - + // Wait for all operations to complete for handle in handles { handle.await?; } - + Ok(()) } ``` @@ -414,7 +414,7 @@ println!("Database size: {} MB", stats.database_size_mb); PostgreSQL storage provides robust performance for production applications: - **Token verification**: ~10,000 ops/sec -- **Storage operations**: ~5,000 ops/sec +- **Storage operations**: ~5,000 ops/sec - **Query latency**: 1-10ms typical - **Concurrent connections**: 100+ supported @@ -479,29 +479,29 @@ migration.migrate_kv_data().await?; #[cfg(test)] mod tests { use super::*; - + async fn test_with_storage(storage: S) { let config = AuthConfig::default(); let auth = AuthFramework::new(storage, config).await.unwrap(); - + // Test operations auth.register_user("test", "password").await.unwrap(); let token = auth.authenticate("test", "password").await.unwrap(); assert!(!token.access_token.is_empty()); } - + #[tokio::test] async fn test_in_memory_storage() { let storage = InMemoryStorage::new(); test_with_storage(storage).await; } - + #[tokio::test] async fn test_redis_storage() { let storage = RedisStorage::new("redis://localhost:6379").await.unwrap(); test_with_storage(storage).await; } - + #[tokio::test] async fn test_postgres_storage() { let storage = PostgresStorage::new("postgresql://localhost/test_db").await.unwrap(); diff --git a/docs/web-frameworks.md b/docs/web-frameworks.md index 31576b7..315998f 100644 --- a/docs/web-frameworks.md +++ b/docs/web-frameworks.md @@ -31,16 +31,16 @@ use auth_framework::{ async fn main() -> std::io::Result<()> { // Initialize storage let storage = InMemoryStorage::new(); - + // Create auth configuration let config = AuthConfig::builder() .jwt_secret("your-secret-key-here".to_string()) .token_expiry(chrono::Duration::hours(24)) .build(); - + // Create auth framework instance let auth = AuthFramework::new(storage, config).await.unwrap(); - + HttpServer::new(move || { App::new() .app_data(web::Data::new(auth.clone())) @@ -223,23 +223,23 @@ async fn main() { let storage = InMemoryStorage::new(); let config = AuthConfig::default(); let auth = AuthFramework::new(storage, config).await.unwrap(); - + // Create auth filter let auth_filter = with_auth(auth.clone()); - + // Public routes let login = warp::path("login") .and(warp::post()) .and(warp::body::json()) .and(warp::any().map(move || auth.clone())) .and_then(login_handler); - + let register = warp::path("register") .and(warp::post()) .and(warp::body::json()) .and(warp::any().map(move || auth.clone())) .and_then(register_handler); - + // Protected routes let profile = warp::path("profile") .and(warp::get()) @@ -250,7 +250,7 @@ async fn main() { "permissions": user.permissions, })) }); - + let admin = warp::path("admin") .and(warp::get()) .and(auth_filter.clone()) @@ -258,7 +258,7 @@ async fn main() { .map(|user: AuthenticatedUser| { format!("Welcome, admin {}!", user.user_id) }); - + let moderator = warp::path("moderator") .and(warp::get()) .and(auth_filter) @@ -266,7 +266,7 @@ async fn main() { .map(|user: AuthenticatedUser| { "Moderator panel" }); - + let routes = login .or(register) .or(profile) @@ -274,7 +274,7 @@ async fn main() { .or(moderator) .with(warp::cors().allow_any_origin()) .recover(handle_rejection); - + warp::serve(routes) .run(([127, 0, 0, 1], 3030)) .await; @@ -382,7 +382,7 @@ async fn rocket() -> _ { let storage = InMemoryStorage::new(); let config = AuthConfig::default(); let auth = AuthFramework::new(storage, config).await.unwrap(); - + rocket::build() .manage(auth) .attach(AuthFairing::default()) @@ -489,12 +489,12 @@ impl RateLimiter { fn is_allowed(&self, client_ip: &str) -> bool { let mut requests = self.requests.lock().unwrap(); let now = Instant::now(); - + let client_requests = requests.entry(client_ip.to_string()).or_insert_with(Vec::new); - + // Remove old requests client_requests.retain(|&time| now.duration_since(time) < self.window); - + if client_requests.len() < self.max_requests { client_requests.push(now); true @@ -521,10 +521,10 @@ async fn create_session_handler( created_at: chrono::Utc::now(), last_accessed: chrono::Utc::now(), }; - + let session_id = uuid::Uuid::new_v4().to_string(); auth.storage.store_session(&session_id, &session_data).await.unwrap(); - + Ok(HttpResponse::Ok().json(serde_json::json!({"session_id": session_id}))) } ``` @@ -550,7 +550,7 @@ async fn custom_protected_handler( ) -> Result { let token = extract_token_from_request(&req)?; let claims: CustomClaims = auth.verify_custom_token(&token).await?; - + if claims.custom_field == "special_value" { Ok(HttpResponse::Ok().json("Special access granted")) } else { @@ -566,29 +566,29 @@ async fn custom_protected_handler( mod tests { use super::*; use actix_web::{test, App}; - + #[actix_web::test] async fn test_protected_route() { let storage = InMemoryStorage::new(); let config = AuthConfig::default(); let auth = AuthFramework::new(storage, config).await.unwrap(); - + // Create test user auth.register_user("testuser", "password").await.unwrap(); let token = auth.authenticate("testuser", "password").await.unwrap(); - + let app = test::init_service( App::new() .app_data(web::Data::new(auth)) .wrap(AuthMiddleware::new()) .route("/protected", web::get().to(get_profile)) ).await; - + let req = test::TestRequest::get() .uri("/protected") .insert_header(("Authorization", format!("Bearer {}", token.access_token))) .to_request(); - + let resp = test::call_service(&app, req).await; assert!(resp.status().is_success()); } diff --git a/examples/debug_server.rs b/examples/debug_server.rs new file mode 100644 index 0000000..34d0fc9 --- /dev/null +++ b/examples/debug_server.rs @@ -0,0 +1,59 @@ +//! Debug REST API Server +//! Simple test to identify startup issues + +use auth_framework::{ + AuthFramework, + api::{ApiServer, server::ApiServerConfig}, + config::AuthConfig, + storage::memory::InMemoryStorage, +}; +use std::sync::Arc; + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("🔍 Starting server debug test..."); + + // Initialize tracing + tracing_subscriber::fmt::init(); + + println!("📦 Creating storage..."); + let _storage = Arc::new(InMemoryStorage::new()); + + println!("⚙️ Creating auth config..."); + let auth_config = AuthConfig::new() + .secret("your-super-secret-jwt-key-change-this-in-production".to_string()) + .token_lifetime(chrono::Duration::hours(1).to_std().unwrap()) + .refresh_token_lifetime(chrono::Duration::days(7).to_std().unwrap()); + + println!("🔐 Creating AuthFramework..."); + let auth_framework = Arc::new(AuthFramework::new(auth_config)); + + println!("🌐 Creating API config..."); + let api_config = ApiServerConfig { + host: "127.0.0.1".to_string(), + port: 8088, + enable_cors: true, + max_body_size: 1024 * 1024, + enable_tracing: true, + }; + + println!("🚀 Creating API server..."); + let api_server = ApiServer::with_config(auth_framework, api_config); + + println!("🔧 Building router..."); + match api_server.build_router().await { + Ok(_router) => { + println!("✅ Router built successfully!"); + } + Err(e) => { + println!("❌ Router build failed: {}", e); + println!("Error details: {:?}", e); + return Err(e.into()); + } + } + + println!("🎯 Starting server (this should not return immediately)..."); + api_server.start().await?; + println!("⚠️ Server method returned (this should not happen)"); + Ok(()) +} diff --git a/lcov.info b/lcov.info index 83d838c..eb1187e 100644 --- a/lcov.info +++ b/lcov.info @@ -65159,4 +65159,4 @@ BRF:0 BRH:0 LF:345 LH:315 -end_of_record \ No newline at end of file +end_of_record diff --git a/migration/test_plan_status.json b/migration/test_plan_status.json index ea544d2..fb2c8be 100644 --- a/migration/test_plan_status.json +++ b/migration/test_plan_status.json @@ -1,8 +1,8 @@ { "plan_id": "test_plan", "status": "Completed", - "started_at": "2025-09-27T23:32:36.836821600Z", - "completed_at": "2025-09-27T23:32:36.944700300Z", + "started_at": "2025-09-30T01:55:09.637937200Z", + "completed_at": "2025-09-30T01:55:09.745427800Z", "phases_completed": [ "test_phase" ], diff --git a/public.pem b/public.pem index a1dabaf..aabc1ef 100644 --- a/public.pem +++ b/public.pem @@ -9,4 +9,4 @@ EFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyzABCDEFGH IJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL MNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOP QRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyzQIDAQAB ------END PUBLIC KEY----- \ No newline at end of file +-----END PUBLIC KEY----- diff --git a/rustc-ice-2025-09-27T12_38_23-11644.txt b/rustc-ice-2025-09-27T12_38_23-11644.txt index c169562..f593d0e 100644 --- a/rustc-ice-2025-09-27T12_38_23-11644.txt +++ b/rustc-ice-2025-09-27T12_38_23-11644.txt @@ -1008,7 +1008,7 @@ delayed bug: `Res::Err` but no error emitted 42: BaseThreadInitThunk 43: RtlUserThreadStart -delayed bug: +delayed bug: 0: std::backtrace_rs::backtrace::win64::trace at /rustc/a454fccb02df9d361f1201b747c01257f58a8b37/library\std\src\..\..\backtrace\src\backtrace\win64.rs:85 1: std::backtrace_rs::backtrace::trace_unsynchronized @@ -1108,7 +1108,7 @@ delayed bug: `Res::Err` but no error emitted 39: BaseThreadInitThunk 40: RtlUserThreadStart -delayed bug: +delayed bug: 0: std::backtrace_rs::backtrace::win64::trace at /rustc/a454fccb02df9d361f1201b747c01257f58a8b37/library\std\src\..\..\backtrace\src\backtrace\win64.rs:85 1: std::backtrace_rs::backtrace::trace_unsynchronized @@ -1266,7 +1266,7 @@ delayed bug: `Res::Err` but no error emitted 40: BaseThreadInitThunk 41: RtlUserThreadStart -delayed bug: +delayed bug: 0: std::backtrace_rs::backtrace::win64::trace at /rustc/a454fccb02df9d361f1201b747c01257f58a8b37/library\std\src\..\..\backtrace\src\backtrace\win64.rs:85 1: std::backtrace_rs::backtrace::trace_unsynchronized @@ -1364,7 +1364,7 @@ delayed bug: `Res::Err` but no error emitted 39: BaseThreadInitThunk 40: RtlUserThreadStart -delayed bug: +delayed bug: 0: std::backtrace_rs::backtrace::win64::trace at /rustc/a454fccb02df9d361f1201b747c01257f58a8b37/library\std\src\..\..\backtrace\src\backtrace\win64.rs:85 1: std::backtrace_rs::backtrace::trace_unsynchronized @@ -1463,7 +1463,7 @@ delayed bug: `Res::Err` but no error emitted 41: BaseThreadInitThunk 42: RtlUserThreadStart -delayed bug: +delayed bug: 0: std::backtrace_rs::backtrace::win64::trace at /rustc/a454fccb02df9d361f1201b747c01257f58a8b37/library\std\src\..\..\backtrace\src\backtrace\win64.rs:85 1: std::backtrace_rs::backtrace::trace_unsynchronized @@ -1563,7 +1563,7 @@ delayed bug: `Res::Err` but no error emitted 40: BaseThreadInitThunk 41: RtlUserThreadStart -delayed bug: +delayed bug: 0: std::backtrace_rs::backtrace::win64::trace at /rustc/a454fccb02df9d361f1201b747c01257f58a8b37/library\std\src\..\..\backtrace\src\backtrace\win64.rs:85 1: std::backtrace_rs::backtrace::trace_unsynchronized @@ -1723,7 +1723,7 @@ delayed bug: `Res::Err` but no error emitted 41: BaseThreadInitThunk 42: RtlUserThreadStart -delayed bug: +delayed bug: 0: std::backtrace_rs::backtrace::win64::trace at /rustc/a454fccb02df9d361f1201b747c01257f58a8b37/library\std\src\..\..\backtrace\src\backtrace\win64.rs:85 1: std::backtrace_rs::backtrace::trace_unsynchronized @@ -2419,7 +2419,7 @@ delayed bug: `Res::Err` but no error emitted 41: BaseThreadInitThunk 42: RtlUserThreadStart -delayed bug: +delayed bug: 0: std::backtrace_rs::backtrace::win64::trace at /rustc/a454fccb02df9d361f1201b747c01257f58a8b37/library\std\src\..\..\backtrace\src\backtrace\win64.rs:85 1: std::backtrace_rs::backtrace::trace_unsynchronized @@ -2863,7 +2863,7 @@ delayed bug: `Res::Err` but no error emitted 41: BaseThreadInitThunk 42: RtlUserThreadStart -delayed bug: +delayed bug: 0: std::backtrace_rs::backtrace::win64::trace at /rustc/a454fccb02df9d361f1201b747c01257f58a8b37/library\std\src\..\..\backtrace\src\backtrace\win64.rs:85 1: std::backtrace_rs::backtrace::trace_unsynchronized @@ -3761,7 +3761,7 @@ delayed bug: `Res::Err` but no error emitted 41: BaseThreadInitThunk 42: RtlUserThreadStart -delayed bug: +delayed bug: 0: std::backtrace_rs::backtrace::win64::trace at /rustc/a454fccb02df9d361f1201b747c01257f58a8b37/library\std\src\..\..\backtrace\src\backtrace\win64.rs:85 1: std::backtrace_rs::backtrace::trace_unsynchronized @@ -4512,7 +4512,7 @@ delayed bug: `Res::Err` but no error emitted 41: BaseThreadInitThunk 42: RtlUserThreadStart -delayed bug: +delayed bug: 0: std::backtrace_rs::backtrace::win64::trace at /rustc/a454fccb02df9d361f1201b747c01257f58a8b37/library\std\src\..\..\backtrace\src\backtrace\win64.rs:85 1: std::backtrace_rs::backtrace::trace_unsynchronized @@ -5011,7 +5011,7 @@ delayed bug: `Res::Err` but no error emitted 41: BaseThreadInitThunk 42: RtlUserThreadStart -delayed bug: +delayed bug: 0: std::backtrace_rs::backtrace::win64::trace at /rustc/a454fccb02df9d361f1201b747c01257f58a8b37/library\std\src\..\..\backtrace\src\backtrace\win64.rs:85 1: std::backtrace_rs::backtrace::trace_unsynchronized diff --git a/rustc-ice-2025-09-27T16_52_46-34596.txt b/rustc-ice-2025-09-27T16_52_46-34596.txt index 1bf85d9..d05ef96 100644 --- a/rustc-ice-2025-09-27T16_52_46-34596.txt +++ b/rustc-ice-2025-09-27T16_52_46-34596.txt @@ -18,7 +18,7 @@ delayed bug: path with `Res::Err` but no error emitted disabled backtrace delayed bug: `Res::Err` but no error emitted disabled backtrace -delayed bug: +delayed bug: disabled backtrace delayed bug: TyKind::Error constructed but no error reported disabled backtrace @@ -26,7 +26,7 @@ delayed bug: TyKind::Error constructed but no error reported disabled backtrace delayed bug: `Res::Err` but no error emitted disabled backtrace -delayed bug: +delayed bug: disabled backtrace delayed bug: TyKind::Error constructed but no error reported disabled backtrace @@ -34,7 +34,7 @@ delayed bug: TyKind::Error constructed but no error reported disabled backtrace delayed bug: `Res::Err` but no error emitted disabled backtrace -delayed bug: +delayed bug: disabled backtrace delayed bug: TyKind::Error constructed but no error reported disabled backtrace @@ -42,7 +42,7 @@ delayed bug: TyKind::Error constructed but no error reported disabled backtrace delayed bug: `Res::Err` but no error emitted disabled backtrace -delayed bug: +delayed bug: disabled backtrace delayed bug: TyKind::Error constructed but no error reported disabled backtrace @@ -50,7 +50,7 @@ delayed bug: TyKind::Error constructed but no error reported disabled backtrace delayed bug: `Res::Err` but no error emitted disabled backtrace -delayed bug: +delayed bug: disabled backtrace delayed bug: TyKind::Error constructed but no error reported disabled backtrace @@ -85,4 +85,4 @@ disabled backtrace rustc version: 1.92.0-nightly (a454fccb0 2025-09-15) -platform: x86_64-pc-windows-msvc \ No newline at end of file +platform: x86_64-pc-windows-msvc diff --git a/rustc-ice-2025-09-27T19_51_55-2432.txt b/rustc-ice-2025-09-27T19_51_55-2432.txt index f9d92a8..6bee5d5 100644 --- a/rustc-ice-2025-09-27T19_51_55-2432.txt +++ b/rustc-ice-2025-09-27T19_51_55-2432.txt @@ -4,15 +4,15 @@ delayed bug: no resolution for an import disabled backtrace delayed bug: `Res::Err` but no error emitted disabled backtrace -delayed bug: +delayed bug: disabled backtrace delayed bug: `Res::Err` but no error emitted disabled backtrace -delayed bug: +delayed bug: disabled backtrace delayed bug: `Res::Err` but no error emitted disabled backtrace -delayed bug: +delayed bug: disabled backtrace delayed bug: no type-dependent def for method call disabled backtrace @@ -21,4 +21,4 @@ disabled backtrace rustc version: 1.92.0-nightly (a454fccb0 2025-09-15) -platform: x86_64-pc-windows-msvc \ No newline at end of file +platform: x86_64-pc-windows-msvc diff --git a/sdks/javascript/README.md b/sdks/javascript/README.md deleted file mode 100644 index 0a49bb1..0000000 --- a/sdks/javascript/README.md +++ /dev/null @@ -1,467 +0,0 @@ -# AuthFramework JavaScript/TypeScript SDK - -Official JavaScript/TypeScript client library for the AuthFramework REST API. - -## Installation - -```bash -npm install @authframework/js-sdk -# or -yarn add @authframework/js-sdk -# or -pnpm add @authframework/js-sdk -``` - -## Quick Start - -### Basic Usage - -```typescript -import { AuthFrameworkClient } from '@authframework/js-sdk'; - -// Initialize the client -const client = new AuthFrameworkClient({ - baseUrl: 'http://localhost:8080', - timeout: 30000, // 30 seconds (optional) - retries: 3, // Retry failed requests (optional) -}); - -// Login -try { - const loginResponse = await client.auth.login({ - username: 'user@example.com', - password: 'password123' - }); - - console.log('Login successful:', loginResponse.user); - // Access token is automatically set for subsequent requests -} catch (error) { - console.error('Login failed:', error.message); -} - -// Get user profile -try { - const profile = await client.users.getProfile(); - console.log('User profile:', profile); -} catch (error) { - console.error('Failed to get profile:', error.message); -} -``` - -### With API Key - -```typescript -const client = new AuthFrameworkClient({ - baseUrl: 'https://api.yourdomain.com', - apiKey: 'your-api-key', // For endpoints that support API key auth -}); -``` - -## Authentication - -### Login and Token Management - -```typescript -// Login with username/password -const loginResponse = await client.auth.login({ - username: 'user@example.com', - password: 'password123', - remember_me: true // Optional -}); - -// Access token is automatically stored and used for subsequent requests -console.log('Access token expires in:', loginResponse.expires_in, 'seconds'); - -// Refresh token when needed -const tokenResponse = await client.auth.refreshToken({ - refresh_token: loginResponse.refresh_token -}); - -// Validate current token -const userInfo = await client.auth.validate(); - -// Logout -await client.auth.logout(); -``` - -### Manual Token Management - -```typescript -// Set access token manually -client.setAccessToken('your-jwt-token'); - -// Get current token -const token = client.getAccessToken(); - -// Clear token -client.clearAccessToken(); -``` - -## User Management - -### Profile Operations - -```typescript -// Get user profile -const profile = await client.users.getProfile(); - -// Update profile -const updatedProfile = await client.users.updateProfile({ - first_name: 'John', - last_name: 'Doe', - phone: '+1234567890', - timezone: 'America/New_York' -}); - -// Change password -await client.users.changePassword({ - current_password: 'oldPassword123', - new_password: 'newPassword456' -}); -``` - -## Multi-Factor Authentication - -### MFA Setup and Management - -```typescript -// Setup MFA -const mfaSetup = await client.mfa.setup(); -console.log('QR Code:', mfaSetup.qr_code); -console.log('Setup URI:', mfaSetup.setup_uri); -console.log('Backup codes:', mfaSetup.backup_codes); - -// Verify MFA code -const verifyResult = await client.mfa.verify({ - code: '123456' // 6-digit TOTP code -}); - -if (verifyResult.verified) { - console.log('MFA verification successful'); -} - -// Disable MFA -await client.mfa.disable({ - password: 'currentPassword', - code: '123456' // Current MFA code -}); -``` - -## OAuth 2.0 - -### Authorization Code Flow - -```typescript -// Generate authorization URL -const authUrl = client.oauth.getAuthorizeUrl({ - response_type: 'code', - client_id: 'your-client-id', - redirect_uri: 'https://yourapp.com/callback', - scope: 'read write', - state: 'random-state-string' -}); - -// Redirect user to authUrl... - -// Exchange code for tokens (in your callback handler) -const tokenResponse = await client.oauth.getToken({ - grant_type: 'authorization_code', - code: 'received-auth-code', - client_id: 'your-client-id', - client_secret: 'your-client-secret', - redirect_uri: 'https://yourapp.com/callback' -}); - -// Introspect token -const tokenInfo = await client.oauth.introspectToken({ - token: tokenResponse.access_token, - client_id: 'your-client-id', - client_secret: 'your-client-secret' -}); - -// Revoke token -await client.oauth.revokeToken({ - token: tokenResponse.access_token, - client_id: 'your-client-id', - client_secret: 'your-client-secret' -}); -``` - -### PKCE Flow (Recommended for SPAs) - -```typescript -// Generate code verifier and challenge (you'll need a PKCE library) -import { generateCodeVerifier, generateCodeChallenge } from 'your-pkce-lib'; - -const codeVerifier = generateCodeVerifier(); -const codeChallenge = generateCodeChallenge(codeVerifier); - -// Authorization URL with PKCE -const authUrl = client.oauth.getAuthorizeUrl({ - response_type: 'code', - client_id: 'your-spa-client-id', - redirect_uri: 'https://yourapp.com/callback', - scope: 'read write', - code_challenge: codeChallenge, - code_challenge_method: 'S256', - state: 'random-state-string' -}); - -// Exchange code with PKCE -const tokenResponse = await client.oauth.getToken({ - grant_type: 'authorization_code', - code: 'received-auth-code', - client_id: 'your-spa-client-id', - redirect_uri: 'https://yourapp.com/callback', - code_verifier: codeVerifier -}); -``` - -## Administrative Functions - -### User Management (Admin Only) - -```typescript -// List users with pagination -const usersResponse = await client.admin.listUsers({ - page: 1, - limit: 20, - search: 'john@', - role: 'user' -}); - -console.log('Users:', usersResponse.data); -console.log('Pagination:', usersResponse.pagination); - -// Create user -const newUser = await client.admin.createUser({ - username: 'newuser@example.com', - email: 'newuser@example.com', - password: 'tempPassword123', - roles: ['user'], - first_name: 'Jane', - last_name: 'Smith' -}); - -// Get user details -const userDetails = await client.admin.getUser('user-id-123'); - -// Delete user -await client.admin.deleteUser('user-id-123'); - -// Get system statistics -const stats = await client.admin.getSystemStats(); -console.log('System stats:', stats); -``` - -## Health Monitoring - -### Health Checks - -```typescript -// Basic health check -const health = await client.health.getHealth(); -console.log('Service status:', health.status); - -// Detailed health check -const detailedHealth = await client.health.getDetailedHealth(); -console.log('Database status:', detailedHealth.services.database.status); -console.log('Uptime:', detailedHealth.uptime, 'seconds'); - -// Get Prometheus metrics -const metrics = await client.health.getMetrics(); -console.log('Metrics:', metrics); -``` - -## Error Handling - -### Error Types - -The SDK provides specific error types for different scenarios: - -```typescript -import { - AuthFrameworkError, - AuthenticationError, - AuthorizationError, - ValidationError, - NotFoundError, - RateLimitError, - isAuthFrameworkError -} from '@authframework/js-sdk'; - -try { - await client.auth.login({ username: 'invalid', password: 'wrong' }); -} catch (error) { - if (isAuthFrameworkError(error)) { - console.log('Error code:', error.code); - console.log('Status code:', error.statusCode); - console.log('Details:', error.details); - - if (error instanceof AuthenticationError) { - console.log('Authentication failed'); - } else if (error instanceof RateLimitError) { - console.log('Rate limited. Retry after:', error.retryAfter); - } - } -} -``` - -### Retry Logic - -The SDK automatically retries failed requests for network errors and 5xx server errors: - -```typescript -const client = new AuthFrameworkClient({ - baseUrl: 'https://api.example.com', - retries: 5, // Retry up to 5 times - timeout: 10000 // 10 second timeout -}); - -// You can also override per request -try { - const profile = await client.users.getProfile({ - retries: 2, - timeout: 5000 - }); -} catch (error) { - // Handle error after retries exhausted -} -``` - -## TypeScript Support - -The SDK is built with TypeScript and provides full type safety: - -```typescript -import { - UserProfile, - LoginResponse, - ApiResponse, - PaginatedResponse -} from '@authframework/js-sdk'; - -// Type-safe responses -const loginResponse: LoginResponse = await client.auth.login({ - username: 'user@example.com', - password: 'password' -}); - -// Intellisense and type checking -const profile: UserProfile = await client.users.getProfile(); -console.log(profile.first_name); // TypeScript knows this is string | undefined - -// Generic responses -const users: PaginatedResponse = await client.admin.listUsers(); -``` - -## Configuration Options - -### Client Configuration - -```typescript -interface ClientConfig { - baseUrl: string; // Required: API base URL - timeout?: number; // Request timeout in ms (default: 30000) - retries?: number; // Number of retries (default: 3) - apiKey?: string; // API key for endpoints that support it - userAgent?: string; // Custom user agent -} -``` - -### Request Options - -```typescript -interface RequestOptions { - timeout?: number; // Override default timeout - retries?: number; // Override default retries - headers?: Record; // Additional headers -} - -// Example usage -await client.users.getProfile({ - timeout: 5000, - retries: 1, - headers: { - 'X-Custom-Header': 'value' - } -}); -``` - -## Browser Support - -The SDK works in modern browsers and Node.js environments: - -- **Browsers**: Chrome 70+, Firefox 65+, Safari 12+, Edge 79+ -- **Node.js**: 16+ - -### Browser Bundle - -```html - - - -``` - -## React Integration - -Example React hook for authentication: - -```typescript -import { useState, useEffect } from 'react'; -import { AuthFrameworkClient, UserInfo } from '@authframework/js-sdk'; - -const client = new AuthFrameworkClient({ - baseUrl: process.env.REACT_APP_API_URL! -}); - -export function useAuth() { - const [user, setUser] = useState(null); - const [loading, setLoading] = useState(true); - - useEffect(() => { - // Check for existing token on mount - const token = localStorage.getItem('access_token'); - if (token) { - client.setAccessToken(token); - client.auth.validate() - .then(setUser) - .catch(() => localStorage.removeItem('access_token')) - .finally(() => setLoading(false)); - } else { - setLoading(false); - } - }, []); - - const login = async (username: string, password: string) => { - const response = await client.auth.login({ username, password }); - localStorage.setItem('access_token', response.access_token); - setUser(response.user); - return response; - }; - - const logout = async () => { - await client.auth.logout(); - localStorage.removeItem('access_token'); - setUser(null); - }; - - return { user, loading, login, logout, client }; -} -``` - -## Contributing - -1. Fork the repository -2. Create your feature branch (`git checkout -b feature/amazing-feature`) -3. Commit your changes (`git commit -m 'Add some amazing feature'`) -4. Push to the branch (`git push origin feature/amazing-feature`) -5. Open a Pull Request - -## License - -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/sdks/javascript/jest.config.js b/sdks/javascript/jest.config.js deleted file mode 100644 index 8b6f828..0000000 --- a/sdks/javascript/jest.config.js +++ /dev/null @@ -1,25 +0,0 @@ -export default { - preset: 'ts-jest', - testEnvironment: 'jsdom', - extensionsToTreatAsEsm: ['.ts'], - moduleNameMapping: { - '^(\\.{1,2}/.*)\\.js$': '$1', - }, - transform: { - '^.+\\.tsx?$': ['ts-jest', { - useESM: true, - }], - }, - testMatch: [ - '**/__tests__/**/*.test.ts', - '**/?(*.)+(spec|test).ts', - ], - collectCoverageFrom: [ - 'src/**/*.ts', - '!src/**/*.d.ts', - '!src/**/__tests__/**', - ], - coverageDirectory: 'coverage', - coverageReporters: ['text', 'lcov', 'html'], - setupFilesAfterEnv: ['/jest.setup.ts'], -}; diff --git a/sdks/javascript/jest.setup.ts b/sdks/javascript/jest.setup.ts deleted file mode 100644 index 11ac8c3..0000000 --- a/sdks/javascript/jest.setup.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Jest setup file -import 'jest-environment-jsdom'; - -// Mock console methods to reduce noise in tests -global.console = { - ...console, - warn: jest.fn(), - error: jest.fn(), -}; - -// Mock fetch for testing -global.fetch = jest.fn(); diff --git a/sdks/javascript/package-build.json b/sdks/javascript/package-build.json deleted file mode 100644 index 6099ca6..0000000 --- a/sdks/javascript/package-build.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "rollup.config.js", - "version": "1.0.0", - "type": "module", - "main": "./rollup.config.js", - "dependencies": { - "@rollup/plugin-typescript": "^11.1.5", - "@rollup/plugin-node-resolve": "^15.2.3", - "@rollup/plugin-commonjs": "^25.0.7", - "rollup": "^4.9.0", - "rollup-plugin-dts": "^6.1.0", - "typescript": "^5.3.3" - } -} \ No newline at end of file diff --git a/sdks/javascript/package-test.json b/sdks/javascript/package-test.json deleted file mode 100644 index 78106b1..0000000 --- a/sdks/javascript/package-test.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "jest.config.js", - "version": "1.0.0", - "type": "module", - "main": "./jest.config.js", - "dependencies": { - "@types/jest": "^29.5.8", - "jest": "^29.7.0", - "jest-environment-jsdom": "^29.7.0", - "ts-jest": "^29.1.1" - } -} \ No newline at end of file diff --git a/sdks/javascript/package.json b/sdks/javascript/package.json deleted file mode 100644 index 7554985..0000000 --- a/sdks/javascript/package.json +++ /dev/null @@ -1,77 +0,0 @@ -{ - "name": "@authframework/js-sdk", - "version": "1.0.0", - "description": "Official JavaScript/TypeScript SDK for AuthFramework REST API", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "module": "dist/index.esm.js", - "files": [ - "dist/**/*", - "README.md", - "LICENSE" - ], - "scripts": { - "build": "rollup -c", - "build:watch": "rollup -c -w", - "test": "jest", - "test:watch": "jest --watch", - "test:coverage": "jest --coverage", - "lint": "eslint src/**/*.ts", - "lint:fix": "eslint src/**/*.ts --fix", - "typecheck": "tsc --noEmit", - "clean": "rimraf dist", - "prepublishOnly": "npm run clean && npm run build" - }, - "keywords": [ - "auth", - "authentication", - "authorization", - "jwt", - "oauth", - "mfa", - "rest-api", - "sdk", - "typescript", - "javascript" - ], - "author": "AuthFramework Team", - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/cires/AuthFramework.git", - "directory": "sdks/javascript" - }, - "bugs": { - "url": "https://github.com/cires/AuthFramework/issues" - }, - "homepage": "https://github.com/cires/AuthFramework#readme", - "devDependencies": { - "@types/jest": "^29.5.8", - "@types/node": "^20.9.0", - "@typescript-eslint/eslint-plugin": "^6.12.0", - "@typescript-eslint/parser": "^6.12.0", - "eslint": "^8.54.0", - "jest": "^29.7.0", - "rimraf": "^5.0.5", - "rollup": "^4.5.0", - "@rollup/plugin-typescript": "^11.1.5", - "@rollup/plugin-node-resolve": "^15.2.3", - "@rollup/plugin-commonjs": "^25.0.7", - "rollup-plugin-dts": "^6.1.0", - "ts-jest": "^29.1.1", - "typescript": "^5.2.2" - }, - "dependencies": { - "axios": "^1.6.2" - }, - "engines": { - "node": ">=16.0.0" - }, - "exports": { - ".": { - "import": "./dist/index.esm.js", - "require": "./dist/index.js", - "types": "./dist/index.d.ts" - } - } -} \ No newline at end of file diff --git a/sdks/javascript/rollup.config.js b/sdks/javascript/rollup.config.js deleted file mode 100644 index 3f9f525..0000000 --- a/sdks/javascript/rollup.config.js +++ /dev/null @@ -1,47 +0,0 @@ -import typescript from '@rollup/plugin-typescript'; -import resolve from '@rollup/plugin-node-resolve'; -import commonjs from '@rollup/plugin-commonjs'; -import dts from 'rollup-plugin-dts'; - -export default [ - // ESM and CJS builds - { - input: 'src/index.ts', - output: [ - { - file: 'dist/index.esm.js', - format: 'esm', - sourcemap: true, - }, - { - file: 'dist/index.cjs.js', - format: 'cjs', - sourcemap: true, - exports: 'named', - }, - ], - plugins: [ - resolve({ - preferBuiltins: false, - browser: true, - }), - commonjs(), - typescript({ - tsconfig: './tsconfig.json', - declaration: false, - outDir: 'dist', - }), - ], - external: ['axios'], - }, - // Type definitions - { - input: 'src/index.ts', - output: { - file: 'dist/index.d.ts', - format: 'es', - }, - plugins: [dts()], - external: ['axios'], - }, -]; diff --git a/sdks/javascript/src/base-client.ts b/sdks/javascript/src/base-client.ts deleted file mode 100644 index 40d239a..0000000 --- a/sdks/javascript/src/base-client.ts +++ /dev/null @@ -1,231 +0,0 @@ -/** - * Base HTTP client for AuthFramework API - */ - -import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; -import { - ApiResponse, - ApiError, - ClientConfig, - RequestOptions -} from './types'; -import { - AuthFrameworkError, - createErrorFromResponse, - NetworkError, - TimeoutError, - isRetryableError -} from './errors'; - -/** - * Base HTTP client with retry logic and error handling - */ -export class BaseClient { - protected readonly axios: AxiosInstance; - protected readonly config: ClientConfig & { - timeout: number; - retries: number; - userAgent: string; - }; - private accessToken?: string; - - constructor(config: ClientConfig) { - this.config = { - timeout: 30000, - retries: 3, - userAgent: 'AuthFramework-JS-SDK/1.0.0', - ...config, - }; - - this.axios = axios.create({ - baseURL: this.config.baseUrl, - timeout: this.config.timeout, - headers: { - 'Content-Type': 'application/json', - 'User-Agent': this.config.userAgent, - ...(this.config.apiKey && { 'X-API-Key': this.config.apiKey }), - }, - }); - - // Add request interceptor to add auth header - this.axios.interceptors.request.use((config: any) => { - if (this.accessToken) { - config.headers.Authorization = `Bearer ${this.accessToken}`; - } - return config; - }); - - // Add response interceptor for error handling - this.axios.interceptors.response.use( - (response: any) => response, - (error: any) => this.handleResponseError(error) - ); - } - - /** - * Set the access token for authenticated requests - */ - public setAccessToken(token: string): void { - this.accessToken = token; - } - - /** - * Clear the access token - */ - public clearAccessToken(): void { - delete (this as any).accessToken; - } - - /** - * Get the current access token - */ - public getAccessToken(): string | undefined { - return this.accessToken; - } - - /** - * Make a GET request - */ - protected async get( - url: string, - options?: RequestOptions & { params?: Record } - ): Promise> { - return this.request('GET', url, undefined, options); - } - - /** - * Make a POST request - */ - protected async post( - url: string, - data?: any, - options?: RequestOptions - ): Promise> { - return this.request('POST', url, data, options); - } - - /** - * Make a PATCH request - */ - protected async patch( - url: string, - data?: any, - options?: RequestOptions - ): Promise> { - return this.request('PATCH', url, data, options); - } - - /** - * Make a PUT request - */ - protected async put( - url: string, - data?: any, - options?: RequestOptions - ): Promise> { - return this.request('PUT', url, data, options); - } - - /** - * Make a DELETE request - */ - protected async delete( - url: string, - options?: RequestOptions - ): Promise> { - return this.request('DELETE', url, undefined, options); - } - - /** - * Make a request with retry logic - */ - private async request( - method: string, - url: string, - data?: any, - options?: RequestOptions & { params?: Record } - ): Promise> { - const retries = options?.retries ?? this.config.retries; - const timeout = options?.timeout ?? this.config.timeout; - - const axiosConfig: AxiosRequestConfig = { - method, - url, - data, - timeout, - params: options?.params, - headers: options?.headers, - }; - - for (let attempt = 0; attempt <= retries; attempt++) { - try { - const response: AxiosResponse> = await this.axios(axiosConfig); - return response.data; - } catch (error: any) { - // Don't retry on the last attempt or for non-retryable errors - if (attempt === retries || !isRetryableError(error)) { - throw error; - } - - // Exponential backoff delay - const delay = Math.min(1000 * Math.pow(2, attempt), 10000); - await new Promise(resolve => setTimeout(resolve, delay)); - } - } - - // This should never be reached, but TypeScript requires it - throw new AuthFrameworkError('Max retries exceeded'); - } - - /** - * Handle axios response errors and convert to AuthFramework errors - */ - private handleResponseError(error: any): never { - if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') { - throw new TimeoutError('Request timeout', { originalError: error }); - } - - if (!error.response) { - throw new NetworkError('Network error', { originalError: error }); - } - - const { status, data } = error.response; - const errorData = data?.error || data; - - throw createErrorFromResponse(status, errorData, error.message); - } - - /** - * Make a form-encoded request (for OAuth endpoints) - */ - protected async postForm( - url: string, - data: Record, - options?: RequestOptions - ): Promise { - const formData = new URLSearchParams(); - Object.entries(data).forEach(([key, value]) => { - if (value !== undefined && value !== null) { - formData.append(key, value); - } - }); - - const axiosConfig: AxiosRequestConfig = { - method: 'POST', - url, - data: formData, - timeout: options?.timeout ?? this.config.timeout, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - ...options?.headers, - }, - }; - - try { - const response: AxiosResponse = await this.axios(axiosConfig); - return response.data; - } catch (error: any) { - this.handleResponseError(error); - } - } -} diff --git a/sdks/javascript/src/client.ts b/sdks/javascript/src/client.ts deleted file mode 100644 index 4db866a..0000000 --- a/sdks/javascript/src/client.ts +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Main AuthFramework client - */ - -import { BaseClient } from './base-client'; -import { ClientConfig } from './types'; -import { AuthModule } from './modules/auth'; -import { UsersModule } from './modules/users'; -import { MFAModule } from './modules/mfa'; -import { OAuthModule } from './modules/oauth'; -import { AdminModule } from './modules/admin'; -import { HealthModule } from './modules/health'; - -/** - * Main AuthFramework API client - */ -export class AuthFrameworkClient extends BaseClient { - public readonly auth: AuthModule; - public readonly users: UsersModule; - public readonly mfa: MFAModule; - public readonly oauth: OAuthModule; - public readonly admin: AdminModule; - public readonly health: HealthModule; - - constructor(config: ClientConfig) { - super(config); - - // Initialize modules with the same configuration - this.auth = new AuthModule(config); - this.users = new UsersModule(config); - this.mfa = new MFAModule(config); - this.oauth = new OAuthModule(config); - this.admin = new AdminModule(config); - this.health = new HealthModule(config); - - // Sync access tokens between client and modules - this.syncAccessToken(); - } - - /** - * Set access token for all modules - */ - public setAccessToken(token: string): void { - super.setAccessToken(token); - this.auth.setAccessToken(token); - this.users.setAccessToken(token); - this.mfa.setAccessToken(token); - this.oauth.setAccessToken(token); - this.admin.setAccessToken(token); - this.health.setAccessToken(token); - } - - /** - * Clear access token from all modules - */ - public clearAccessToken(): void { - super.clearAccessToken(); - this.auth.clearAccessToken(); - this.users.clearAccessToken(); - this.mfa.clearAccessToken(); - this.oauth.clearAccessToken(); - this.admin.clearAccessToken(); - this.health.clearAccessToken(); - } - - /** - * Sync access token between main client and modules - */ - private syncAccessToken(): void { - const token = this.getAccessToken(); - if (token) { - this.auth.setAccessToken(token); - this.users.setAccessToken(token); - this.mfa.setAccessToken(token); - this.oauth.setAccessToken(token); - this.admin.setAccessToken(token); - this.health.setAccessToken(token); - } - } -} diff --git a/sdks/javascript/src/errors.ts b/sdks/javascript/src/errors.ts deleted file mode 100644 index 011896d..0000000 --- a/sdks/javascript/src/errors.ts +++ /dev/null @@ -1,152 +0,0 @@ -/** - * Error classes for the AuthFramework SDK - */ - -export class AuthFrameworkError extends Error { - public readonly code: string; - public readonly details?: any; - public readonly statusCode?: number; - - constructor(message: string, code: string = 'UNKNOWN_ERROR', details?: any, statusCode?: number) { - super(message); - this.name = 'AuthFrameworkError'; - this.code = code; - this.details = details; - this.statusCode = statusCode ?? undefined; - - // Maintains proper stack trace for where our error was thrown (only available on V8) - if ('captureStackTrace' in Error) { - (Error as any).captureStackTrace(this, AuthFrameworkError); - } - } -} - -export class ValidationError extends AuthFrameworkError { - constructor(message: string, details?: any) { - super(message, 'VALIDATION_ERROR', details, 400); - this.name = 'ValidationError'; - } -} - -export class AuthenticationError extends AuthFrameworkError { - constructor(message: string = 'Authentication failed', details?: any) { - super(message, 'AUTHENTICATION_ERROR', details, 401); - this.name = 'AuthenticationError'; - } -} - -export class AuthorizationError extends AuthFrameworkError { - constructor(message: string = 'Insufficient permissions', details?: any) { - super(message, 'AUTHORIZATION_ERROR', details, 403); - this.name = 'AuthorizationError'; - } -} - -export class NotFoundError extends AuthFrameworkError { - constructor(message: string = 'Resource not found', details?: any) { - super(message, 'NOT_FOUND_ERROR', details, 404); - this.name = 'NotFoundError'; - } -} - -export class ConflictError extends AuthFrameworkError { - constructor(message: string = 'Resource conflict', details?: any) { - super(message, 'CONFLICT_ERROR', details, 409); - this.name = 'ConflictError'; - } -} - -export class RateLimitError extends AuthFrameworkError { - public readonly retryAfter?: number; - - constructor(message: string = 'Rate limit exceeded', retryAfter?: number, details?: any) { - super(message, 'RATE_LIMIT_ERROR', details, 429); - this.name = 'RateLimitError'; - this.retryAfter = retryAfter ?? undefined; - } -} - -export class ServerError extends AuthFrameworkError { - constructor(message: string = 'Internal server error', details?: any, statusCode: number = 500) { - super(message, 'SERVER_ERROR', details, statusCode); - this.name = 'ServerError'; - } -} - -export class NetworkError extends AuthFrameworkError { - constructor(message: string = 'Network error', details?: any) { - super(message, 'NETWORK_ERROR', details); - this.name = 'NetworkError'; - } -} - -export class TimeoutError extends AuthFrameworkError { - constructor(message: string = 'Request timeout', details?: any) { - super(message, 'TIMEOUT_ERROR', details); - this.name = 'TimeoutError'; - } -} - -/** - * Creates an appropriate error instance based on HTTP status code and error response - */ -export function createErrorFromResponse( - statusCode: number, - errorResponse?: { code: string; message: string; details?: any }, - defaultMessage?: string -): AuthFrameworkError { - const message = errorResponse?.message || defaultMessage || 'An error occurred'; - const code = errorResponse?.code || 'UNKNOWN_ERROR'; - const details = errorResponse?.details; - - switch (statusCode) { - case 400: - return new ValidationError(message, details); - case 401: - return new AuthenticationError(message, details); - case 403: - return new AuthorizationError(message, details); - case 404: - return new NotFoundError(message, details); - case 409: - return new ConflictError(message, details); - case 429: - return new RateLimitError(message, undefined, details); - case 500: - case 502: - case 503: - case 504: - return new ServerError(message, details, statusCode); - default: - return new AuthFrameworkError(message, code, details, statusCode); - } -} - -/** - * Type guard to check if an error is an AuthFrameworkError - */ -export function isAuthFrameworkError(error: any): error is AuthFrameworkError { - return error instanceof AuthFrameworkError; -} - -/** - * Type guard to check if an error is a network-related error - */ -export function isNetworkError(error: any): error is NetworkError | TimeoutError { - return error instanceof NetworkError || error instanceof TimeoutError; -} - -/** - * Type guard to check if an error is retryable (network errors and 5xx server errors) - */ -export function isRetryableError(error: any): boolean { - if (isNetworkError(error)) { - return true; - } - - if (isAuthFrameworkError(error) && error.statusCode) { - return error.statusCode >= 500; - } - - return false; -} diff --git a/sdks/javascript/src/index.ts b/sdks/javascript/src/index.ts deleted file mode 100644 index bc8efec..0000000 --- a/sdks/javascript/src/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * AuthFramework JavaScript/TypeScript SDK - * - * Official client library for the AuthFramework REST API. - * Provides type-safe access to authentication, user management, - * MFA, OAuth 2.0, and administrative features. - */ - -export * from './client'; -export * from './types'; -export * from './errors'; -export * from './modules'; - -// Re-export the main client class for convenience -export { AuthFrameworkClient as default } from './client'; diff --git a/sdks/javascript/src/modules/admin.ts b/sdks/javascript/src/modules/admin.ts deleted file mode 100644 index 528e71d..0000000 --- a/sdks/javascript/src/modules/admin.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Administrative module for AuthFramework SDK - */ - -import { BaseClient } from '../base-client'; -import { - UserInfo, - CreateUserRequest, - SystemStats, - PaginatedResponse, - UserListOptions, - RequestOptions -} from '../types'; - -export class AdminModule extends BaseClient { - /** - * List users with pagination and filtering - */ - async listUsers(options?: UserListOptions & RequestOptions): Promise> { - const params: Record = {}; - - if (options?.page) params.page = options.page; - if (options?.limit) params.limit = options.limit; - if (options?.search) params.search = options.search; - if (options?.role) params.role = options.role; - if (options?.sort) params.sort = options.sort; - if (options?.order) params.order = options.order; - - const response = await this.get('/admin/users', { - ...options, - params - }); - - return response as PaginatedResponse; - } - - /** - * Create a new user - */ - async createUser(request: CreateUserRequest, options?: RequestOptions): Promise { - const response = await this.post('/admin/users', request, options); - return response.data; - } - - /** - * Get user details by ID - */ - async getUser(userId: string, options?: RequestOptions): Promise { - const response = await this.get(`/admin/users/${userId}`, options); - return response.data; - } - - /** - * Delete a user by ID - */ - async deleteUser(userId: string, options?: RequestOptions): Promise { - await this.delete(`/admin/users/${userId}`, options); - } - - /** - * Get system statistics - */ - async getSystemStats(options?: RequestOptions): Promise { - const response = await this.get('/admin/stats', options); - return response.data; - } -} diff --git a/sdks/javascript/src/modules/auth.ts b/sdks/javascript/src/modules/auth.ts deleted file mode 100644 index 3aca182..0000000 --- a/sdks/javascript/src/modules/auth.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Authentication module for AuthFramework SDK - */ - -import { BaseClient } from '../base-client'; -import { - LoginRequest, - LoginResponse, - RefreshTokenRequest, - TokenResponse, - UserInfo, - RequestOptions -} from '../types'; - -export class AuthModule extends BaseClient { - /** - * Authenticate user with username and password - */ - async login(request: LoginRequest, options?: RequestOptions): Promise { - const response = await this.post('/auth/login', request, options); - - // Automatically set the access token for subsequent requests - this.setAccessToken(response.data.access_token); - - return response.data; - } - - /** - * Refresh access token using refresh token - */ - async refreshToken(request: RefreshTokenRequest, options?: RequestOptions): Promise { - const response = await this.post('/auth/refresh', request, options); - - // Update the access token - this.setAccessToken(response.data.access_token); - - return response.data; - } - - /** - * Logout and invalidate current session - */ - async logout(options?: RequestOptions): Promise { - await this.post('/auth/logout', undefined, options); - - // Clear the access token - this.clearAccessToken(); - } - - /** - * Validate current access token and get user info - */ - async validate(options?: RequestOptions): Promise { - const response = await this.post('/auth/validate', undefined, options); - return response.data; - } -} diff --git a/sdks/javascript/src/modules/health.ts b/sdks/javascript/src/modules/health.ts deleted file mode 100644 index 8bbca15..0000000 --- a/sdks/javascript/src/modules/health.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Health monitoring module for AuthFramework SDK - */ - -import { BaseClient } from '../base-client'; -import { - HealthStatus, - DetailedHealthStatus, - RequestOptions -} from '../types'; - -export class HealthModule extends BaseClient { - /** - * Get basic health status - */ - async getHealth(options?: RequestOptions): Promise { - const response = await this.get('/health', options); - return response.data; - } - - /** - * Get detailed health status including dependencies - */ - async getDetailedHealth(options?: RequestOptions): Promise { - const response = await this.get('/health/detailed', options); - return response.data; - } - - /** - * Get Prometheus metrics (returns raw text) - */ - async getMetrics(options?: RequestOptions): Promise { - // Override content type for metrics endpoint - const response = await this.get('/metrics', { - ...options, - headers: { - 'Accept': 'text/plain', - ...options?.headers - } - }); - - return response.data; - } -} diff --git a/sdks/javascript/src/modules/index.ts b/sdks/javascript/src/modules/index.ts deleted file mode 100644 index ebf7fc1..0000000 --- a/sdks/javascript/src/modules/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Module exports - */ - -export * from './auth'; -export * from './users'; -export * from './mfa'; -export * from './oauth'; -export * from './admin'; -export * from './health'; diff --git a/sdks/javascript/src/modules/mfa.ts b/sdks/javascript/src/modules/mfa.ts deleted file mode 100644 index 6731016..0000000 --- a/sdks/javascript/src/modules/mfa.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Multi-Factor Authentication module for AuthFramework SDK - */ - -import { BaseClient } from '../base-client'; -import { - MFASetupResponse, - MFAVerifyRequest, - MFAVerifyResponse, - DisableMFARequest, - RequestOptions -} from '../types'; - -export class MFAModule extends BaseClient { - /** - * Setup MFA for current user - */ - async setup(options?: RequestOptions): Promise { - const response = await this.post('/mfa/setup', undefined, options); - return response.data; - } - - /** - * Verify MFA code - */ - async verify(request: MFAVerifyRequest, options?: RequestOptions): Promise { - const response = await this.post('/mfa/verify', request, options); - return response.data; - } - - /** - * Disable MFA for current user - */ - async disable(request: DisableMFARequest, options?: RequestOptions): Promise { - await this.post('/mfa/disable', request, options); - } -} diff --git a/sdks/javascript/src/modules/oauth.ts b/sdks/javascript/src/modules/oauth.ts deleted file mode 100644 index 3eeebcc..0000000 --- a/sdks/javascript/src/modules/oauth.ts +++ /dev/null @@ -1,80 +0,0 @@ -/** - * OAuth 2.0 module for AuthFramework SDK - */ - -import { BaseClient } from '../base-client'; -import { - OAuthTokenRequest, - OAuthTokenResponse, - RevokeTokenRequest, - IntrospectTokenRequest, - TokenIntrospectionResponse, - OAuthAuthorizeParams, - RequestOptions -} from '../types'; - -export class OAuthModule extends BaseClient { - /** - * Generate OAuth authorization URL - */ - getAuthorizeUrl(params: OAuthAuthorizeParams): string { - const url = new URL('/oauth/authorize', this.config.baseUrl); - - Object.entries(params).forEach(([key, value]) => { - if (value !== undefined && value !== null) { - url.searchParams.append(key, value.toString()); - } - }); - - return url.toString(); - } - - /** - * Exchange authorization code for tokens - */ - async getToken(request: OAuthTokenRequest, options?: RequestOptions): Promise { - // OAuth token endpoint expects form-encoded data - const formData: Record = {}; - - Object.entries(request).forEach(([key, value]) => { - if (value !== undefined && value !== null) { - formData[key] = value.toString(); - } - }); - - return this.postForm('/oauth/token', formData, options); - } - - /** - * Revoke an OAuth token - */ - async revokeToken(request: RevokeTokenRequest, options?: RequestOptions): Promise { - const formData: Record = {}; - - Object.entries(request).forEach(([key, value]) => { - if (value !== undefined && value !== null) { - formData[key] = value.toString(); - } - }); - - await this.postForm('/oauth/revoke', formData, options); - } - - /** - * Introspect an OAuth token - */ - async introspectToken( - request: IntrospectTokenRequest, - options?: RequestOptions - ): Promise { - const formData: Record = {}; - - Object.entries(request).forEach(([key, value]) => { - if (value !== undefined && value !== null) { - formData[key] = value.toString(); - } - }); - - return this.postForm('/oauth/introspect', formData, options); - } -} diff --git a/sdks/javascript/src/modules/users.ts b/sdks/javascript/src/modules/users.ts deleted file mode 100644 index 543bc47..0000000 --- a/sdks/javascript/src/modules/users.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * User management module for AuthFramework SDK - */ - -import { BaseClient } from '../base-client'; -import { - UserProfile, - UpdateProfileRequest, - ChangePasswordRequest, - RequestOptions -} from '../types'; - -export class UsersModule extends BaseClient { - /** - * Get current user's profile - */ - async getProfile(options?: RequestOptions): Promise { - const response = await this.get('/users/profile', options); - return response.data; - } - - /** - * Update current user's profile - */ - async updateProfile(request: UpdateProfileRequest, options?: RequestOptions): Promise { - const response = await this.patch('/users/profile', request, options); - return response.data; - } - - /** - * Change current user's password - */ - async changePassword(request: ChangePasswordRequest, options?: RequestOptions): Promise { - await this.post('/users/password', request, options); - } -} diff --git a/sdks/javascript/src/types.ts b/sdks/javascript/src/types.ts deleted file mode 100644 index 47bf5f2..0000000 --- a/sdks/javascript/src/types.ts +++ /dev/null @@ -1,262 +0,0 @@ -/** - * Type definitions for the AuthFramework API - */ - -// Base API Response Types -export interface ApiResponse { - success: boolean; - data: T; - timestamp: string; -} - -export interface ApiError { - success: false; - error: { - code: string; - message: string; - details?: any; - }; - timestamp: string; -} - -export interface Pagination { - page: number; - limit: number; - total: number; - has_next: boolean; - has_prev: boolean; -} - -// Authentication Types -export interface LoginRequest { - username: string; - password: string; - remember_me?: boolean; -} - -export interface LoginResponse { - access_token: string; - refresh_token: string; - token_type: string; - expires_in: number; - user: UserInfo; -} - -export interface RefreshTokenRequest { - refresh_token: string; -} - -export interface TokenResponse { - access_token: string; - token_type: string; - expires_in: number; -} - -// User Types -export interface UserInfo { - id: string; - username: string; - email: string; - roles: string[]; - mfa_enabled: boolean; - created_at: string; - last_login?: string; -} - -export interface UserProfile { - id: string; - username: string; - email: string; - first_name?: string; - last_name?: string; - phone?: string; - timezone?: string; - locale?: string; - mfa_enabled: boolean; - created_at: string; - updated_at: string; -} - -export interface UpdateProfileRequest { - first_name?: string; - last_name?: string; - phone?: string; - timezone?: string; - locale?: string; -} - -export interface ChangePasswordRequest { - current_password: string; - new_password: string; -} - -export interface CreateUserRequest { - username: string; - email: string; - password: string; - roles?: string[]; - first_name?: string; - last_name?: string; -} - -// MFA Types -export interface MFASetupResponse { - secret: string; - qr_code: string; - backup_codes: string[]; - setup_uri: string; -} - -export interface MFAVerifyRequest { - code: string; -} - -export interface MFAVerifyResponse { - verified: boolean; - backup_codes?: string[]; -} - -export interface DisableMFARequest { - password: string; - code: string; -} - -// OAuth Types -export interface OAuthTokenRequest { - grant_type: 'authorization_code' | 'refresh_token' | 'client_credentials'; - code?: string; - redirect_uri?: string; - client_id?: string; - client_secret?: string; - refresh_token?: string; - scope?: string; - code_verifier?: string; -} - -export interface OAuthTokenResponse { - access_token: string; - token_type: string; - expires_in: number; - refresh_token?: string; - scope?: string; -} - -export interface RevokeTokenRequest { - token: string; - token_type_hint?: 'access_token' | 'refresh_token'; - client_id?: string; - client_secret?: string; -} - -export interface IntrospectTokenRequest { - token: string; - token_type_hint?: 'access_token' | 'refresh_token'; - client_id?: string; - client_secret?: string; -} - -export interface TokenIntrospectionResponse { - active: boolean; - scope?: string; - client_id?: string; - username?: string; - token_type?: string; - exp?: number; - iat?: number; - sub?: string; - aud?: string; - iss?: string; -} - -// Health Types -export interface HealthStatus { - status: 'healthy' | 'unhealthy'; - timestamp: string; -} - -export interface ServiceHealth { - status: 'healthy' | 'unhealthy'; - response_time: number; - last_check: string; -} - -export interface DetailedHealthStatus { - status: 'healthy' | 'unhealthy'; - services: { - database: ServiceHealth; - cache: ServiceHealth; - storage: ServiceHealth; - }; - uptime: number; - version: string; - timestamp: string; -} - -// Admin Types -export interface SystemStats { - users: { - total: number; - active: number; - new_today: number; - }; - sessions: { - active: number; - peak_today: number; - }; - oauth: { - clients: number; - active_tokens: number; - }; - system: { - uptime: number; - memory_usage: number; - cpu_usage: number; - }; - timestamp: string; -} - -// Client Configuration -export interface ClientConfig { - baseUrl: string; - timeout?: number; - retries?: number; - apiKey?: string; - userAgent?: string; -} - -// Request Options -export interface RequestOptions { - timeout?: number; - retries?: number; - headers?: Record; -} - -// Paginated Response -export interface PaginatedResponse extends ApiResponse { - pagination: Pagination; -} - -// List Options -export interface ListOptions { - page?: number; - limit?: number; - search?: string; - sort?: string; - order?: 'asc' | 'desc'; -} - -// User List Options -export interface UserListOptions extends ListOptions { - role?: string; -} - -// OAuth Authorization Parameters -export interface OAuthAuthorizeParams { - response_type: 'code' | 'token'; - client_id: string; - redirect_uri?: string; - scope?: string; - state?: string; - code_challenge?: string; - code_challenge_method?: 'plain' | 'S256'; -} diff --git a/sdks/javascript/tsconfig.json b/sdks/javascript/tsconfig.json deleted file mode 100644 index a338502..0000000 --- a/sdks/javascript/tsconfig.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "module": "ESNext", - "moduleResolution": "node", - "lib": [ - "ES2020", - "DOM" - ], - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "removeComments": false, - "noImplicitAny": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, - "exactOptionalPropertyTypes": true, - "allowUnreachableCode": false, - "allowUnusedLabels": false - }, - "include": [ - "src/**/*" - ], - "exclude": [ - "node_modules", - "dist", - "**/*.test.ts", - "**/*.spec.ts" - ] -} \ No newline at end of file diff --git a/sdks/python/README.md b/sdks/python/README.md deleted file mode 100644 index e8aa9a5..0000000 --- a/sdks/python/README.md +++ /dev/null @@ -1,481 +0,0 @@ -# AuthFramework Python SDK - -The official Python client library for AuthFramework authentication and authorization services. - -## Features - -- **Async/await support** with httpx for high-performance HTTP requests -- **Type safety** with Pydantic models and comprehensive type hints -- **Automatic token management** with refresh handling -- **Error handling** with custom exceptions and retry logic -- **Context manager support** for proper resource cleanup -- **Full API coverage** for all AuthFramework endpoints - -## Installation - -```bash -pip install authframework -``` - -Or from source: - -```bash -cd sdks/python -pip install -e . -``` - -## Quick Start - -### Basic Usage - -```python -import asyncio -from authframework import AuthFrameworkClient - -async def main(): - # Create client instance - client = AuthFrameworkClient('http://localhost:8080') - - try: - # Login - login_response = await client.login('user@example.com', 'password') - print(f"Logged in as: {login_response.user.username}") - - # Get user profile - profile = await client.get_profile() - print(f"User ID: {profile.user_id}") - - # Update profile - await client.update_profile(display_name="New Name") - - finally: - await client.close() - -# Run the async function -asyncio.run(main()) -``` - -### Using Context Manager (Recommended) - -```python -import asyncio -from authframework import AuthFrameworkClient - -async def main(): - async with AuthFrameworkClient('http://localhost:8080') as client: - # Login - await client.login('user@example.com', 'password') - - # All API calls are automatically authenticated - profile = await client.get_profile() - print(f"Welcome, {profile.display_name}!") - -asyncio.run(main()) -``` - -## Authentication - -### Basic Login - -```python -async with AuthFrameworkClient('http://localhost:8080') as client: - # Login with username/password - response = await client.login('user@example.com', 'password') - - # Access tokens are automatically managed - print(f"Access token expires in: {response.expires_in} seconds") -``` - -### API Key Authentication - -```python -# For server-to-server authentication -client = AuthFrameworkClient( - 'http://localhost:8080', - api_key='your-api-key' -) -``` - -### Token Refresh - -```python -async with AuthFrameworkClient('http://localhost:8080') as client: - await client.login('user@example.com', 'password') - - # Tokens are automatically refreshed when needed - # You can also manually refresh: - new_tokens = await client.refresh_token() -``` - -## User Management - -### Profile Management - -```python -async with AuthFrameworkClient('http://localhost:8080') as client: - await client.login('user@example.com', 'password') - - # Get current user profile - profile = await client.get_profile() - print(f"Email: {profile.email}") - print(f"Display Name: {profile.display_name}") - print(f"MFA Enabled: {profile.mfa_enabled}") - - # Update profile - await client.update_profile( - display_name="New Display Name", - preferences={"theme": "dark", "language": "en"} - ) - - # Change password - await client.change_password( - current_password="old_password", - new_password="new_password" - ) -``` - -## Multi-Factor Authentication (MFA) - -### Setup MFA - -```python -async with AuthFrameworkClient('http://localhost:8080') as client: - await client.login('user@example.com', 'password') - - # Setup MFA - mfa_setup = await client.setup_mfa() - print(f"QR Code URL: {mfa_setup.qr_code}") - print(f"Secret Key: {mfa_setup.secret}") - print(f"Backup Codes: {mfa_setup.backup_codes}") - - # Verify MFA setup with code from authenticator app - await client.verify_mfa("123456") -``` - -### Disable MFA - -```python -async with AuthFrameworkClient('http://localhost:8080') as client: - await client.login('user@example.com', 'password') - - # Disable MFA (requires password and current MFA code) - await client.disable_mfa( - password="current_password", - code="123456" - ) -``` - -## OAuth 2.0 Integration - -### Authorization Code Flow - -```python -async with AuthFrameworkClient('http://localhost:8080') as client: - # Generate authorization URL - auth_url = client.get_oauth_authorize_url( - response_type="code", - client_id="your-app-id", - redirect_uri="https://yourapp.com/callback", - scope="read write", - state="random-state-value" - ) - - print(f"Redirect user to: {auth_url}") - - # After user authorizes and you receive the code: - token_response = await client.get_oauth_token( - grant_type="authorization_code", - code="authorization-code", - client_id="your-app-id", - client_secret="your-app-secret", - redirect_uri="https://yourapp.com/callback" - ) - - print(f"Access Token: {token_response.access_token}") -``` - -### Client Credentials Flow - -```python -async with AuthFrameworkClient('http://localhost:8080') as client: - # Server-to-server authentication - token_response = await client.get_oauth_token( - grant_type="client_credentials", - client_id="your-service-id", - client_secret="your-service-secret", - scope="admin" - ) - - # Use the access token for API calls - client._access_token = token_response.access_token -``` - -## Administrative Functions - -### User Management (Admin Only) - -```python -async with AuthFrameworkClient('http://localhost:8080') as client: - # Login as admin - await client.login('admin@example.com', 'admin_password') - - # List users with pagination - users = await client.list_users(page=1, limit=10, search="john") - - # Create new user - new_user = await client.create_user( - username="newuser@example.com", - password="secure_password", - email="newuser@example.com", - display_name="New User", - roles=["user"] - ) - - # Get user details - user = await client.get_user(new_user.user_id) - - # Delete user - await client.delete_user(user.user_id) - - # Get system statistics - stats = await client.get_system_stats() - print(f"Total Users: {stats.total_users}") - print(f"Active Sessions: {stats.active_sessions}") -``` - -## Health Monitoring - -### Basic Health Check - -```python -async with AuthFrameworkClient('http://localhost:8080') as client: - # Basic health status - health = await client.get_health() - print(f"Status: {health.status}") - print(f"Version: {health.version}") - - # Detailed health information - detailed_health = await client.get_detailed_health() - print(f"Database: {detailed_health.database}") - print(f"Redis: {detailed_health.redis}") - print(f"Uptime: {detailed_health.uptime}") -``` - -## Error Handling - -### Exception Types - -```python -from authframework.exceptions import ( - AuthFrameworkError, # Base exception - AuthenticationError, # 401 errors - AuthorizationError, # 403 errors - ValidationError, # 400 errors - NotFoundError, # 404 errors - RateLimitError, # 429 errors - ServerError, # 5xx errors - NetworkError, # Network issues - TimeoutError # Request timeouts -) - -async with AuthFrameworkClient('http://localhost:8080') as client: - try: - await client.login('invalid@email.com', 'wrong_password') - except AuthenticationError as e: - print(f"Login failed: {e.message}") - except ValidationError as e: - print(f"Invalid input: {e.message}") - print(f"Details: {e.details}") - except RateLimitError as e: - print(f"Rate limited. Retry after: {e.retry_after} seconds") - except AuthFrameworkError as e: - print(f"API error: {e.message} (Status: {e.status_code})") -``` - -### Retry Logic - -```python -# Client automatically retries on transient errors -client = AuthFrameworkClient( - 'http://localhost:8080', - retries=3, # Number of retry attempts - timeout=30.0 # Request timeout in seconds -) -``` - -## Configuration - -### Client Options - -```python -client = AuthFrameworkClient( - base_url='http://localhost:8080', - timeout=30.0, # Request timeout in seconds - retries=3, # Number of retry attempts - api_key='optional-key' # For API key authentication -) -``` - -### Environment Variables - -```bash -# You can set default values via environment variables -export AUTHFRAMEWORK_BASE_URL=http://localhost:8080 -export AUTHFRAMEWORK_TIMEOUT=30 -export AUTHFRAMEWORK_RETRIES=3 -export AUTHFRAMEWORK_API_KEY=your-api-key -``` - -```python -import os -from authframework import AuthFrameworkClient - -# Use environment variables as defaults -client = AuthFrameworkClient( - base_url=os.getenv('AUTHFRAMEWORK_BASE_URL', 'http://localhost:8080'), - timeout=float(os.getenv('AUTHFRAMEWORK_TIMEOUT', '30.0')), - retries=int(os.getenv('AUTHFRAMEWORK_RETRIES', '3')), - api_key=os.getenv('AUTHFRAMEWORK_API_KEY') -) -``` - -## Type Safety - -The SDK is fully typed with Pydantic models: - -```python -from authframework.models import UserInfo, LoginResponse - -async with AuthFrameworkClient('http://localhost:8080') as client: - # Return types are properly typed - response: LoginResponse = await client.login('user@example.com', 'password') - profile: UserInfo = await client.get_profile() - - # Access typed fields with IDE support - print(f"User ID: {profile.user_id}") - print(f"Email: {profile.email}") - print(f"Created: {profile.created_at}") -``` - -## Advanced Usage - -### Custom HTTP Client Configuration - -```python -import httpx -from authframework import AuthFrameworkClient - -# Create custom HTTP client -http_client = httpx.AsyncClient( - limits=httpx.Limits(max_connections=100), - verify=False, # Disable SSL verification (not recommended for production) - proxies={'http://': 'http://proxy:8080'} -) - -client = AuthFrameworkClient('http://localhost:8080') -client._client = http_client -``` - -### Concurrent Requests - -```python -import asyncio -from authframework import AuthFrameworkClient - -async def process_users(): - async with AuthFrameworkClient('http://localhost:8080') as client: - await client.login('admin@example.com', 'password') - - # Make concurrent requests - tasks = [ - client.get_user(f"user_{i}") - for i in range(10) - ] - - users = await asyncio.gather(*tasks, return_exceptions=True) - - for user in users: - if isinstance(user, Exception): - print(f"Error: {user}") - else: - print(f"User: {user.username}") -``` - -## Testing - -### Mock Client for Testing - -```python -from unittest.mock import AsyncMock -from authframework import AuthFrameworkClient - -# Mock the client for testing -async def test_user_login(): - client = AuthFrameworkClient('http://localhost:8080') - client.login = AsyncMock(return_value=MockLoginResponse()) - - # Test your code that uses the client - result = await client.login('test@example.com', 'password') - assert result.access_token == 'mock_token' -``` - -## Development - -### Running Tests - -```bash -cd sdks/python -pytest tests/ -``` - -### Building - -```bash -cd sdks/python -python -m build -``` - -### Installing in Development Mode - -```bash -cd sdks/python -pip install -e .[dev] -``` - -## API Reference - -### Models - -All request and response models are available in `authframework.models`: - -- `LoginRequest`, `LoginResponse` -- `UserInfo`, `UserProfile` -- `MFASetupResponse`, `MFAVerifyResponse` -- `OAuthTokenResponse` -- `HealthStatus`, `DetailedHealthStatus` -- `SystemStats` - -### Exceptions - -All custom exceptions are available in `authframework.exceptions`: - -- `AuthFrameworkError` - Base exception class -- `AuthenticationError` - Authentication failures (401) -- `AuthorizationError` - Authorization failures (403) -- `ValidationError` - Validation errors (400) -- `NotFoundError` - Resource not found (404) -- `RateLimitError` - Rate limiting (429) -- `ServerError` - Server errors (5xx) -- `NetworkError` - Network connectivity issues -- `TimeoutError` - Request timeouts - -## Support - -- **Documentation**: [AuthFramework Docs](https://authframework.dev/docs) -- **API Reference**: [API Documentation](https://authframework.dev/api) -- **Issues**: [GitHub Issues](https://github.com/authframework/authframework/issues) -- **Discussions**: [GitHub Discussions](https://github.com/authframework/authframework/discussions) - -## License - -This project is licensed under the MIT License - see the [LICENSE](../../LICENSE) file for details. diff --git a/sdks/python/__pycache__/test_import.cpython-313-pytest-8.4.1.pyc b/sdks/python/__pycache__/test_import.cpython-313-pytest-8.4.1.pyc deleted file mode 100644 index 299123a..0000000 Binary files a/sdks/python/__pycache__/test_import.cpython-313-pytest-8.4.1.pyc and /dev/null differ diff --git a/sdks/python/examples/__init__.py b/sdks/python/examples/__init__.py deleted file mode 100644 index 0d68a2d..0000000 --- a/sdks/python/examples/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -""" -AuthFramework Python SDK Examples - -This package contains example scripts demonstrating various features -of the AuthFramework Python SDK. -""" diff --git a/sdks/python/examples/__pycache__/example.cpython-313.pyc b/sdks/python/examples/__pycache__/example.cpython-313.pyc deleted file mode 100644 index d29753a..0000000 Binary files a/sdks/python/examples/__pycache__/example.cpython-313.pyc and /dev/null differ diff --git a/sdks/python/examples/example.py b/sdks/python/examples/example.py deleted file mode 100644 index ba177b5..0000000 --- a/sdks/python/examples/example.py +++ /dev/null @@ -1,170 +0,0 @@ -"""Example usage of the AuthFramework Python SDK.""" -# Copyright (c) 2025 AuthFramework Team. All rights reserved. - -import asyncio -import logging - -from authframework import AuthFrameworkClient -from authframework.exceptions import AuthenticationError, AuthFrameworkError - - -# Configure logging -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - - -async def main() -> None: - """Execute main example function.""" - # Initialize client - client = AuthFrameworkClient("http://localhost:8080") - - try: - # Example 1: Login and get profile - logger.info("=== Login Example ===") - - login_response = await client.auth.login("user@example.com", "password") - logger.info( - "Login successful! Token expires in %s seconds", - login_response.get("expires_in", "unknown"), - ) - - # Get user profile - profile = await client.user.get_profile() - logger.info( - "Welcome, %s! (ID: %s)", - profile.get("display_name", "User"), - profile.get("user_id", "unknown"), - ) - - # Example 2: Update profile - logger.info("=== Profile Update Example ===") - - await client.user.update_profile( - profile_data={ - "display_name": "Updated Name", - "preferences": {"theme": "dark", "notifications": True}, - }, - ) - logger.info("Profile updated successfully!") - - # Example 3: MFA setup (if not already enabled) - logger.info("=== MFA Setup Example ===") - - if not profile["mfa_enabled"]: - mfa_setup = await client.mfa.enable_totp() - logger.info("MFA Secret: %s", mfa_setup["secret"]) - logger.info("QR Code URL: %s", mfa_setup["qr_code"]) - logger.info("Scan the QR code with your authenticator app") - - # In a real app, you'd prompt the user for the code - # For demonstration, we'll simulate MFA verification - logger.info("MFA enabled successfully!") - else: - logger.info("MFA is already enabled for this user") - - # Example 4: OAuth authorization URL - logger.info("=== OAuth Example ===") - - auth_url = await client.oauth.authorize( - client_id="example-app", - redirect_uri="https://example.com/callback", - scope="read write", - state="random-state-123", - ) - logger.info("OAuth Authorization URL: %s", auth_url) - - # Example 5: Health check - logger.info("=== Health Check Example ===") - - health = await client.health_check() - logger.info("Service status: %s", health["status"]) - logger.info("Service version: %s", health["version"]) - - # Example 6: Admin functions (if user has admin role) - logger.info("=== Admin Functions Example ===") - - try: - stats = await client.admin.get_system_stats() - logger.info("Total users: %s", stats["total_users"]) - logger.info("Active sessions: %s", stats["active_sessions"]) - - # List users - users = await client.user.get_users(limit=5) - logger.info("Found %s users", len(users.get("users", []))) - - except AuthenticationError: - logger.info("User doesn't have admin permissions") - - # Example 7: Logout - logger.info("=== Logout Example ===") - - await client.auth.logout() - logger.info("Logged out successfully!") - - except AuthenticationError as e: - logger.exception("Authentication failed: %s", e.message) - except AuthFrameworkError as e: - logger.exception("API error: %s (Status: %s)", e.message, e.status_code) - except Exception: - logger.exception("Unexpected error occurred") - finally: - # Always close the client - await client.close() - - -async def context_manager_example() -> None: - """Use context manager example (recommended approach).""" - logger.info("=== Context Manager Example ===") - - try: - async with AuthFrameworkClient("http://localhost:8080") as client: - # Login - await client.auth.login("user@example.com", "password") - - # Get profile - profile = await client.user.get_profile() - logger.info("User: %s", profile["display_name"]) - - # Client is automatically closed when exiting the context - - except AuthFrameworkError: - logger.exception("API error occurred") - - -async def concurrent_requests_example() -> None: - """Demonstrate making concurrent requests.""" - logger.info("=== Concurrent Requests Example ===") - - async with AuthFrameworkClient("http://localhost:8080") as client: - # Login first - await client.auth.login("admin@example.com", "admin_password") - - # Make multiple concurrent requests - tasks = [ - client.health_check(), - client.user.get_profile(), - client.admin.get_system_stats(), - ] - - try: - results = await asyncio.gather(*tasks, return_exceptions=True) - - for i, result in enumerate(results): - if isinstance(result, Exception): - logger.error("Task %s failed: %s", i, result) - else: - logger.info("Task %s completed successfully", i) - - except Exception: - logger.exception("Concurrent requests failed") - - -if __name__ == "__main__": - # Run the main example - asyncio.run(main()) - - # Run context manager example - asyncio.run(context_manager_example()) - - # Run concurrent requests example - asyncio.run(concurrent_requests_example()) diff --git a/sdks/python/pyproject.toml b/sdks/python/pyproject.toml deleted file mode 100644 index e1671d8..0000000 --- a/sdks/python/pyproject.toml +++ /dev/null @@ -1,151 +0,0 @@ -[build-system] - build-backend = "hatchling.build" - requires = ["hatchling"] - -[project] - authors = [ - { name = "AuthFramework Team", email = "support@authframework.dev" }, - ] - classifiers = [ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.9", - "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: System :: Systems Administration :: Authentication/Directory", - ] - dependencies = [ - "httpx>=0.25.0", - "pydantic>=2.0.0", - "typing_extensions>=4.0.0; python_version<'3.11'", - ] - description = "Official Python SDK for AuthFramework REST API" - keywords = [ - "auth", - "authentication", - "authorization", - "jwt", - "mfa", - "oauth", - "rest-api", - "sdk", - ] - license = "MIT" - name = "authframework" - readme = "README.md" - requires-python = ">=3.9" - version = "1.0.0" - - [project.optional-dependencies] - dev = [ - "black>=23.0.0", - "flake8>=6.0.0", - "isort>=5.12.0", - "mypy>=1.5.0", - "pre-commit>=3.4.0", - "pytest-asyncio>=0.21.0", - "pytest-cov>=4.0.0", - "pytest>=7.0.0", - ] - docs = [ - "sphinx-autodoc-typehints>=1.24.0", - "sphinx-rtd-theme>=1.3.0", - "sphinx>=7.0.0", - ] - - [project.urls] - Documentation = "https://github.com/cires/AuthFramework/tree/main/sdks/python" - Homepage = "https://github.com/cires/AuthFramework" - Issues = "https://github.com/cires/AuthFramework/issues" - Repository = "https://github.com/cires/AuthFramework.git" - -[tool.hatch.build.targets.wheel] - packages = ["src/authframework"] - -[tool.hatch.build.targets.sdist] - include = ["/LICENSE", "/README.md", "/src", "/tests"] - -[tool.black] - extend-exclude = ''' -/( - # directories - \.eggs - | \.git - | \.hg - | \.mypy_cache - | \.tox - | \.venv - | build - | dist -)/ -''' - include = '\.pyi?$' - line-length = 100 - target-version = ['py39'] - -[tool.isort] - ensure_newline_before_comments = true - force_grid_wrap = 0 - include_trailing_comma = true - line_length = 100 - multi_line_output = 3 - profile = "black" - use_parentheses = true - -[tool.mypy] - check_untyped_defs = true - disallow_incomplete_defs = true - disallow_untyped_decorators = true - disallow_untyped_defs = true - no_implicit_optional = true - python_version = "3.9" - strict_equality = true - warn_no_return = true - warn_redundant_casts = true - warn_return_any = true - warn_unreachable = true - warn_unused_configs = true - warn_unused_ignores = true - -[tool.pytest.ini_options] - addopts = [ - "--cov-report=html", - "--cov-report=term-missing", - "--cov-report=xml", - "--cov=authframework", - "--strict-config", - "--strict-markers", - ] - asyncio_mode = "auto" - markers = [ - "integration: marks tests as integration tests", - "slow: marks tests as slow running", - "unit: marks tests as unit tests", - ] - python_classes = ["Test*"] - python_files = ["test_*.py"] - python_functions = ["test_*"] - testpaths = ["tests"] - -[tool.coverage.run] - omit = ["*/test_*", "*/tests/*"] - source = ["src/authframework"] - -[tool.coverage.report] - exclude_lines = [ - "@(abc\\.)?abstractmethod", - "class .*\\bProtocol\\):", - "def __repr__", - "if 0:", - "if __name__ == .__main__.:", - "if self.debug:", - "if settings.DEBUG", - "pragma: no cover", - "raise AssertionError", - "raise NotImplementedError", - ] diff --git a/sdks/python/src/authframework/__init__.py b/sdks/python/src/authframework/__init__.py deleted file mode 100644 index c7a9cd1..0000000 --- a/sdks/python/src/authframework/__init__.py +++ /dev/null @@ -1,39 +0,0 @@ -""" -AuthFramework Python SDK - -Official Python client library for the AuthFramework REST API. -Provides type-safe access to authentication, user management, -MFA, OAuth 2.0, and administrative features. -""" - -from .client import AuthFrameworkClient -from .exceptions import * -from .models import * - -__version__ = "1.0.0" -__author__ = "AuthFramework Team" -__email__ = "support@authframework.dev" - -__all__ = [ - "AuthFrameworkClient", - # Exceptions - "AuthFrameworkError", - "ValidationError", - "AuthenticationError", - "AuthorizationError", - "NotFoundError", - "ConflictError", - "RateLimitError", - "ServerError", - "NetworkError", - "TimeoutError", - # Models - "UserInfo", - "UserProfile", - "LoginResponse", - "TokenResponse", - "MFASetupResponse", - "SystemStats", - "HealthStatus", - "DetailedHealthStatus", -] diff --git a/sdks/python/src/authframework/__pycache__/__init__.cpython-313.pyc b/sdks/python/src/authframework/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index d8b2a81..0000000 Binary files a/sdks/python/src/authframework/__pycache__/__init__.cpython-313.pyc and /dev/null differ diff --git a/sdks/python/src/authframework/__pycache__/_admin.cpython-313.pyc b/sdks/python/src/authframework/__pycache__/_admin.cpython-313.pyc deleted file mode 100644 index 1cdd783..0000000 Binary files a/sdks/python/src/authframework/__pycache__/_admin.cpython-313.pyc and /dev/null differ diff --git a/sdks/python/src/authframework/__pycache__/_auth.cpython-313.pyc b/sdks/python/src/authframework/__pycache__/_auth.cpython-313.pyc deleted file mode 100644 index 8fe290e..0000000 Binary files a/sdks/python/src/authframework/__pycache__/_auth.cpython-313.pyc and /dev/null differ diff --git a/sdks/python/src/authframework/__pycache__/_base.cpython-313.pyc b/sdks/python/src/authframework/__pycache__/_base.cpython-313.pyc deleted file mode 100644 index c44c583..0000000 Binary files a/sdks/python/src/authframework/__pycache__/_base.cpython-313.pyc and /dev/null differ diff --git a/sdks/python/src/authframework/__pycache__/_mfa.cpython-313.pyc b/sdks/python/src/authframework/__pycache__/_mfa.cpython-313.pyc deleted file mode 100644 index b97b190..0000000 Binary files a/sdks/python/src/authframework/__pycache__/_mfa.cpython-313.pyc and /dev/null differ diff --git a/sdks/python/src/authframework/__pycache__/_oauth.cpython-313.pyc b/sdks/python/src/authframework/__pycache__/_oauth.cpython-313.pyc deleted file mode 100644 index a2a721e..0000000 Binary files a/sdks/python/src/authframework/__pycache__/_oauth.cpython-313.pyc and /dev/null differ diff --git a/sdks/python/src/authframework/__pycache__/_user.cpython-313.pyc b/sdks/python/src/authframework/__pycache__/_user.cpython-313.pyc deleted file mode 100644 index f1d678d..0000000 Binary files a/sdks/python/src/authframework/__pycache__/_user.cpython-313.pyc and /dev/null differ diff --git a/sdks/python/src/authframework/__pycache__/client.cpython-313.pyc b/sdks/python/src/authframework/__pycache__/client.cpython-313.pyc deleted file mode 100644 index 1b55ea2..0000000 Binary files a/sdks/python/src/authframework/__pycache__/client.cpython-313.pyc and /dev/null differ diff --git a/sdks/python/src/authframework/__pycache__/exceptions.cpython-313.pyc b/sdks/python/src/authframework/__pycache__/exceptions.cpython-313.pyc deleted file mode 100644 index 60f54fe..0000000 Binary files a/sdks/python/src/authframework/__pycache__/exceptions.cpython-313.pyc and /dev/null differ diff --git a/sdks/python/src/authframework/__pycache__/models.cpython-313.pyc b/sdks/python/src/authframework/__pycache__/models.cpython-313.pyc deleted file mode 100644 index 6c4b0be..0000000 Binary files a/sdks/python/src/authframework/__pycache__/models.cpython-313.pyc and /dev/null differ diff --git a/sdks/python/src/authframework/_admin.py b/sdks/python/src/authframework/_admin.py deleted file mode 100644 index 3fb3e15..0000000 --- a/sdks/python/src/authframework/_admin.py +++ /dev/null @@ -1,218 +0,0 @@ -"""Admin service for AuthFramework. - -Copyright (c) 2025 AuthFramework. All rights reserved. -""" - -from __future__ import annotations - -from typing import Any - -from ._base import BaseClient, RequestConfig - - -class AdminService: - """Service for administrative operations.""" - - def __init__(self, client: BaseClient) -> None: - """Initialize admin service. - - Args: - client: The base HTTP client - - """ - self._client = client - - async def get_system_stats(self) -> dict[str, Any]: - """Get system statistics. - - Returns: - System statistics and metrics. - - """ - return await self._client.make_request("GET", "/admin/stats") - - async def get_user_sessions(self, user_id: str) -> dict[str, Any]: - """Get active sessions for a user. - - Args: - user_id: User ID - - Returns: - List of active sessions. - - """ - return await self._client.make_request( - "GET", f"/admin/users/{user_id}/sessions" - ) - - async def revoke_user_sessions( - self, - user_id: str, - session_id: str | None = None, - ) -> dict[str, Any]: - """Revoke user sessions. - - Args: - user_id: User ID - session_id: Specific session ID to revoke (all if None) - - Returns: - Revocation confirmation. - - """ - endpoint = f"/admin/users/{user_id}/sessions" - if session_id: - endpoint += f"/{session_id}" - - return await self._client.make_request("DELETE", endpoint) - - async def get_audit_logs( - self, - limit: int = 100, - offset: int = 0, - user_id: str | None = None, - action: str | None = None, - start_date: str | None = None, - end_date: str | None = None, - ) -> dict[str, Any]: - """Get audit logs. - - Args: - limit: Maximum number of logs to return - offset: Number of logs to skip - user_id: Filter by user ID - action: Filter by action type - start_date: Filter by start date (ISO 8601) - end_date: Filter by end date (ISO 8601) - - Returns: - Audit logs and pagination info. - - """ - params: dict[str, Any] = {"limit": limit, "offset": offset} - - if user_id: - params["user_id"] = user_id - if action: - params["action"] = action - if start_date: - params["start_date"] = start_date - if end_date: - params["end_date"] = end_date - - config = RequestConfig(params=params) - return await self._client.make_request( - "GET", "/admin/audit-logs", config=config - ) - - async def create_role(self, role_data: dict[str, Any]) -> dict[str, Any]: - """Create a new role. - - Args: - role_data: Role creation data - - Returns: - Created role data. - - """ - config = RequestConfig(json_data=role_data) - return await self._client.make_request("POST", "/admin/roles", config=config) - - async def get_roles(self) -> dict[str, Any]: - """Get all roles. - - Returns: - List of all roles. - - """ - return await self._client.make_request("GET", "/admin/roles") - - async def update_role( - self, - role_id: str, - role_data: dict[str, Any], - ) -> dict[str, Any]: - """Update a role. - - Args: - role_id: Role ID - role_data: Updated role data - - Returns: - Updated role data. - - """ - config = RequestConfig(json_data=role_data) - return await self._client.make_request( - "PUT", f"/admin/roles/{role_id}", config=config - ) - - async def delete_role(self, role_id: str) -> dict[str, Any]: - """Delete a role. - - Args: - role_id: Role ID - - Returns: - Deletion confirmation. - - """ - return await self._client.make_request("DELETE", f"/admin/roles/{role_id}") - - async def assign_role(self, user_id: str, role_id: str) -> dict[str, Any]: - """Assign role to user. - - Args: - user_id: User ID - role_id: Role ID - - Returns: - Assignment confirmation. - - """ - data = {"role_id": role_id} - config = RequestConfig(json_data=data) - return await self._client.make_request( - "POST", f"/admin/users/{user_id}/roles", config=config - ) - - async def revoke_role(self, user_id: str, role_id: str) -> dict[str, Any]: - """Revoke role from user. - - Args: - user_id: User ID - role_id: Role ID - - Returns: - Revocation confirmation. - - """ - return await self._client.make_request( - "DELETE", f"/admin/users/{user_id}/roles/{role_id}" - ) - - async def get_permissions(self) -> dict[str, Any]: - """Get all permissions. - - Returns: - List of all permissions. - - """ - return await self._client.make_request("GET", "/admin/permissions") - - async def create_permission( - self, permission_data: dict[str, Any] - ) -> dict[str, Any]: - """Create a new permission. - - Args: - permission_data: Permission creation data - - Returns: - Created permission data. - - """ - config = RequestConfig(json_data=permission_data) - return await self._client.make_request( - "POST", "/admin/permissions", config=config - ) diff --git a/sdks/python/src/authframework/_auth.py b/sdks/python/src/authframework/_auth.py deleted file mode 100644 index 0666e64..0000000 --- a/sdks/python/src/authframework/_auth.py +++ /dev/null @@ -1,224 +0,0 @@ -"""Authentication service for AuthFramework. - -Copyright (c) 2025 AuthFramework. All rights reserved. -""" - -from __future__ import annotations - -from typing import Any - -from ._base import BaseClient, RequestConfig - -class AuthService: - """Service for authentication operations.""" - - def __init__(self, client: BaseClient) -> None: - """Initialize authentication service. - - Args: - client: The base HTTP client - - """ - self._client = client - - async def login( - self, - username: str, - password: str, - remember_me: bool = False, - ) -> dict[str, Any]: - """Authenticate a user with username and password. - - Args: - username: User's username or email - password: User's password - remember_me: Whether to extend session lifetime - - Returns: - Authentication response with tokens and user info. - - """ - data = { - "username": username, - "password": password, - "remember_me": remember_me, - } - - config = RequestConfig(json_data=data) - response = await self._client.make_request("POST", "/auth/login", config=config) - - # Store access token for future requests - if "access_token" in response: - self._client.set_access_token(response["access_token"]) - - return response - - async def logout(self) -> dict[str, Any]: - """Log out the current user and invalidate tokens. - - Returns: - Logout confirmation response. - - """ - response = await self._client.make_request("POST", "/auth/logout") - self._client.clear_access_token() - return response - - async def refresh_token(self, refresh_token: str) -> dict[str, Any]: - """Refresh access token using refresh token. - - Args: - refresh_token: The refresh token - - Returns: - Response with new access token. - - """ - data = {"refresh_token": refresh_token} - config = RequestConfig(json_data=data) - response = await self._client.make_request( - "POST", "/auth/refresh", config=config - ) - - # Update stored access token - if "access_token" in response: - self._client.set_access_token(response["access_token"]) - - return response - - async def register( - self, - username: str, - email: str, - password: str, - user_data: dict[str, Any] | None = None, - ) -> dict[str, Any]: - """Register a new user account. - - Args: - username: Desired username - email: User's email address - password: User's password - user_data: Additional user data - - Returns: - Registration response. - - """ - data = { - "username": username, - "email": email, - "password": password, - } - - if user_data: - data.update(user_data) - - config = RequestConfig(json_data=data) - return await self._client.make_request("POST", "/auth/register", config=config) - - async def verify_email(self, token: str) -> dict[str, Any]: - """Verify user's email address. - - Args: - token: Email verification token - - Returns: - Verification response. - - """ - data = {"token": token} - config = RequestConfig(json_data=data) - return await self._client.make_request( - "POST", "/auth/verify-email", config=config - ) - - async def reset_password_request(self, email: str) -> dict[str, Any]: - """Request password reset email. - - Args: - email: User's email address - - Returns: - Password reset request response. - - """ - data = {"email": email} - config = RequestConfig(json_data=data) - return await self._client.make_request( - "POST", "/auth/reset-password", config=config - ) - - async def reset_password_confirm( - self, - token: str, - new_password: str, - ) -> dict[str, Any]: - """Confirm password reset with new password. - - Args: - token: Password reset token - new_password: New password - - Returns: - Password reset confirmation response. - - """ - data = { - "token": token, - "new_password": new_password, - } - config = RequestConfig(json_data=data) - return await self._client.make_request( - "POST", "/auth/reset-password/confirm", config=config - ) - - async def change_password( - self, - current_password: str, - new_password: str, - ) -> dict[str, Any]: - """Change user's password. - - Args: - current_password: Current password - new_password: New password - - Returns: - Password change response. - - """ - data = { - "current_password": current_password, - "new_password": new_password, - } - config = RequestConfig(json_data=data) - return await self._client.make_request( - "POST", "/auth/change-password", config=config - ) - - async def validate_token(self, token: str | None = None) -> dict[str, Any]: - """Validate an access token. - - Args: - token: Token to validate (uses current token if None) - - Returns: - Token validation response. - - """ - if token: - # Temporarily use provided token - original_token = self._client.get_access_token() - self._client.set_access_token(token) - try: - response = await self._client.make_request("GET", "/auth/validate") - return response - finally: - # Restore original token - if original_token: - self._client.set_access_token(original_token) - else: - self._client.clear_access_token() - - return await self._client.make_request("GET", "/auth/validate") diff --git a/sdks/python/src/authframework/_base.py b/sdks/python/src/authframework/_base.py deleted file mode 100644 index 3aa1936..0000000 --- a/sdks/python/src/authframework/_base.py +++ /dev/null @@ -1,281 +0,0 @@ -"""Base HTTP client for AuthFramework API operations. - -Copyright (c) 2025 AuthFramework. All rights reserved. -""" - -from __future__ import annotations - -import asyncio -from typing import Any, NamedTuple -from urllib.parse import urljoin - -try: - from typing import Self -except ImportError: - from typing_extensions import Self - -import httpx - -from .exceptions import ( - AuthFrameworkError, - NetworkError, - TimeoutError as AuthTimeoutError, - create_error_from_response, - is_retryable_error, -) - -# HTTP Error Status Constants -HTTP_SUCCESS_THRESHOLD = 400 - - -class RequestConfig(NamedTuple): - """Configuration for HTTP requests.""" - - json_data: dict[str, Any] | None = None - form_data: dict[str, str | None] | None = None - params: dict[str, Any] | None = None - timeout: float | None = None - retries: int | None = None - - -class BaseClient: - """Base HTTP client for making API requests.""" - - def __init__( - self, - base_url: str, - timeout: float = 30.0, - retries: int = 3, - api_key: str | None = None, - ) -> None: - """Initialize base HTTP client. - - Args: - base_url: The base URL of the API - timeout: Request timeout in seconds - retries: Number of retry attempts for failed requests - api_key: Optional API key for authentication - - """ - self.base_url = base_url.rstrip("/") - self.timeout = timeout - self.retries = retries - self.api_key = api_key - self._access_token: str | None = None - - # Create HTTP client - headers = {"User-Agent": "AuthFramework-Python-SDK/1.0.0"} - if api_key: - headers["X-API-Key"] = api_key - - self._client = httpx.AsyncClient( - timeout=timeout, - headers=headers, - ) - - async def __aenter__(self) -> Self: - """Async context manager entry. - - Returns: - The client instance. - - """ - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: object, - ) -> None: - """Async context manager exit. - - Args: - exc_type: Exception type if an exception occurred. - exc_val: Exception value if an exception occurred. - exc_tb: Exception traceback if an exception occurred. - - """ - await self._client.aclose() - - async def close(self) -> None: - """Close the HTTP client.""" - await self._client.aclose() - - def set_access_token(self, token: str) -> None: - """Set the access token for authenticated requests.""" - self._access_token = token - - def clear_access_token(self) -> None: - """Clear the access token.""" - self._access_token = None - - def get_access_token(self) -> str | None: - """Get the current access token. - - Returns: - Current access token or None if not set. - - """ - return self._access_token - - async def make_request( - self, - method: str, - endpoint: str, - *, - config: RequestConfig | None = None, - ) -> dict[str, Any]: - """Make an HTTP request with retry logic. - - Args: - method: HTTP method (GET, POST, etc.) - endpoint: API endpoint path - config: Request configuration - - Returns: - Parsed JSON response data. - - Raises: - AuthFrameworkError: For authentication/authorization errors - NetworkError: For network-related errors - AuthTimeoutError: For timeout errors - - """ - if config is None: - config = RequestConfig() - - url = urljoin(self.base_url, endpoint.lstrip("/")) - request_timeout = config.timeout or self.timeout - request_retries = config.retries if config.retries is not None else self.retries - - headers: dict[str, str] = {} - if self._access_token: - headers["Authorization"] = f"Bearer {self._access_token}" - - for attempt in range(request_retries + 1): - response = await self._attempt_request( - method, - url, - headers, - config, - request_timeout, - ) - if response: - return response - - # Exponential backoff for retries - if attempt < request_retries: - await asyncio.sleep(min(2**attempt, 10)) - - retries_msg = "Max retries exceeded" - raise AuthFrameworkError(retries_msg) - - async def _attempt_request( - self, - method: str, - url: str, - headers: dict[str, str], - config: RequestConfig, - request_timeout: float, - ) -> dict[str, Any] | None: - """Attempt a single HTTP request. - - Returns: - Response data if successful, None if retryable error. - - Raises: - Various errors for non-retryable failures. - - """ - try: - response = await self._execute_request( - method, - url, - headers, - config, - request_timeout, - ) - - if response.status_code < HTTP_SUCCESS_THRESHOLD: - return response.json() - - # Handle error response - error_info = self._parse_error_response(response) - self._raise_api_error(response.status_code, error_info) - - except httpx.TimeoutException as e: - timeout_msg = "Request timeout" - raise AuthTimeoutError(timeout_msg) from e - except httpx.NetworkError as e: - network_msg = "Network error" - raise NetworkError(network_msg) from e - except AuthFrameworkError: - # Don't retry AuthFramework errors - raise - except Exception as e: - if not is_retryable_error(e): - failed_msg = "Request failed" - raise AuthFrameworkError(failed_msg) from e - return None - - return None - - async def _execute_request( - self, - method: str, - url: str, - headers: dict[str, str], - config: RequestConfig, - timeout: float, - ) -> httpx.Response: - """Execute the actual HTTP request. - - Returns: - The HTTP response. - - """ - if config.form_data: - headers["Content-Type"] = "application/x-www-form-urlencoded" - return await self._client.request( - method, - url, - data=config.form_data, - params=config.params, - headers=headers, - timeout=timeout, - ) - - return await self._client.request( - method, - url, - json=config.json_data, - params=config.params, - headers=headers, - timeout=timeout, - ) - - @staticmethod - def _parse_error_response(response: httpx.Response) -> dict[str, Any]: - """Parse error response from the API. - - Returns: - Parsed error data. - - """ - try: - error_data = response.json() - return error_data.get("error", {}) - except (ValueError, KeyError): - return {"message": response.text, "code": "UNKNOWN_ERROR"} - - @staticmethod - def _raise_api_error(status_code: int, error_info: dict[str, Any]) -> None: - """Raise appropriate error for API response. - - Args: - status_code: HTTP status code - error_info: Error information from response - - """ - raise create_error_from_response(status_code, error_info) diff --git a/sdks/python/src/authframework/_mfa.py b/sdks/python/src/authframework/_mfa.py deleted file mode 100644 index a897ff1..0000000 --- a/sdks/python/src/authframework/_mfa.py +++ /dev/null @@ -1,240 +0,0 @@ -"""Multi-factor authentication service for AuthFramework. - -Copyright (c) 2025 AuthFramework. All rights reserved. -""" - -from __future__ import annotations - -from typing import Any - -from ._base import BaseClient, RequestConfig - - -class MFAService: - """Service for multi-factor authentication operations.""" - - def __init__(self, client: BaseClient) -> None: - """Initialize MFA service. - - Args: - client: The base HTTP client - - """ - self._client = client - - async def enable_totp(self) -> dict[str, Any]: - """Enable TOTP authentication. - - Returns: - TOTP setup data including QR code. - - """ - return await self._client.make_request("POST", "/mfa/totp/enable") - - async def verify_totp_setup(self, code: str) -> dict[str, Any]: - """Verify TOTP setup with code. - - Args: - code: TOTP verification code - - Returns: - Verification response with backup codes. - - """ - data = {"code": code} - config = RequestConfig(json_data=data) - return await self._client.make_request( - "POST", "/mfa/totp/verify", config=config - ) - - async def disable_totp(self, password: str) -> dict[str, Any]: - """Disable TOTP authentication. - - Args: - password: User's password for confirmation - - Returns: - Disable confirmation. - - """ - data = {"password": password} - config = RequestConfig(json_data=data) - return await self._client.make_request( - "POST", "/mfa/totp/disable", config=config - ) - - async def verify_totp(self, code: str) -> dict[str, Any]: - """Verify TOTP code during login. - - Args: - code: TOTP code - - Returns: - Verification response. - - """ - data = {"code": code} - config = RequestConfig(json_data=data) - return await self._client.make_request( - "POST", "/mfa/totp/verify-login", config=config - ) - - async def enable_sms(self, phone_number: str) -> dict[str, Any]: - """Enable SMS authentication. - - Args: - phone_number: Phone number for SMS - - Returns: - SMS setup confirmation. - - """ - data = {"phone_number": phone_number} - config = RequestConfig(json_data=data) - return await self._client.make_request("POST", "/mfa/sms/enable", config=config) - - async def verify_sms_setup(self, code: str) -> dict[str, Any]: - """Verify SMS setup with code. - - Args: - code: SMS verification code - - Returns: - Verification response. - - """ - data = {"code": code} - config = RequestConfig(json_data=data) - return await self._client.make_request("POST", "/mfa/sms/verify", config=config) - - async def disable_sms(self, password: str) -> dict[str, Any]: - """Disable SMS authentication. - - Args: - password: User's password for confirmation - - Returns: - Disable confirmation. - - """ - data = {"password": password} - config = RequestConfig(json_data=data) - return await self._client.make_request( - "POST", "/mfa/sms/disable", config=config - ) - - async def send_sms(self) -> dict[str, Any]: - """Send SMS code during login. - - Returns: - SMS send confirmation. - - """ - return await self._client.make_request("POST", "/mfa/sms/send") - - async def verify_sms(self, code: str) -> dict[str, Any]: - """Verify SMS code during login. - - Args: - code: SMS code - - Returns: - Verification response. - - """ - data = {"code": code} - config = RequestConfig(json_data=data) - return await self._client.make_request( - "POST", "/mfa/sms/verify-login", config=config - ) - - async def enable_email(self) -> dict[str, Any]: - """Enable email authentication. - - Returns: - Email setup confirmation. - - """ - return await self._client.make_request("POST", "/mfa/email/enable") - - async def disable_email(self, password: str) -> dict[str, Any]: - """Disable email authentication. - - Args: - password: User's password for confirmation - - Returns: - Disable confirmation. - - """ - data = {"password": password} - config = RequestConfig(json_data=data) - return await self._client.make_request( - "POST", "/mfa/email/disable", config=config - ) - - async def send_email(self) -> dict[str, Any]: - """Send email code during login. - - Returns: - Email send confirmation. - - """ - return await self._client.make_request("POST", "/mfa/email/send") - - async def verify_email_mfa(self, code: str) -> dict[str, Any]: - """Verify email code during MFA login. - - Args: - code: Email code - - Returns: - Verification response. - - """ - data = {"code": code} - config = RequestConfig(json_data=data) - return await self._client.make_request( - "POST", "/mfa/email/verify-login", config=config - ) - - async def get_backup_codes(self) -> dict[str, Any]: - """Get MFA backup codes. - - Returns: - List of backup codes. - - """ - return await self._client.make_request("GET", "/mfa/backup-codes") - - async def regenerate_backup_codes(self, password: str) -> dict[str, Any]: - """Regenerate MFA backup codes. - - Args: - password: User's password for confirmation - - Returns: - New backup codes. - - """ - data = {"password": password} - config = RequestConfig(json_data=data) - return await self._client.make_request( - "POST", "/mfa/backup-codes/regenerate", config=config - ) - - async def verify_backup_code(self, code: str) -> dict[str, Any]: - """Verify backup code during login. - - Args: - code: Backup code - - Returns: - Verification response. - - """ - data = {"code": code} - config = RequestConfig(json_data=data) - return await self._client.make_request( - "POST", "/mfa/backup-code/verify", config=config - ) diff --git a/sdks/python/src/authframework/_oauth.py b/sdks/python/src/authframework/_oauth.py deleted file mode 100644 index 52e1d92..0000000 --- a/sdks/python/src/authframework/_oauth.py +++ /dev/null @@ -1,210 +0,0 @@ -"""OAuth service for AuthFramework. - -Copyright (c) 2025 AuthFramework. All rights reserved. -""" - -from __future__ import annotations - -from typing import Any - -from ._base import BaseClient, RequestConfig - - -class OAuthService: - """Service for OAuth operations.""" - - def __init__(self, client: BaseClient) -> None: - """Initialize OAuth service. - - Args: - client: The base HTTP client - - """ - self._client = client - - async def authorize( - self, - client_id: str, - redirect_uri: str, - scope: str, - state: str | None = None, - code_challenge: str | None = None, - code_challenge_method: str | None = None, - ) -> dict[str, Any]: - """Initialize OAuth authorization flow. - - Args: - client_id: OAuth client ID - redirect_uri: Redirect URI after authorization - scope: Requested scopes - state: Optional state parameter - code_challenge: PKCE code challenge - code_challenge_method: PKCE challenge method - - Returns: - Authorization response with redirect URL. - - """ - params: dict[str, Any] = { - "client_id": client_id, - "redirect_uri": redirect_uri, - "scope": scope, - "response_type": "code", - } - - if state: - params["state"] = state - if code_challenge: - params["code_challenge"] = code_challenge - if code_challenge_method: - params["code_challenge_method"] = code_challenge_method - - config = RequestConfig(params=params) - return await self._client.make_request("GET", "/oauth/authorize", config=config) - - async def token( - self, - grant_type: str, - client_id: str, - client_secret: str | None = None, - code: str | None = None, - redirect_uri: str | None = None, - refresh_token: str | None = None, - username: str | None = None, - password: str | None = None, - scope: str | None = None, - code_verifier: str | None = None, - ) -> dict[str, Any]: - """Exchange authorization code or credentials for tokens. - - Args: - grant_type: OAuth grant type - client_id: OAuth client ID - client_secret: OAuth client secret - code: Authorization code - redirect_uri: Redirect URI - refresh_token: Refresh token for refresh grant - username: Username for password grant - password: Password for password grant - scope: Requested scope - code_verifier: PKCE code verifier - - Returns: - Token response with access and refresh tokens. - - """ - data: dict[str, Any] = { - "grant_type": grant_type, - "client_id": client_id, - } - - if client_secret: - data["client_secret"] = client_secret - if code: - data["code"] = code - if redirect_uri: - data["redirect_uri"] = redirect_uri - if refresh_token: - data["refresh_token"] = refresh_token - if username: - data["username"] = username - if password: - data["password"] = password - if scope: - data["scope"] = scope - if code_verifier: - data["code_verifier"] = code_verifier - - config = RequestConfig(form_data=data) - response = await self._client.make_request( - "POST", "/oauth/token", config=config - ) - - # Store access token if received - if "access_token" in response: - self._client.set_access_token(response["access_token"]) - - return response - - async def revoke( - self, - token: str, - client_id: str, - client_secret: str | None = None, - token_type_hint: str | None = None, - ) -> dict[str, Any]: - """Revoke an OAuth token. - - Args: - token: Token to revoke - client_id: OAuth client ID - client_secret: OAuth client secret - token_type_hint: Hint about token type - - Returns: - Revocation confirmation. - - """ - data: dict[str, Any] = { - "token": token, - "client_id": client_id, - } - - if client_secret: - data["client_secret"] = client_secret - if token_type_hint: - data["token_type_hint"] = token_type_hint - - config = RequestConfig(form_data=data) - response = await self._client.make_request( - "POST", "/oauth/revoke", config=config - ) - - # Clear token if it was the current one - if token == self._client.get_access_token(): - self._client.clear_access_token() - - return response - - async def introspect( - self, - token: str, - client_id: str, - client_secret: str | None = None, - token_type_hint: str | None = None, - ) -> dict[str, Any]: - """Introspect an OAuth token. - - Args: - token: Token to introspect - client_id: OAuth client ID - client_secret: OAuth client secret - token_type_hint: Hint about token type - - Returns: - Token introspection response. - - """ - data: dict[str, Any] = { - "token": token, - "client_id": client_id, - } - - if client_secret: - data["client_secret"] = client_secret - if token_type_hint: - data["token_type_hint"] = token_type_hint - - config = RequestConfig(form_data=data) - return await self._client.make_request( - "POST", "/oauth/introspect", config=config - ) - - async def get_userinfo(self) -> dict[str, Any]: - """Get user information using current token. - - Returns: - User information from the token. - - """ - return await self._client.make_request("GET", "/oauth/userinfo") diff --git a/sdks/python/src/authframework/_user.py b/sdks/python/src/authframework/_user.py deleted file mode 100644 index 0ac7e00..0000000 --- a/sdks/python/src/authframework/_user.py +++ /dev/null @@ -1,152 +0,0 @@ -"""User management service for AuthFramework. - -Copyright (c) 2025 AuthFramework. All rights reserved. -""" - -from __future__ import annotations - -from typing import Any - -from ._base import BaseClient, RequestConfig - - -class UserService: - """Service for user management operations.""" - - def __init__(self, client: BaseClient) -> None: - """Initialize user service. - - Args: - client: The base HTTP client - - """ - self._client = client - - async def get_profile(self) -> dict[str, Any]: - """Get current user's profile. - - Returns: - User profile data. - - """ - return await self._client.make_request("GET", "/user/profile") - - async def update_profile(self, profile_data: dict[str, Any]) -> dict[str, Any]: - """Update current user's profile. - - Args: - profile_data: Updated profile information - - Returns: - Updated profile data. - - """ - config = RequestConfig(json_data=profile_data) - return await self._client.make_request("PUT", "/user/profile", config=config) - - async def get_users( - self, - limit: int = 50, - offset: int = 0, - search: str | None = None, - ) -> dict[str, Any]: - """Get list of users. - - Args: - limit: Maximum number of users to return - offset: Number of users to skip - search: Search query - - Returns: - List of users and pagination info. - - """ - params: dict[str, Any] = {"limit": limit, "offset": offset} - if search: - params["search"] = search - - config = RequestConfig(params=params) - return await self._client.make_request("GET", "/users", config=config) - - async def get_user(self, user_id: str) -> dict[str, Any]: - """Get specific user by ID. - - Args: - user_id: User ID - - Returns: - User data. - - """ - return await self._client.make_request("GET", f"/users/{user_id}") - - async def create_user(self, user_data: dict[str, Any]) -> dict[str, Any]: - """Create a new user (admin only). - - Args: - user_data: User creation data - - Returns: - Created user data. - - """ - config = RequestConfig(json_data=user_data) - return await self._client.make_request("POST", "/users", config=config) - - async def update_user( - self, - user_id: str, - user_data: dict[str, Any], - ) -> dict[str, Any]: - """Update user information (admin only). - - Args: - user_id: User ID - user_data: Updated user data - - Returns: - Updated user data. - - """ - config = RequestConfig(json_data=user_data) - return await self._client.make_request( - "PUT", - f"/users/{user_id}", - config=config, - ) - - async def delete_user(self, user_id: str) -> dict[str, Any]: - """Delete a user (admin only). - - Args: - user_id: User ID - - Returns: - Deletion confirmation. - - """ - return await self._client.make_request("DELETE", f"/users/{user_id}") - - async def deactivate_user(self, user_id: str) -> dict[str, Any]: - """Deactivate a user account. - - Args: - user_id: User ID - - Returns: - Deactivation confirmation. - - """ - return await self._client.make_request("POST", f"/users/{user_id}/deactivate") - - async def activate_user(self, user_id: str) -> dict[str, Any]: - """Activate a user account. - - Args: - user_id: User ID - - Returns: - Activation confirmation. - - """ - return await self._client.make_request("POST", f"/users/{user_id}/activate") diff --git a/sdks/python/src/authframework/client.py b/sdks/python/src/authframework/client.py deleted file mode 100644 index a4c936e..0000000 --- a/sdks/python/src/authframework/client.py +++ /dev/null @@ -1,125 +0,0 @@ -"""Simplified AuthFramework client using service composition. - -Copyright (c) 2025 AuthFramework. All rights reserved. -""" - -from __future__ import annotations - -from typing import Any - -try: - from typing import Self -except ImportError: - from typing_extensions import Self - -from ._admin import AdminService -from ._auth import AuthService -from ._base import BaseClient -from ._mfa import MFAService -from ._oauth import OAuthService -from ._user import UserService - - -class AuthFrameworkClient: - """Simplified AuthFramework client using service composition.""" - - def __init__( - self, - base_url: str, - *, - timeout: float = 30.0, - retries: int = 3, - api_key: str | None = None, - ) -> None: - """Initialize AuthFramework client. - - Args: - base_url: Base URL of the AuthFramework server - timeout: Request timeout in seconds - retries: Number of retry attempts for failed requests - api_key: Optional API key for authentication - - """ - self._client = BaseClient( - base_url=base_url, - timeout=timeout, - retries=retries, - api_key=api_key, - ) - - # Initialize service clients - self.auth = AuthService(self._client) - self.user = UserService(self._client) - self.mfa = MFAService(self._client) - self.oauth = OAuthService(self._client) - self.admin = AdminService(self._client) - - async def __aenter__(self) -> Self: - """Async context manager entry. - - Returns: - The client instance. - - """ - await self._client.__aenter__() - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: object, - ) -> None: - """Async context manager exit. - - Args: - exc_type: Exception type if an exception occurred - exc_val: Exception value if an exception occurred - exc_tb: Exception traceback if an exception occurred - - """ - await self._client.__aexit__(exc_type, exc_val, exc_tb) - - async def close(self) -> None: - """Close the client and clean up resources.""" - await self._client.close() - - def set_access_token(self, token: str) -> None: - """Set access token for authenticated requests. - - Args: - token: Access token to set - - """ - self._client.set_access_token(token) - - def clear_access_token(self) -> None: - """Clear the stored access token.""" - self._client.clear_access_token() - - def get_access_token(self) -> str | None: - """Get the current access token. - - Returns: - Current access token or None if not set. - - """ - return self._client.get_access_token() - - async def health_check(self) -> dict[str, Any]: - """Check server health status. - - Returns: - Server health information. - - """ - return await self._client.make_request("GET", "/health") - - async def get_server_info(self) -> dict[str, Any]: - """Get server information and capabilities. - - Returns: - Server information and supported features. - - """ - return await self._client.make_request("GET", "/info") diff --git a/sdks/python/src/authframework/client_new.py b/sdks/python/src/authframework/client_new.py deleted file mode 100644 index a4c936e..0000000 --- a/sdks/python/src/authframework/client_new.py +++ /dev/null @@ -1,125 +0,0 @@ -"""Simplified AuthFramework client using service composition. - -Copyright (c) 2025 AuthFramework. All rights reserved. -""" - -from __future__ import annotations - -from typing import Any - -try: - from typing import Self -except ImportError: - from typing_extensions import Self - -from ._admin import AdminService -from ._auth import AuthService -from ._base import BaseClient -from ._mfa import MFAService -from ._oauth import OAuthService -from ._user import UserService - - -class AuthFrameworkClient: - """Simplified AuthFramework client using service composition.""" - - def __init__( - self, - base_url: str, - *, - timeout: float = 30.0, - retries: int = 3, - api_key: str | None = None, - ) -> None: - """Initialize AuthFramework client. - - Args: - base_url: Base URL of the AuthFramework server - timeout: Request timeout in seconds - retries: Number of retry attempts for failed requests - api_key: Optional API key for authentication - - """ - self._client = BaseClient( - base_url=base_url, - timeout=timeout, - retries=retries, - api_key=api_key, - ) - - # Initialize service clients - self.auth = AuthService(self._client) - self.user = UserService(self._client) - self.mfa = MFAService(self._client) - self.oauth = OAuthService(self._client) - self.admin = AdminService(self._client) - - async def __aenter__(self) -> Self: - """Async context manager entry. - - Returns: - The client instance. - - """ - await self._client.__aenter__() - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: object, - ) -> None: - """Async context manager exit. - - Args: - exc_type: Exception type if an exception occurred - exc_val: Exception value if an exception occurred - exc_tb: Exception traceback if an exception occurred - - """ - await self._client.__aexit__(exc_type, exc_val, exc_tb) - - async def close(self) -> None: - """Close the client and clean up resources.""" - await self._client.close() - - def set_access_token(self, token: str) -> None: - """Set access token for authenticated requests. - - Args: - token: Access token to set - - """ - self._client.set_access_token(token) - - def clear_access_token(self) -> None: - """Clear the stored access token.""" - self._client.clear_access_token() - - def get_access_token(self) -> str | None: - """Get the current access token. - - Returns: - Current access token or None if not set. - - """ - return self._client.get_access_token() - - async def health_check(self) -> dict[str, Any]: - """Check server health status. - - Returns: - Server health information. - - """ - return await self._client.make_request("GET", "/health") - - async def get_server_info(self) -> dict[str, Any]: - """Get server information and capabilities. - - Returns: - Server information and supported features. - - """ - return await self._client.make_request("GET", "/info") diff --git a/sdks/python/src/authframework/client_old.py b/sdks/python/src/authframework/client_old.py deleted file mode 100644 index d1e1077..0000000 --- a/sdks/python/src/authframework/client_old.py +++ /dev/null @@ -1,315 +0,0 @@ -"""AuthFramework Python client. - -Copyright (c) 2025 AuthFramework. All rights reserved. -""" - - -from __future__ import annotations - -import asyncio -from typing import Any -from urllib.parse import urljoin, urlencode - -import httpx - -from .exceptions import ( - AuthFrameworkError, - NetworkError, - TimeoutError as AuthTimeoutError, - create_error_from_response, - is_retryable_error, -) -from .models import ( - DetailedHealthStatus, - HealthStatus, - LoginResponse, - MFASetupResponse, - MFAVerifyResponse, - OAuthTokenResponse, - SystemStats, - TokenResponse, - UserInfo, - UserProfile, -) - - -class AuthFrameworkClient: - """Main AuthFramework API client.""" - - def __init__( - self, - base_url: str, - timeout: float = 30.0, - retries: int = 3, - api_key: str | None = None, - ) -> None: - """Initialize the AuthFramework client. - - Args: - base_url: The base URL of the AuthFramework API - timeout: Request timeout in seconds - retries: Number of retry attempts for failed requests - api_key: Optional API key for authentication - - """ - self.base_url = base_url.rstrip("/") - self.timeout = timeout - self.retries = retries - self.api_key = api_key - self._access_token: str | None = None - - # Create HTTP client - headers = {"User-Agent": "AuthFramework-Python-SDK/1.0.0"} - if api_key: - headers["X-API-Key"] = api_key - - self._client = httpx.AsyncClient( - timeout=timeout, - headers=headers, - ) - - async def __aenter__(self) -> "AuthFrameworkClient": - """Async context manager entry. - - Returns: - The client instance. - """ - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: object, - ) -> None: - """Async context manager exit. - - Args: - exc_type: Exception type if an exception occurred. - exc_val: Exception value if an exception occurred. - exc_tb: Exception traceback if an exception occurred. - """ - await self._client.aclose() - - async def close(self) -> None: - """Close the HTTP client.""" - await self._client.aclose() - - def set_access_token(self, token: str) -> None: - """Set the access token for authenticated requests.""" - self._access_token = token - - def clear_access_token(self) -> None: - """Clear the access token.""" - self._access_token = None - - def get_access_token(self) -> str | None: - """Get the current access token.""" - return self._access_token - - async def _make_request( - self, - method: str, - endpoint: str, - json_data: dict[str, Any | None] | None = None, - form_data: dict[str, str | None] | None = None, - params: dict[str, Any | None] | None = None, - timeout: float | None = None, - retries: int | None = None, - ) -> dict[str, Any]: - """Make an HTTP request with retry logic.""" - url = urljoin(self.base_url, endpoint.lstrip("/")) - request_timeout = timeout or self.timeout - request_retries = retries if retries is not None else self.retries - - headers: dict[str, str] = {} - if self._access_token: - headers["Authorization"] = f"Bearer {self._access_token}" - - for attempt in range(request_retries + 1): - try: - if form_data: - headers["Content-Type"] = "application/x-www-form-urlencoded" - response = await self._client.request( - method, - url, - data=form_data, - params=params, - headers=headers, - timeout=request_timeout, - ) - else: - response = await self._client.request( - method, - url, - json=json_data, - params=params, - headers=headers, - timeout=request_timeout, - ) - - if response.status_code < 400: - return response.json() - - # Handle error response - try: - error_data = response.json() - error_info = error_data.get("error", {}) - except Exception: - error_info = {"message": response.text, "code": "UNKNOWN_ERROR"} - - raise create_error_from_response(response.status_code, error_info) - - except httpx.TimeoutException as e: - if attempt == request_retries: - timeout_msg = "Request timeout" - raise AuthTimeoutError(timeout_msg) from e - except httpx.NetworkError as e: - if attempt == request_retries: - network_msg = "Network error" - raise NetworkError(network_msg) from e - except AuthFrameworkError: - # Don't retry AuthFramework errors - raise - except Exception as e: - if attempt == request_retries or not is_retryable_error(e): - failed_msg = "Request failed" - raise AuthFrameworkError(failed_msg) from e - - # Exponential backoff - if attempt < request_retries: - await asyncio.sleep(min(2**attempt, 10)) - - retries_msg = "Max retries exceeded" - raise AuthFrameworkError(retries_msg) - - # Authentication methods - async def login( - self, - username: str, - password: str, - remember_me: bool = False, - ) -> LoginResponse: - """Authenticate user and return tokens. - - Returns: - LoginResponse containing access tokens and user info. - - """ - data = {"username": username, "password": password, "remember_me": remember_me} - response = await self._make_request("POST", "/auth/login", json_data=data) - - login_response = LoginResponse(**response["data"]) - self.set_access_token(login_response.access_token) - return login_response - - async def refresh_token(self, refresh_token: str) -> TokenResponse: - """Refresh access token. - - Returns: - TokenResponse containing new access token. - - """ - data = {"refresh_token": refresh_token} - response = await self._make_request("POST", "/auth/refresh", json_data=data) - - token_response = TokenResponse(**response["data"]) - self.set_access_token(token_response.access_token) - return token_response - - async def logout(self) -> None: - """Logout and invalidate session.""" - await self._make_request("POST", "/auth/logout") - self.clear_access_token() - - async def validate_token(self) -> UserInfo: - """Validate current token and get user info.""" - response = await self._make_request("POST", "/auth/validate") - return UserInfo(**response["data"]) - - # User management methods - async def get_profile(self) -> UserProfile: - """Get current user's profile.""" - response = await self._make_request("GET", "/users/profile") - return UserProfile(**response["data"]) - - async def update_profile(self, **kwargs: Any) -> UserProfile: - """Update current user's profile.""" - response = await self._make_request("PATCH", "/users/profile", json_data=kwargs) - return UserProfile(**response["data"]) - - async def change_password(self, current_password: str, new_password: str) -> None: - """Change current user's password.""" - data = {"current_password": current_password, "new_password": new_password} - await self._make_request("POST", "/users/password", json_data=data) - - # MFA methods - async def setup_mfa(self) -> MFASetupResponse: - """Set up MFA for current user. - - Returns: - MFASetupResponse containing setup information. - - """ - response = await self._make_request("POST", "/mfa/setup") - return MFASetupResponse(**response["data"]) - - async def verify_mfa(self, code: str) -> MFAVerifyResponse: - """Verify MFA code.""" - data = {"code": code} - response = await self._make_request("POST", "/mfa/verify", json_data=data) - return MFAVerifyResponse(**response["data"]) - - async def disable_mfa(self, password: str, code: str) -> None: - """Disable MFA for current user.""" - data = {"password": password, "code": code} - await self._make_request("POST", "/mfa/disable", json_data=data) - - # Health methods - async def get_health(self) -> HealthStatus: - """Get basic health status.""" - response = await self._make_request("GET", "/health") - return HealthStatus(**response["data"]) - - async def get_detailed_health(self) -> DetailedHealthStatus: - """Get detailed health status.""" - response = await self._make_request("GET", "/health/detailed") - return DetailedHealthStatus(**response["data"]) - - # OAuth methods - def get_oauth_authorize_url(self, **params: Any) -> str: - """Generate OAuth authorization URL.""" - query_string = urlencode({k: v for k, v in params.items() if v is not None}) - return f"{self.base_url}/oauth/authorize?{query_string}" - - async def get_oauth_token(self, **kwargs: Any) -> OAuthTokenResponse: - """Get OAuth token.""" - form_data: dict[str, str | None] = { - k: str(v) if v is not None else None for k, v in kwargs.items() - } - response = await self._make_request("POST", "/oauth/token", form_data=form_data) - return OAuthTokenResponse(**response) - - # Admin methods (require admin permissions) - async def list_users(self, **params: Any) -> dict[str, Any]: - """List users (admin only).""" - return await self._make_request("GET", "/admin/users", params=params) - - async def create_user(self, **kwargs: Any) -> UserInfo: - """Create a new user (admin only).""" - response = await self._make_request("POST", "/admin/users", json_data=kwargs) - return UserInfo(**response["data"]) - - async def get_user(self, user_id: str) -> UserInfo: - """Get user details (admin only).""" - response = await self._make_request("GET", f"/admin/users/{user_id}") - return UserInfo(**response["data"]) - - async def delete_user(self, user_id: str) -> None: - """Delete user (admin only).""" - await self._make_request("DELETE", f"/admin/users/{user_id}") - - async def get_system_stats(self) -> SystemStats: - """Get system statistics (admin only).""" - response = await self._make_request("GET", "/admin/stats") - return SystemStats(**response["data"]) diff --git a/sdks/python/src/authframework/exceptions.py b/sdks/python/src/authframework/exceptions.py deleted file mode 100644 index 7e98bda..0000000 --- a/sdks/python/src/authframework/exceptions.py +++ /dev/null @@ -1,155 +0,0 @@ -""" -Exception classes for the AuthFramework SDK. -""" - -from __future__ import annotations - -from typing import Any - - -class AuthFrameworkError(Exception): - """Base exception for AuthFramework SDK errors.""" - - def __init__( - self, - message: str, - code: str = "UNKNOWN_ERROR", - details: Any | None = None, - status_code: int | None = None, - ) -> None: - super().__init__(message) - self.message = message - self.code = code - self.details = details - self.status_code = status_code - - -class ValidationError(AuthFrameworkError): - """Raised when request validation fails.""" - - def __init__(self, message: str, details: Any | None = None) -> None: - super().__init__(message, "VALIDATION_ERROR", details, 400) - - -class AuthenticationError(AuthFrameworkError): - """Raised when authentication fails.""" - - def __init__( - self, message: str = "Authentication failed", details: Any | None = None - ) -> None: - super().__init__(message, "AUTHENTICATION_ERROR", details, 401) - - -class AuthorizationError(AuthFrameworkError): - """Raised when authorization fails.""" - - def __init__( - self, message: str = "Insufficient permissions", details: Any | None = None - ) -> None: - super().__init__(message, "AUTHORIZATION_ERROR", details, 403) - - -class NotFoundError(AuthFrameworkError): - """Raised when a resource is not found.""" - - def __init__( - self, message: str = "Resource not found", details: Any | None = None - ) -> None: - super().__init__(message, "NOT_FOUND_ERROR", details, 404) - - -class ConflictError(AuthFrameworkError): - """Raised when a resource conflict occurs.""" - - def __init__( - self, message: str = "Resource conflict", details: Any | None = None - ) -> None: - super().__init__(message, "CONFLICT_ERROR", details, 409) - - -class RateLimitError(AuthFrameworkError): - """Raised when rate limit is exceeded.""" - - def __init__( - self, - message: str = "Rate limit exceeded", - retry_after: int | None = None, - details: Any | None = None, - ) -> None: - super().__init__(message, "RATE_LIMIT_ERROR", details, 429) - self.retry_after = retry_after - - -class ServerError(AuthFrameworkError): - """Raised when a server error occurs.""" - - def __init__( - self, - message: str = "Internal server error", - details: Any | None = None, - status_code: int = 500, - ) -> None: - super().__init__(message, "SERVER_ERROR", details, status_code) - - -class NetworkError(AuthFrameworkError): - """Raised when a network error occurs.""" - - def __init__( - self, message: str = "Network error", details: Any | None = None - ) -> None: - super().__init__(message, "NETWORK_ERROR", details) - - -class TimeoutError(AuthFrameworkError): # noqa: A001 - """Raised when a request times out.""" - - def __init__( - self, message: str = "Request timeout", details: Any | None = None - ) -> None: - super().__init__(message, "TIMEOUT_ERROR", details) - - -def create_error_from_response( - status_code: int, - error_response: dict[str, Any | None] | None = None, - default_message: str | None = None, -) -> AuthFrameworkError: - """Create an appropriate error instance based on HTTP status code and error response.""" - message = (error_response or {}).get( - "message", default_message or "An error occurred" - ) - code = (error_response or {}).get("code", "UNKNOWN_ERROR") - details = (error_response or {}).get("details") - - # Ensure message and code are strings - message_str = str(message) if message is not None else "An error occurred" - code_str = str(code) if code is not None else "UNKNOWN_ERROR" - - if status_code == 400: - return ValidationError(message_str, details) - elif status_code == 401: - return AuthenticationError(message_str, details) - elif status_code == 403: - return AuthorizationError(message_str, details) - elif status_code == 404: - return NotFoundError(message_str, details) - elif status_code == 409: - return ConflictError(message_str, details) - elif status_code == 429: - return RateLimitError(message_str, details=details) - elif status_code >= 500: - return ServerError(message_str, details, status_code) - else: - return AuthFrameworkError(message_str, code_str, details, status_code) - - -def is_retryable_error(error: Exception) -> bool: - """Check if an error is retryable (network errors and 5xx server errors).""" - if isinstance(error, (NetworkError, TimeoutError)): - return True - - if isinstance(error, AuthFrameworkError) and error.status_code: - return error.status_code >= 500 - - return False diff --git a/sdks/python/src/authframework/models.py b/sdks/python/src/authframework/models.py deleted file mode 100644 index 4d22e20..0000000 --- a/sdks/python/src/authframework/models.py +++ /dev/null @@ -1,301 +0,0 @@ -"""Pydantic models for AuthFramework API responses and requests.""" - -from __future__ import annotations - -from datetime import datetime -from typing import Any - -from pydantic import BaseModel - -class ApiResponse(BaseModel): - """Base API response model.""" - - success: bool - timestamp: datetime - - -class ApiError(BaseModel): - """API error response model.""" - - success: bool = False - error: dict[str, Any] - timestamp: datetime - - -class Pagination(BaseModel): - """Pagination information.""" - - page: int - limit: int - total: int - has_next: bool - has_prev: bool - - -class PaginatedResponse(ApiResponse): - """Paginated API response.""" - - pagination: Pagination - - -# Authentication Models -class LoginRequest(BaseModel): - """Login request model.""" - - username: str - password: str - remember_me: bool | None = False - - -class UserInfo(BaseModel): - """User information model.""" - - id: str - username: str - email: str - roles: list[str] - mfa_enabled: bool - created_at: datetime - last_login: datetime | None = None - - -class LoginResponse(BaseModel): - """Login response model.""" - - access_token: str - refresh_token: str - token_type: str - expires_in: int - user: UserInfo - - -class RefreshTokenRequest(BaseModel): - """Refresh token request model.""" - - refresh_token: str - - -class TokenResponse(BaseModel): - """Token response model.""" - - access_token: str - token_type: str - expires_in: int - - -# User Models -class UserProfile(BaseModel): - """User profile model.""" - - id: str - user_id: str # Alias for id for backwards compatibility - username: str - email: str - display_name: str | None = None - first_name: str | None = None - last_name: str | None = None - phone: str | None = None - timezone: str | None = None - locale: str | None = None - mfa_enabled: bool - created_at: datetime - updated_at: datetime - - -class UpdateProfileRequest(BaseModel): - """Update profile request model.""" - - first_name: str | None = None - last_name: str | None = None - phone: str | None = None - timezone: str | None = None - locale: str | None = None - - -class ChangePasswordRequest(BaseModel): - """Change password request model.""" - - current_password: str - new_password: str - - -class CreateUserRequest(BaseModel): - """Create user request model.""" - - username: str - email: str - password: str - roles: list[str] | None = None - first_name: str | None = None - last_name: str | None = None - - -# MFA Models -class MFASetupResponse(BaseModel): - """MFA setup response model.""" - - secret: str - qr_code: str - backup_codes: list[str] - setup_uri: str - - -class MFAVerifyRequest(BaseModel): - """MFA verification request model.""" - - code: str - - -class MFAVerifyResponse(BaseModel): - """MFA verification response model.""" - - verified: bool - backup_codes: list[str] | None = None - - -class DisableMFARequest(BaseModel): - """Disable MFA request model.""" - - password: str - code: str - - -# OAuth Models -class OAuthTokenRequest(BaseModel): - """OAuth token request model.""" - - grant_type: str - code: str | None = None - redirect_uri: str | None = None - client_id: str | None = None - client_secret: str | None = None - refresh_token: str | None = None - scope: str | None = None - code_verifier: str | None = None - - -class OAuthTokenResponse(BaseModel): - """OAuth token response model.""" - - access_token: str - token_type: str - expires_in: int - refresh_token: str | None = None - scope: str | None = None - - -class RevokeTokenRequest(BaseModel): - """Revoke token request model.""" - - token: str - token_type_hint: str | None = None - client_id: str | None = None - client_secret: str | None = None - - -class IntrospectTokenRequest(BaseModel): - """Introspect token request model.""" - - token: str - token_type_hint: str | None = None - client_id: str | None = None - client_secret: str | None = None - - -class TokenIntrospectionResponse(BaseModel): - """Token introspection response model.""" - - active: bool - scope: str | None = None - client_id: str | None = None - username: str | None = None - token_type: str | None = None - exp: int | None = None - iat: int | None = None - sub: str | None = None - aud: str | None = None - iss: str | None = None - - -# Health Models -class HealthStatus(BaseModel): - """Health status model.""" - - status: str - version: str - timestamp: datetime - - -class ServiceHealth(BaseModel): - """Service health model.""" - - status: str - response_time: float - last_check: datetime - - -class DetailedHealthStatus(BaseModel): - """Detailed health status model.""" - - status: str - services: dict[str, ServiceHealth] - uptime: int - version: str - timestamp: datetime - - -# Admin Models -class SystemStats(BaseModel): - """System statistics model.""" - - total_users: int - active_sessions: int - users: dict[str, int] - sessions: dict[str, int] - oauth: dict[str, int] - system: dict[str, int | float] - timestamp: datetime - - -# OAuth Authorization Parameters -class OAuthAuthorizeParams(BaseModel): - """OAuth authorization parameters model.""" - - response_type: str - client_id: str - redirect_uri: str | None = None - scope: str | None = None - state: str | None = None - code_challenge: str | None = None - code_challenge_method: str | None = None - - -# Request Options -class RequestOptions(BaseModel): - """Request options model.""" - - timeout: float | None = None - retries: int | None = None - headers: dict[str, str] | None = None - - class Config: - """Pydantic configuration.""" - - extra = "allow" - - -# List Options -class ListOptions(BaseModel): - """List options model.""" - - page: int | None = 1 - limit: int | None = 20 - search: str | None = None - sort: str | None = None - order: str | None = None - - -class UserListOptions(ListOptions): - """User list options model.""" - - role: str | None = None diff --git a/sdks/python/src/authframework/py.typed b/sdks/python/src/authframework/py.typed deleted file mode 100644 index e69de29..0000000 diff --git a/sdks/python/tests/__init__.py b/sdks/python/tests/__init__.py deleted file mode 100644 index 66173ae..0000000 --- a/sdks/python/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Test package diff --git a/sdks/python/tests/__pycache__/__init__.cpython-313.pyc b/sdks/python/tests/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 4267e36..0000000 Binary files a/sdks/python/tests/__pycache__/__init__.cpython-313.pyc and /dev/null differ diff --git a/sdks/python/tests/__pycache__/conftest.cpython-313-pytest-8.4.1.pyc b/sdks/python/tests/__pycache__/conftest.cpython-313-pytest-8.4.1.pyc deleted file mode 100644 index 34898fe..0000000 Binary files a/sdks/python/tests/__pycache__/conftest.cpython-313-pytest-8.4.1.pyc and /dev/null differ diff --git a/sdks/python/tests/__pycache__/test_architecture.cpython-313-pytest-8.4.1.pyc b/sdks/python/tests/__pycache__/test_architecture.cpython-313-pytest-8.4.1.pyc deleted file mode 100644 index 548655f..0000000 Binary files a/sdks/python/tests/__pycache__/test_architecture.cpython-313-pytest-8.4.1.pyc and /dev/null differ diff --git a/sdks/python/tests/__pycache__/test_architecture_fixed.cpython-313-pytest-8.4.1.pyc b/sdks/python/tests/__pycache__/test_architecture_fixed.cpython-313-pytest-8.4.1.pyc deleted file mode 100644 index 417dc11..0000000 Binary files a/sdks/python/tests/__pycache__/test_architecture_fixed.cpython-313-pytest-8.4.1.pyc and /dev/null differ diff --git a/sdks/python/tests/conftest.py b/sdks/python/tests/conftest.py deleted file mode 100644 index 292f082..0000000 --- a/sdks/python/tests/conftest.py +++ /dev/null @@ -1,126 +0,0 @@ -"""Test configuration and common utilities. - -Copyright (c) 2025 AuthFramework. All rights reserved. -""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, Any - -import pytest -import respx -from authframework import AuthFrameworkClient - -if TYPE_CHECKING: - from collections.abc import AsyncGenerator, Generator - - -@pytest.fixture -def base_url() -> str: - """Return base URL for test server. - - Returns: - str: The base URL for testing. - - """ - return "https://api.authframework.test" - - -@pytest.fixture -def api_key() -> str: - """Return test API key. - - Returns: - str: The API key for testing. - - """ - return "test-api-key-12345" - - -@pytest.fixture -async def client( - base_url: str, - api_key: str, -) -> AsyncGenerator[AuthFrameworkClient, None]: - """Create test client. - - Yields: - AuthFrameworkClient: Configured test client. - - """ - async with AuthFrameworkClient( - base_url=base_url, - api_key=api_key, - timeout=5.0, - retries=1, - ) as client: - yield client - - -@pytest.fixture -def mock_responses() -> Generator[Any, None, None]: - """Mock HTTP responses. - - Yields: - The mock router for HTTP requests. - - """ - with respx.mock: - yield respx - - -@pytest.fixture -def sample_user_data() -> dict[str, Any]: - """Sample user data for testing. - - Returns: - dict[str, Any]: Sample user data. - - """ - return { - "id": "user123", - "username": "testuser", - "email": "test@example.com", - "first_name": "Test", - "last_name": "User", - "is_active": True, - "created_at": "2024-01-01T00:00:00Z", - } - - -@pytest.fixture -def sample_login_response() -> dict[str, Any]: - """Sample login response. - - Returns: - dict[str, Any]: Sample login response data. - - """ - return { - "access_token": "test-access-token", - "refresh_token": "test-refresh-token", - "token_type": "Bearer", - "expires_in": 3600, - "user": { - "id": "user123", - "username": "testuser", - "email": "test@example.com", - }, - } - - -@pytest.fixture -def sample_error_response() -> dict[str, Any]: - """Sample error response. - - Returns: - dict[str, Any]: Sample error response data. - - """ - return { - "error": { - "code": "INVALID_CREDENTIALS", - "message": "Invalid username or password", - "details": {"field": "password"}, - }, - } diff --git a/sdks/python/tests/test_architecture.py b/sdks/python/tests/test_architecture.py deleted file mode 100644 index f7cf82e..0000000 --- a/sdks/python/tests/test_architecture.py +++ /dev/null @@ -1,148 +0,0 @@ -"""Basic tests for AuthFramework client architecture. - -Copyright (c) 2025 AuthFramework. All rights reserved. -""" - -import asyncio -import logging -import os -from unittest.mock import MagicMock - -from authframework.client import AuthFrameworkClient, BaseClient - -# Create a module-level logger -logger = logging.getLogger(__name__) - - -def test_client_initialization() -> None: - """Test client initialization with proper service composition. - - Raises: - AssertionError: If any required service or base client is missing. - TypeError: If client internal structure is invalid. - - """ - client = AuthFrameworkClient("https://api.test.com") - - # Verify services are initialized - if not hasattr(client, "auth"): - msg = "Client missing 'auth' service" - raise AssertionError(msg) - if not hasattr(client, "user"): - msg = "Client missing 'user' service" - raise AssertionError(msg) - if not hasattr(client, "mfa"): - msg = "Client missing 'mfa' service" - raise AssertionError(msg) - if not hasattr(client, "oauth"): - msg = "Client missing 'oauth' service" - raise AssertionError(msg) - if not hasattr(client, "admin"): - msg = "Client missing 'admin' service" - raise AssertionError(msg) - - # Verify base client is created - if not hasattr(client, "_client"): - msg = "Client missing '_client' attribute" - raise TypeError(msg) - - -async def test_client_context_manager() -> None: - """Test client works as async context manager. - - Raises: - AssertionError: If client missing required services. - - """ - async with AuthFrameworkClient("https://api.test.com") as client: - if not hasattr(client, "auth"): - msg = "Client missing 'auth' service in context manager" - raise AssertionError(msg) - - -def test_token_management() -> None: - """Test client token management functionality. - - Raises: - AssertionError: If token management operations fail. - - """ - client = AuthFrameworkClient("https://api.test.com") - - # Test setting access token - # Use environment variable or fallback for testing - test_token = os.environ.get("TEST_TOKEN", "mock-test-token-123") - client.set_access_token(test_token) - - if client.get_access_token() != test_token: - msg = f"Expected token {test_token}, got {client.get_access_token()}" - raise AssertionError(msg) - - # Test clearing token - client.clear_access_token() - if client.get_access_token() is not None: - msg = f"Expected None after clearing token, got {client.get_access_token()}" - raise AssertionError(msg) - - -def test_service_separation() -> None: - """Test service separation and composition. - - Raises: - AssertionError: If service separation validation fails. - - """ - client = AuthFrameworkClient("https://api.test.com") - - # Create a mock base client for testing (unused but part of test setup) - _base_client = MagicMock(spec=BaseClient) - - # This test verifies the client has the expected internal structure - # In a real implementation, this would use a public API - if not hasattr(client, "_client"): - msg = "Client missing base client interface" - raise AssertionError(msg) - - -def test_basic_functionality() -> None: - """Run basic functionality tests.""" - logger.info("Running basic architecture tests...") - test_client_initialization() - logger.info("✓ Client initialization test passed") - test_token_management() - logger.info("✓ Token management test passed") - - test_service_separation() - logger.info("✓ Architecture separation test passed") - - -async def test_main() -> None: - """Run all architecture tests.""" - try: - await test_client_context_manager() - - logger.info("\n🎉 All architecture tests passed!") - logger.info("\nArchitecture validation summary:") - logger.info("✅ Main client has only 6 essential methods (well under 20 limit)") - logger.info("✅ Services are properly separated with no method overlap") - logger.info("✅ Each service has distinct responsibilities") - logger.info("✅ Token management works correctly") - logger.info("✅ Context manager pattern implemented") - logger.info("✅ Error handling is consistent and informative") - - logger.info("\nThe architectural issues have been resolved!") - - except Exception: - logger.exception("Architecture test failed") - raise - - -if __name__ == "__main__": - # Configure logging for test output - logging.basicConfig(level=logging.INFO, format="%(message)s") - - # Run basic tests - test_basic_functionality() - - # Run async tests - asyncio.run(test_main()) diff --git a/sdks/python/tests/test_architecture_fixed.py b/sdks/python/tests/test_architecture_fixed.py deleted file mode 100644 index f7cf82e..0000000 --- a/sdks/python/tests/test_architecture_fixed.py +++ /dev/null @@ -1,148 +0,0 @@ -"""Basic tests for AuthFramework client architecture. - -Copyright (c) 2025 AuthFramework. All rights reserved. -""" - -import asyncio -import logging -import os -from unittest.mock import MagicMock - -from authframework.client import AuthFrameworkClient, BaseClient - -# Create a module-level logger -logger = logging.getLogger(__name__) - - -def test_client_initialization() -> None: - """Test client initialization with proper service composition. - - Raises: - AssertionError: If any required service or base client is missing. - TypeError: If client internal structure is invalid. - - """ - client = AuthFrameworkClient("https://api.test.com") - - # Verify services are initialized - if not hasattr(client, "auth"): - msg = "Client missing 'auth' service" - raise AssertionError(msg) - if not hasattr(client, "user"): - msg = "Client missing 'user' service" - raise AssertionError(msg) - if not hasattr(client, "mfa"): - msg = "Client missing 'mfa' service" - raise AssertionError(msg) - if not hasattr(client, "oauth"): - msg = "Client missing 'oauth' service" - raise AssertionError(msg) - if not hasattr(client, "admin"): - msg = "Client missing 'admin' service" - raise AssertionError(msg) - - # Verify base client is created - if not hasattr(client, "_client"): - msg = "Client missing '_client' attribute" - raise TypeError(msg) - - -async def test_client_context_manager() -> None: - """Test client works as async context manager. - - Raises: - AssertionError: If client missing required services. - - """ - async with AuthFrameworkClient("https://api.test.com") as client: - if not hasattr(client, "auth"): - msg = "Client missing 'auth' service in context manager" - raise AssertionError(msg) - - -def test_token_management() -> None: - """Test client token management functionality. - - Raises: - AssertionError: If token management operations fail. - - """ - client = AuthFrameworkClient("https://api.test.com") - - # Test setting access token - # Use environment variable or fallback for testing - test_token = os.environ.get("TEST_TOKEN", "mock-test-token-123") - client.set_access_token(test_token) - - if client.get_access_token() != test_token: - msg = f"Expected token {test_token}, got {client.get_access_token()}" - raise AssertionError(msg) - - # Test clearing token - client.clear_access_token() - if client.get_access_token() is not None: - msg = f"Expected None after clearing token, got {client.get_access_token()}" - raise AssertionError(msg) - - -def test_service_separation() -> None: - """Test service separation and composition. - - Raises: - AssertionError: If service separation validation fails. - - """ - client = AuthFrameworkClient("https://api.test.com") - - # Create a mock base client for testing (unused but part of test setup) - _base_client = MagicMock(spec=BaseClient) - - # This test verifies the client has the expected internal structure - # In a real implementation, this would use a public API - if not hasattr(client, "_client"): - msg = "Client missing base client interface" - raise AssertionError(msg) - - -def test_basic_functionality() -> None: - """Run basic functionality tests.""" - logger.info("Running basic architecture tests...") - test_client_initialization() - logger.info("✓ Client initialization test passed") - test_token_management() - logger.info("✓ Token management test passed") - - test_service_separation() - logger.info("✓ Architecture separation test passed") - - -async def test_main() -> None: - """Run all architecture tests.""" - try: - await test_client_context_manager() - - logger.info("\n🎉 All architecture tests passed!") - logger.info("\nArchitecture validation summary:") - logger.info("✅ Main client has only 6 essential methods (well under 20 limit)") - logger.info("✅ Services are properly separated with no method overlap") - logger.info("✅ Each service has distinct responsibilities") - logger.info("✅ Token management works correctly") - logger.info("✅ Context manager pattern implemented") - logger.info("✅ Error handling is consistent and informative") - - logger.info("\nThe architectural issues have been resolved!") - - except Exception: - logger.exception("Architecture test failed") - raise - - -if __name__ == "__main__": - # Configure logging for test output - logging.basicConfig(level=logging.INFO, format="%(message)s") - - # Run basic tests - test_basic_functionality() - - # Run async tests - asyncio.run(test_main()) diff --git a/src/admin/mod.rs b/src/admin/mod.rs index cf9c0f4..fcba4ba 100644 --- a/src/admin/mod.rs +++ b/src/admin/mod.rs @@ -54,7 +54,7 @@ //! #[tokio::main] //! async fn main() -> Result<(), Box> { //! let settings = AuthFrameworkSettings::default(); -//! +//! //! // Create administrative interface //! let app_state = AppState::new(settings)?; //! // Note: AdminInterface would be created here in real usage diff --git a/src/api/server.rs b/src/api/server.rs index dd93214..d0c3937 100644 --- a/src/api/server.rs +++ b/src/api/server.rs @@ -88,14 +88,17 @@ impl ApiServer { .route("/oauth/token", post(oauth::token)) .route("/oauth/revoke", post(oauth::revoke_token)) .route("/oauth/introspect", post(oauth::introspect_token)) - .route("/oauth/clients/:client_id", get(oauth::get_client_info)) + .route("/oauth/clients/{client_id}", get(oauth::get_client_info)) // User management endpoints (authenticated) .route("/users/profile", get(users::get_profile)) .route("/users/profile", put(users::update_profile)) .route("/users/change-password", post(users::change_password)) .route("/users/sessions", get(users::get_sessions)) - .route("/users/sessions/:session_id", delete(users::revoke_session)) - .route("/users/:user_id/profile", get(users::get_user_profile)) + .route( + "/users/sessions/{session_id}", + delete(users::revoke_session), + ) + .route("/users/{user_id}/profile", get(users::get_user_profile)) // Multi-factor authentication endpoints (authenticated) .route("/mfa/setup", post(mfa::setup_mfa)) .route("/mfa/verify", post(mfa::verify_mfa)) @@ -109,9 +112,12 @@ impl ApiServer { // Administrative endpoints (admin only) .route("/admin/users", get(admin::list_users)) .route("/admin/users", post(admin::create_user)) - .route("/admin/users/:user_id/roles", put(admin::update_user_roles)) - .route("/admin/users/:user_id", delete(admin::delete_user)) - .route("/admin/users/:user_id/activate", put(admin::activate_user)) + .route( + "/admin/users/{user_id}/roles", + put(admin::update_user_roles), + ) + .route("/admin/users/{user_id}", delete(admin::delete_user)) + .route("/admin/users/{user_id}/activate", put(admin::activate_user)) .route("/admin/stats", get(admin::get_system_stats)) .route("/admin/audit-logs", get(admin::get_audit_logs)) // Set shared state diff --git a/src/auth_modular/mfa/mod.rs b/src/auth_modular/mfa/mod.rs index bac063c..30c6450 100644 --- a/src/auth_modular/mfa/mod.rs +++ b/src/auth_modular/mfa/mod.rs @@ -80,7 +80,7 @@ pub use sms_kit::SmsKitManager as SmsManager; /// // Generate challenge - example of typical usage patterns /// # // let challenge = mfa_manager.create_challenge("user123", MfaMethodType::Totp).await?; /// -/// // Verify user's response - example of typical usage patterns +/// // Verify user's response - example of typical usage patterns /// # // let verification = mfa_manager.verify_challenge(&challenge.id, "123456").await?; /// # Ok(()) /// # } diff --git a/src/authorization_enhanced/hierarchy_tests.rs b/src/authorization_enhanced/hierarchy_tests.rs index 1e0cf8f..9c7be1c 100644 --- a/src/authorization_enhanced/hierarchy_tests.rs +++ b/src/authorization_enhanced/hierarchy_tests.rs @@ -129,5 +129,3 @@ mod hierarchy_feature_tests { println!("✅ API integration with hierarchy features confirmed!"); } } - - diff --git a/src/authorization_enhanced/storage.rs b/src/authorization_enhanced/storage.rs index 2633a98..45c83be 100644 --- a/src/authorization_enhanced/storage.rs +++ b/src/authorization_enhanced/storage.rs @@ -747,5 +747,3 @@ mod tests { assert_eq!(retrieved.unwrap().action, "read"); } } - - diff --git a/src/builders.rs b/src/builders.rs index 592afd3..acb4ca9 100644 --- a/src/builders.rs +++ b/src/builders.rs @@ -17,7 +17,7 @@ //! .jwt_auth_from_env() //! .build().await?; //! -//! // Web app with database +//! // Web app with database //! let auth2 = AuthFramework::quick_start() //! .jwt_auth("your-secret-key") //! .with_postgres("postgresql://...") diff --git a/src/errors.rs b/src/errors.rs index 1b9989c..717e913 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -218,7 +218,7 @@ pub enum AuthError { #[error("TOML serialization error: {0}")] TomlSer(#[from] toml::ser::Error), - /// TOML deserialization errors + /// TOML deserialization errors #[error("TOML deserialization error: {0}")] TomlDe(#[from] toml::de::Error), diff --git a/src/integrations/axum.rs b/src/integrations/axum.rs index 698c705..b98a366 100644 --- a/src/integrations/axum.rs +++ b/src/integrations/axum.rs @@ -20,12 +20,12 @@ //! let _auth = AuthFramework::quick_start() //! .jwt_auth_from_env() //! .build().await?; -//! +//! //! // Create authentication middleware //! let _auth_middleware = RequireAuth::new() //! .with_roles(&["user", "admin"]) //! .with_permissions(&["read", "write"]); -//! +//! //! println!("Auth framework configured for Axum integration"); //! Ok(()) //! } @@ -52,7 +52,7 @@ //! // Configure middleware //! let _permission_middleware = RequirePermission::new("admin:read") //! .for_resource("user-profiles"); -//! +//! //! println!("Advanced auth configuration completed"); //! Ok(()) //! } diff --git a/src/lib.rs b/src/lib.rs index 0b19914..0574b8d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -225,9 +225,9 @@ pub mod permissions; pub mod profile_utils; pub mod providers; -// SDK generation for multiple languages -#[cfg(feature = "enhanced-rbac")] -pub mod sdks; +// SDK generation removed - use standalone SDK repositories: +// - Python SDK: https://github.com/ciresnave/authframework-python +// - JavaScript SDK: https://github.com/ciresnave/authframework-js pub mod server; pub mod storage; diff --git a/src/methods/passkey/mod.rs b/src/methods/passkey/mod.rs index 9145882..e61dee4 100644 --- a/src/methods/passkey/mod.rs +++ b/src/methods/passkey/mod.rs @@ -72,7 +72,7 @@ //! // PasskeyAuthMethod is now configured and ready for use //! // Registration and authentication flows would be implemented //! // based on the specific passkey implementation requirements -//! +//! //! Ok(()) //! } //! ``` @@ -229,7 +229,7 @@ impl UserValidationMethod for PasskeyUserValidation { /// // Register a new passkey - methods would be implemented based on actual API /// # // let challenge = passkey_method.start_registration("user123", "user@example.com").await?; /// -/// // Authenticate with passkey - methods would be implemented based on actual API +/// // Authenticate with passkey - methods would be implemented based on actual API /// # // let auth_challenge = passkey_method.start_authentication("user123").await?; /// # Ok(()) /// # } diff --git a/src/migrations/001_create_users_table.sql b/src/migrations/001_create_users_table.sql index 91546b1..5ac83ea 100644 --- a/src/migrations/001_create_users_table.sql +++ b/src/migrations/001_create_users_table.sql @@ -5,25 +5,25 @@ CREATE TABLE IF NOT EXISTS users ( username VARCHAR(100) UNIQUE, password_hash VARCHAR(255) NOT NULL, salt VARCHAR(255), - + -- Profile information first_name VARCHAR(100), last_name VARCHAR(100), phone VARCHAR(20), avatar_url TEXT, - + -- Account status is_active BOOLEAN DEFAULT true, is_verified BOOLEAN DEFAULT false, is_locked BOOLEAN DEFAULT false, - + -- Security metadata failed_login_attempts INTEGER DEFAULT 0, locked_until TIMESTAMP WITH TIME ZONE, last_login_at TIMESTAMP WITH TIME ZONE, last_login_ip INET, password_changed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - + -- Timestamps created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), @@ -59,7 +59,7 @@ CREATE TABLE IF NOT EXISTS user_profiles ( locale VARCHAR(10), preferences JSONB DEFAULT '{}', metadata JSONB DEFAULT '{}', - + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); diff --git a/src/migrations/002_create_roles_permissions.sql b/src/migrations/002_create_roles_permissions.sql index 3e489e6..743ac9b 100644 --- a/src/migrations/002_create_roles_permissions.sql +++ b/src/migrations/002_create_roles_permissions.sql @@ -5,11 +5,11 @@ CREATE TABLE IF NOT EXISTS roles ( description TEXT, parent_role_id UUID REFERENCES roles(id), is_system BOOLEAN DEFAULT false, - + -- Role metadata priority INTEGER DEFAULT 0, max_users INTEGER, - + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); @@ -21,11 +21,11 @@ CREATE TABLE IF NOT EXISTS permissions ( description TEXT, resource VARCHAR(100) NOT NULL, action VARCHAR(50) NOT NULL, - + -- Permission metadata is_system BOOLEAN DEFAULT false, conditions JSONB DEFAULT '{}', - + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); @@ -36,7 +36,7 @@ CREATE TABLE IF NOT EXISTS role_permissions ( permission_id UUID REFERENCES permissions(id) ON DELETE CASCADE, granted_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), granted_by UUID REFERENCES users(id), - + PRIMARY KEY (role_id, permission_id) ); @@ -47,7 +47,7 @@ CREATE TABLE IF NOT EXISTS user_roles ( assigned_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), assigned_by UUID REFERENCES users(id), expires_at TIMESTAMP WITH TIME ZONE, - + PRIMARY KEY (user_id, role_id) ); @@ -59,7 +59,7 @@ CREATE TABLE IF NOT EXISTS user_permissions ( granted_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), granted_by UUID REFERENCES users(id), expires_at TIMESTAMP WITH TIME ZONE, - + PRIMARY KEY (user_id, permission_id) ); @@ -68,7 +68,7 @@ CREATE TABLE IF NOT EXISTS permission_groups ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(100) UNIQUE NOT NULL, description TEXT, - + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); @@ -76,7 +76,7 @@ CREATE TABLE IF NOT EXISTS permission_groups ( CREATE TABLE IF NOT EXISTS permission_group_permissions ( group_id UUID REFERENCES permission_groups(id) ON DELETE CASCADE, permission_id UUID REFERENCES permissions(id) ON DELETE CASCADE, - + PRIMARY KEY (group_id, permission_id) ); @@ -129,7 +129,7 @@ INSERT INTO role_permissions (role_id, permission_id) SELECT admin_role.id, p.id FROM admin_role, permissions p WHERE p.is_system = true UNION ALL SELECT user_role.id, p.id FROM user_role, permissions p WHERE p.name = 'user.read' -UNION ALL -SELECT moderator_role.id, p.id FROM moderator_role, permissions p +UNION ALL +SELECT moderator_role.id, p.id FROM moderator_role, permissions p WHERE p.name IN ('user.read', 'user.write', 'audit.read') ON CONFLICT (role_id, permission_id) DO NOTHING; diff --git a/src/migrations/003_create_sessions_table.sql b/src/migrations/003_create_sessions_table.sql index 1eadeff..35f4c61 100644 --- a/src/migrations/003_create_sessions_table.sql +++ b/src/migrations/003_create_sessions_table.sql @@ -3,18 +3,18 @@ CREATE TABLE IF NOT EXISTS sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES users(id) ON DELETE CASCADE, token_hash VARCHAR(255) UNIQUE NOT NULL, - + -- Session metadata device_info JSONB DEFAULT '{}', user_agent TEXT, ip_address INET, location JSONB DEFAULT '{}', - + -- Security tracking risk_score DECIMAL(3,2) DEFAULT 0.00, security_flags JSONB DEFAULT '{}', is_suspicious BOOLEAN DEFAULT false, - + -- Session lifecycle is_active BOOLEAN DEFAULT true, last_activity TIMESTAMP WITH TIME ZONE DEFAULT NOW(), @@ -30,16 +30,16 @@ CREATE TABLE IF NOT EXISTS refresh_tokens ( user_id UUID REFERENCES users(id) ON DELETE CASCADE, session_id UUID REFERENCES sessions(id) ON DELETE CASCADE, token_hash VARCHAR(255) UNIQUE NOT NULL, - + -- Token metadata device_fingerprint VARCHAR(255), family VARCHAR(100), -- Token family for rotation - + -- Lifecycle is_revoked BOOLEAN DEFAULT false, revoked_at TIMESTAMP WITH TIME ZONE, revocation_reason VARCHAR(100), - + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), expires_at TIMESTAMP WITH TIME ZONE NOT NULL, last_used TIMESTAMP WITH TIME ZONE DEFAULT NOW() @@ -50,30 +50,30 @@ CREATE TABLE IF NOT EXISTS user_devices ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES users(id) ON DELETE CASCADE, device_fingerprint VARCHAR(255) NOT NULL, - + -- Device information device_name VARCHAR(255), device_type VARCHAR(50), -- mobile, desktop, tablet, etc. os VARCHAR(100), browser VARCHAR(100), - + -- Trust and security is_trusted BOOLEAN DEFAULT false, trust_score DECIMAL(3,2) DEFAULT 0.00, risk_indicators JSONB DEFAULT '{}', - + -- Usage tracking first_seen TIMESTAMP WITH TIME ZONE DEFAULT NOW(), last_seen TIMESTAMP WITH TIME ZONE DEFAULT NOW(), total_sessions INTEGER DEFAULT 0, - + -- Location tracking last_location JSONB DEFAULT '{}', locations_history JSONB DEFAULT '[]', - + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - + UNIQUE(user_id, device_fingerprint) ); @@ -82,20 +82,20 @@ CREATE TABLE IF NOT EXISTS session_events ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), session_id UUID REFERENCES sessions(id) ON DELETE CASCADE, user_id UUID REFERENCES users(id) ON DELETE CASCADE, - + event_type VARCHAR(50) NOT NULL, -- login, logout, refresh, suspicious_activity, etc. event_data JSONB DEFAULT '{}', - + -- Context ip_address INET, user_agent TEXT, - + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Create active sessions view for easy querying CREATE OR REPLACE VIEW active_sessions AS -SELECT +SELECT s.*, u.email, u.username, @@ -104,9 +104,9 @@ SELECT ud.is_trusted FROM sessions s JOIN users u ON s.user_id = u.id -LEFT JOIN user_devices ud ON s.user_id = ud.user_id +LEFT JOIN user_devices ud ON s.user_id = ud.user_id AND s.device_info->>'fingerprint' = ud.device_fingerprint -WHERE s.is_active = true +WHERE s.is_active = true AND s.expires_at > NOW() AND s.terminated_at IS NULL; @@ -141,27 +141,27 @@ DECLARE deleted_count INTEGER; BEGIN -- Mark expired sessions as inactive - UPDATE sessions - SET is_active = false, + UPDATE sessions + SET is_active = false, terminated_at = NOW(), termination_reason = 'expired' - WHERE expires_at < NOW() + WHERE expires_at < NOW() AND is_active = true AND terminated_at IS NULL; - + GET DIAGNOSTICS deleted_count = ROW_COUNT; - + -- Revoke associated refresh tokens - UPDATE refresh_tokens + UPDATE refresh_tokens SET is_revoked = true, revoked_at = NOW(), revocation_reason = 'session_expired' WHERE session_id IN ( - SELECT id FROM sessions - WHERE expires_at < NOW() + SELECT id FROM sessions + WHERE expires_at < NOW() AND terminated_at IS NOT NULL ) AND is_revoked = false; - + RETURN deleted_count; END; $$ LANGUAGE plpgsql; @@ -170,19 +170,19 @@ $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION update_device_last_seen() RETURNS TRIGGER AS $$ BEGIN - UPDATE user_devices + UPDATE user_devices SET last_seen = NEW.last_activity, total_sessions = total_sessions + CASE WHEN TG_OP = 'INSERT' THEN 1 ELSE 0 END - WHERE user_id = NEW.user_id + WHERE user_id = NEW.user_id AND device_fingerprint = NEW.device_info->>'fingerprint'; - + RETURN NEW; END; $$ LANGUAGE plpgsql; -- Trigger to update device tracking on session activity -CREATE TRIGGER update_device_on_session_activity +CREATE TRIGGER update_device_on_session_activity AFTER INSERT OR UPDATE OF last_activity ON sessions - FOR EACH ROW + FOR EACH ROW WHEN (NEW.device_info->>'fingerprint' IS NOT NULL) EXECUTE FUNCTION update_device_last_seen(); diff --git a/src/migrations/004_create_audit_logs.sql b/src/migrations/004_create_audit_logs.sql index fd0e455..6850fe8 100644 --- a/src/migrations/004_create_audit_logs.sql +++ b/src/migrations/004_create_audit_logs.sql @@ -6,21 +6,21 @@ CREATE TABLE IF NOT EXISTS audit_logs ( event_category VARCHAR(50) NOT NULL, resource VARCHAR(100), resource_id VARCHAR(255), - + -- Event details description TEXT, outcome VARCHAR(20) NOT NULL, -- success, failure, pending risk_level VARCHAR(20) DEFAULT 'low', -- low, medium, high, critical - + -- Context information ip_address INET, user_agent TEXT, session_id UUID, request_id VARCHAR(100), - + -- Additional metadata metadata JSONB DEFAULT '{}', - + -- Timestamp created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); @@ -35,7 +35,7 @@ CREATE INDEX IF NOT EXISTS idx_audit_logs_ip_address ON audit_logs(ip_address); -- Create audit statistics view CREATE OR REPLACE VIEW audit_statistics AS -SELECT +SELECT event_type, COUNT(*) as total_events, COUNT(CASE WHEN outcome = 'success' THEN 1 END) as successful_events, diff --git a/src/migrations/005_create_mfa_table.sql b/src/migrations/005_create_mfa_table.sql index 330339a..4636f01 100644 --- a/src/migrations/005_create_mfa_table.sql +++ b/src/migrations/005_create_mfa_table.sql @@ -3,27 +3,27 @@ CREATE TABLE IF NOT EXISTS user_mfa ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES users(id) ON DELETE CASCADE, method_type VARCHAR(50) NOT NULL, -- totp, sms, email, backup_codes - + -- Method-specific data secret_key VARCHAR(255), -- For TOTP phone_number VARCHAR(20), -- For SMS email_address VARCHAR(255), -- For email MFA backup_codes JSONB, -- For backup codes - + -- Configuration is_enabled BOOLEAN DEFAULT true, is_verified BOOLEAN DEFAULT false, recovery_questions JSONB DEFAULT '{}', - + -- Usage tracking last_used TIMESTAMP WITH TIME ZONE, use_count INTEGER DEFAULT 0, failure_count INTEGER DEFAULT 0, - + -- Timestamps created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - + UNIQUE(user_id, method_type) ); @@ -33,16 +33,16 @@ CREATE TABLE IF NOT EXISTS mfa_challenges ( user_id UUID REFERENCES users(id) ON DELETE CASCADE, challenge_code VARCHAR(10) NOT NULL, method_type VARCHAR(50) NOT NULL, - + -- Challenge state is_used BOOLEAN DEFAULT false, attempts INTEGER DEFAULT 0, max_attempts INTEGER DEFAULT 3, - + -- Context ip_address INET, user_agent TEXT, - + -- Lifecycle created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), expires_at TIMESTAMP WITH TIME ZONE NOT NULL, @@ -54,12 +54,12 @@ CREATE TABLE IF NOT EXISTS mfa_recovery_codes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES users(id) ON DELETE CASCADE, code_hash VARCHAR(255) NOT NULL, - + -- Usage tracking is_used BOOLEAN DEFAULT false, used_at TIMESTAMP WITH TIME ZONE, used_ip INET, - + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); @@ -85,9 +85,9 @@ RETURNS INTEGER AS $$ DECLARE deleted_count INTEGER; BEGIN - DELETE FROM mfa_challenges + DELETE FROM mfa_challenges WHERE expires_at < NOW(); - + GET DIAGNOSTICS deleted_count = ROW_COUNT; RETURN deleted_count; END; diff --git a/src/sdks/javascript.rs b/src/sdks/javascript.rs deleted file mode 100644 index bc90d07..0000000 --- a/src/sdks/javascript.rs +++ /dev/null @@ -1,1795 +0,0 @@ -//! JavaScript/TypeScript SDK enhancements for role-system integration -//! -//! This module provides enhanced SDK generation for JavaScript/TypeScript clients -//! with comprehensive RBAC functionality. - -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -/// Enhanced JavaScript/TypeScript SDK configuration -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EnhancedSdkConfig { - /// Base API URL - pub base_url: String, - /// API version - pub version: String, - /// Enable TypeScript types generation - pub typescript: bool, - /// Include role management functions - pub include_rbac: bool, - /// Include conditional permission helpers - pub include_conditional_permissions: bool, - /// Include audit logging - pub include_audit: bool, - /// Custom client name - pub client_name: String, -} - -impl Default for EnhancedSdkConfig { - fn default() -> Self { - Self { - base_url: "https://api.example.com".to_string(), - version: "v1".to_string(), - typescript: true, - include_rbac: true, - include_conditional_permissions: true, - include_audit: true, - client_name: "AuthFrameworkClient".to_string(), - } - } -} - -/// JavaScript/TypeScript SDK generator -pub struct JsSdkGenerator { - config: EnhancedSdkConfig, -} - -impl JsSdkGenerator { - /// Create new SDK generator - pub fn new(config: EnhancedSdkConfig) -> Self { - Self { config } - } - - /// Generate complete SDK - pub fn generate_sdk(&self) -> Result, Box> { - let mut files = HashMap::new(); - - // Generate base client - files.insert("client.ts".to_string(), self.generate_base_client()?); - - // Generate types - if self.config.typescript { - files.insert("types.ts".to_string(), self.generate_types()?); - } - - // Generate RBAC module - if self.config.include_rbac { - files.insert("rbac.ts".to_string(), self.generate_rbac_module()?); - } - - // Generate conditional permissions module - if self.config.include_conditional_permissions { - files.insert( - "conditional.ts".to_string(), - self.generate_conditional_module()?, - ); - } - - // Generate audit module - if self.config.include_audit { - files.insert("audit.ts".to_string(), self.generate_audit_module()?); - } - - // Generate utilities - files.insert("utils.ts".to_string(), self.generate_utils()?); - - // Generate main index file - files.insert("index.ts".to_string(), self.generate_index()?); - - // Generate package.json - files.insert("package.json".to_string(), self.generate_package_json()?); - - // Generate README - files.insert("README.md".to_string(), self.generate_readme()?); - - Ok(files) - } - - /// Generate base HTTP client - fn generate_base_client(&self) -> Result> { - let client_code = format!( - r#" -/** - * Enhanced {} with RBAC Support - * Generated by AuthFramework SDK Generator - */ - -export interface ClientConfig {{ - baseUrl: string; - apiKey?: string; - accessToken?: string; - timeout?: number; - retryAttempts?: number; -}} - -export interface ApiResponse {{ - success: boolean; - data?: T; - error?: string; - message?: string; -}} - -export class HttpError extends Error {{ - constructor( - public status: number, - public statusText: string, - public body?: any - ) {{ - super(`HTTP ${{status}}: ${{statusText}}`); - this.name = 'HttpError'; - }} -}} - -export class {} {{ - private baseUrl: string; - private headers: Record = {{}}; - private timeout: number; - private retryAttempts: number; - - constructor(config: ClientConfig) {{ - this.baseUrl = config.baseUrl.replace(/\/$/, ''); - this.timeout = config.timeout || 30000; - this.retryAttempts = config.retryAttempts || 3; - - if (config.apiKey) {{ - this.headers['X-API-Key'] = config.apiKey; - }} - - if (config.accessToken) {{ - this.headers['Authorization'] = `Bearer ${{config.accessToken}}`; - }} - - this.headers['Content-Type'] = 'application/json'; - }} - - /** - * Set authentication token - */ - setAccessToken(token: string): void {{ - this.headers['Authorization'] = `Bearer ${{token}}`; - }} - - /** - * Clear authentication token - */ - clearAccessToken(): void {{ - delete this.headers['Authorization']; - }} - - /** - * Set API key - */ - setApiKey(apiKey: string): void {{ - this.headers['X-API-Key'] = apiKey; - }} - - /** - * Make HTTP request with retry logic - */ - private async makeRequest( - method: string, - path: string, - body?: any, - headers?: Record - ): Promise> {{ - const url = `${{this.baseUrl}}${{path}}`; - const requestHeaders = {{ ...this.headers, ...headers }}; - - let lastError: Error | null = null; - - for (let attempt = 0; attempt <= this.retryAttempts; attempt++) {{ - try {{ - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), this.timeout); - - const response = await fetch(url, {{ - method, - headers: requestHeaders, - body: body ? JSON.stringify(body) : undefined, - signal: controller.signal, - }}); - - clearTimeout(timeoutId); - - const responseData = await response.json(); - - if (!response.ok) {{ - throw new HttpError(response.status, response.statusText, responseData); - }} - - return responseData as ApiResponse; - }} catch (error) {{ - lastError = error as Error; - - // Don't retry on client errors (4xx) - if (error instanceof HttpError && error.status >= 400 && error.status < 500) {{ - throw error; - }} - - // Wait before retry (exponential backoff) - if (attempt < this.retryAttempts) {{ - await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000)); - }} - }} - }} - - throw lastError || new Error('Request failed after all retries'); - }} - - /** - * GET request - */ - async get(path: string, headers?: Record): Promise> {{ - return this.makeRequest('GET', path, undefined, headers); - }} - - /** - * POST request - */ - async post(path: string, body?: any, headers?: Record): Promise> {{ - return this.makeRequest('POST', path, body, headers); - }} - - /** - * PUT request - */ - async put(path: string, body?: any, headers?: Record): Promise> {{ - return this.makeRequest('PUT', path, body, headers); - }} - - /** - * DELETE request - */ - async delete(path: string, headers?: Record): Promise> {{ - return this.makeRequest('DELETE', path, undefined, headers); - }} - - /** - * PATCH request - */ - async patch(path: string, body?: any, headers?: Record): Promise> {{ - return this.makeRequest('PATCH', path, body, headers); - }} -}} -"#, - self.config.client_name, self.config.client_name - ); - - Ok(client_code) - } - - /// Generate TypeScript types - fn generate_types(&self) -> Result> { - let types_code = r#" -/** - * TypeScript type definitions for AuthFramework RBAC - */ - -export interface Role { - id: string; - name: string; - description?: string; - parent_id?: string; - permissions: string[]; - created_at: string; - updated_at: string; -} - -export interface Permission { - id: string; - action: string; - resource: string; - conditions?: Record; - created_at: string; -} - -export interface UserRole { - role_id: string; - role_name: string; - assigned_at: string; - assigned_by?: string; - expires_at?: string; -} - -export interface UserRolesResponse { - user_id: string; - roles: UserRole[]; - effective_permissions: string[]; -} - -export interface CreateRoleRequest { - name: string; - description?: string; - parent_id?: string; - permissions?: string[]; -} - -export interface UpdateRoleRequest { - name?: string; - description?: string; - parent_id?: string; -} - -export interface AssignRoleRequest { - role_id: string; - expires_at?: string; - reason?: string; -} - -export interface BulkAssignRequest { - assignments: BulkAssignment[]; -} - -export interface BulkAssignment { - user_id: string; - role_id: string; - expires_at?: string; -} - -export interface ElevateRoleRequest { - target_role: string; - duration_minutes?: number; - justification: string; -} - -export interface PermissionCheckRequest { - action: string; - resource: string; - context?: Record; -} - -export interface PermissionCheckResponse { - granted: boolean; - reason: string; - required_roles: string[]; - missing_permissions: string[]; -} - -export interface AuditEntry { - id: string; - user_id?: string; - action: string; - resource?: string; - result: string; - context: Record; - timestamp: string; -} - -export interface AuditLogResponse { - entries: AuditEntry[]; - total_count: number; - page: number; - per_page: number; -} - -export interface AuditQuery { - user_id?: string; - action?: string; - resource?: string; - start_time?: string; - end_time?: string; - page?: number; - per_page?: number; -} - -export interface ConditionalContext { - time_of_day?: 'business_hours' | 'after_hours' | 'weekend' | 'holiday'; - day_type?: 'weekday' | 'weekend' | 'holiday'; - device_type?: 'desktop' | 'mobile' | 'tablet' | 'unknown'; - connection_type?: 'direct' | 'vpn' | 'proxy' | 'tor' | 'corporate' | 'unknown'; - security_level?: 'low' | 'medium' | 'high' | 'critical'; - risk_score?: number; - ip_address?: string; - user_agent?: string; - custom_attributes?: Record; -} - -export interface RoleListQuery { - page?: number; - per_page?: number; - parent_id?: string; - include_permissions?: boolean; -} - -export type TimeOfDay = 'business_hours' | 'after_hours' | 'weekend' | 'holiday'; -export type DayType = 'weekday' | 'weekend' | 'holiday'; -export type DeviceType = 'desktop' | 'mobile' | 'tablet' | 'unknown'; -export type ConnectionType = 'direct' | 'vpn' | 'proxy' | 'tor' | 'corporate' | 'unknown'; -export type SecurityLevel = 'low' | 'medium' | 'high' | 'critical'; -"#; - - Ok(types_code.to_string()) - } - - /// Generate RBAC module - fn generate_rbac_module(&self) -> Result> { - let rbac_code = format!( - r#" -/** - * RBAC (Role-Based Access Control) Module - * Provides comprehensive role and permission management - */ - -import {{ {} }} from './client'; -import {{ - Role, CreateRoleRequest, UpdateRoleRequest, UserRolesResponse, - AssignRoleRequest, BulkAssignRequest, ElevateRoleRequest, - PermissionCheckRequest, PermissionCheckResponse, RoleListQuery, - ApiResponse -}} from './types'; - -export class RbacManager {{ - constructor(private client: {}) {{}} - - // ============================================================================ - // ROLE MANAGEMENT - // ============================================================================ - - /** - * Create a new role - */ - async createRole(request: CreateRoleRequest): Promise> {{ - return this.client.post('/{}/rbac/roles', request); - }} - - /** - * Get role by ID - */ - async getRole(roleId: string): Promise> {{ - return this.client.get(`/{}/rbac/roles/${{roleId}}`); - }} - - /** - * List all roles with pagination - */ - async listRoles(query?: RoleListQuery): Promise> {{ - const params = new URLSearchParams(); - if (query?.page) params.append('page', query.page.toString()); - if (query?.per_page) params.append('per_page', query.per_page.toString()); - if (query?.parent_id) params.append('parent_id', query.parent_id); - if (query?.include_permissions) params.append('include_permissions', query.include_permissions.toString()); - - const queryString = params.toString(); - const path = queryString ? `/{}/rbac/roles?${{queryString}}` : '/{}/rbac/roles'; - - return this.client.get(path); - }} - - /** - * Update an existing role - */ - async updateRole(roleId: string, request: UpdateRoleRequest): Promise> {{ - return this.client.put(`/{}/rbac/roles/${{roleId}}`, request); - }} - - /** - * Delete a role - */ - async deleteRole(roleId: string): Promise> {{ - return this.client.delete(`/{}/rbac/roles/${{roleId}}`); - }} - - // ============================================================================ - // USER ROLE ASSIGNMENTS - // ============================================================================ - - /** - * Assign role to user - */ - async assignUserRole(userId: string, request: AssignRoleRequest): Promise> {{ - return this.client.post(`/{}/rbac/users/${{userId}}/roles`, request); - }} - - /** - * Revoke role from user - */ - async revokeUserRole(userId: string, roleId: string): Promise> {{ - return this.client.delete(`/{}/rbac/users/${{userId}}/roles/${{roleId}}`); - }} - - /** - * Get user's roles and effective permissions - */ - async getUserRoles(userId: string): Promise> {{ - return this.client.get(`/{}/rbac/users/${{userId}}/roles`); - }} - - /** - * Bulk assign roles to multiple users - */ - async bulkAssignRoles(request: BulkAssignRequest): Promise> {{ - return this.client.post('/{}/rbac/bulk/assign', request); - }} - - // ============================================================================ - // PERMISSION CHECKING - // ============================================================================ - - /** - * Check if current user has permission - */ - async checkPermission(request: PermissionCheckRequest): Promise> {{ - return this.client.post('/{}/rbac/check-permission', request); - }} - - /** - * Quick permission check (returns boolean) - */ - async hasPermission(action: string, resource: string, context?: Record): Promise {{ - try {{ - const response = await this.checkPermission({{ action, resource, context }}); - return response.data?.granted || false; - }} catch (error) {{ - console.error('Permission check failed:', error); - return false; - }} - }} - - /** - * Request role elevation - */ - async elevateRole(request: ElevateRoleRequest): Promise> {{ - return this.client.post('/{}/rbac/elevate', request); - }} - - // ============================================================================ - // CONVENIENCE METHODS - // ============================================================================ - - /** - * Check if user has any of the specified roles - */ - async userHasAnyRole(userId: string, roleNames: string[]): Promise {{ - try {{ - const response = await this.getUserRoles(userId); - if (!response.data) return false; - - const userRoleNames = response.data.roles.map(r => r.role_name); - return roleNames.some(role => userRoleNames.includes(role)); - }} catch (error) {{ - console.error('Role check failed:', error); - return false; - }} - }} - - /** - * Check if user has all of the specified roles - */ - async userHasAllRoles(userId: string, roleNames: string[]): Promise {{ - try {{ - const response = await this.getUserRoles(userId); - if (!response.data) return false; - - const userRoleNames = response.data.roles.map(r => r.role_name); - return roleNames.every(role => userRoleNames.includes(role)); - }} catch (error) {{ - console.error('Role check failed:', error); - return false; - }} - }} - - /** - * Get role hierarchy (parent-child relationships) - */ - async getRoleHierarchy(): Promise> {{ - try {{ - const response = await this.listRoles({{ include_permissions: false }}); - if (!response.data) return {{}}; - - const hierarchy: Record = {{}}; - - for (const role of response.data) {{ - if (role.parent_id) {{ - if (!hierarchy[role.parent_id]) {{ - hierarchy[role.parent_id] = []; - }} - hierarchy[role.parent_id].push(role.id); - }} - }} - - return hierarchy; - }} catch (error) {{ - console.error('Failed to build role hierarchy:', error); - return {{}}; - }} - }} - - /** - * Get all child roles for a given parent role - */ - async getChildRoles(parentRoleId: string): Promise {{ - try {{ - const response = await this.listRoles({{ parent_id: parentRoleId }}); - return response.data || []; - }} catch (error) {{ - console.error('Failed to get child roles:', error); - return []; - }} - }} -}} -"#, - self.config.client_name, - self.config.client_name, - self.config.version, - self.config.version, - self.config.version, - self.config.version, - self.config.version, - self.config.version, - self.config.version, - self.config.version, - self.config.version, - self.config.version, - self.config.version, - self.config.version - ); - - Ok(rbac_code) - } - - /// Generate conditional permissions module - fn generate_conditional_module(&self) -> Result> { - let conditional_code = r#" -/** - * Conditional Permissions Module - * Provides context-aware permission checking based on time, location, device, etc. - */ - -import { ConditionalContext, PermissionCheckRequest, PermissionCheckResponse, ApiResponse } from './types'; - -export class ConditionalPermissions { - constructor(private rbacManager: any) {} - - /** - * Build context from current browser/environment - */ - buildContext(): ConditionalContext { - const context: ConditionalContext = {}; - - // Time-based context - const now = new Date(); - const hour = now.getHours(); - const dayOfWeek = now.getDay(); // 0 = Sunday, 6 = Saturday - - if (hour >= 9 && hour < 17 && dayOfWeek >= 1 && dayOfWeek <= 5) { - context.time_of_day = 'business_hours'; - context.day_type = 'weekday'; - } else if (dayOfWeek === 0 || dayOfWeek === 6) { - context.time_of_day = 'weekend'; - context.day_type = 'weekend'; - } else { - context.time_of_day = 'after_hours'; - context.day_type = 'weekday'; - } - - // Device detection - if (navigator.userAgent) { - const ua = navigator.userAgent.toLowerCase(); - if (ua.includes('mobile') || ua.includes('android') || ua.includes('iphone')) { - context.device_type = 'mobile'; - } else if (ua.includes('tablet') || ua.includes('ipad')) { - context.device_type = 'tablet'; - } else { - context.device_type = 'desktop'; - } - - context.user_agent = navigator.userAgent; - } - - // Connection type detection (basic) - if (navigator.connection && (navigator.connection as any).effectiveType) { - const connection = navigator.connection as any; - if (connection.effectiveType === 'slow-2g' || connection.effectiveType === '2g') { - context.connection_type = 'proxy'; // Might indicate proxy/VPN - } else { - context.connection_type = 'direct'; - } - } - - return context; - } - - /** - * Check permission with current environmental context - */ - async checkPermissionWithContext( - action: string, - resource: string, - additionalContext?: Record - ): Promise> { - const context = this.buildContext(); - const contextMap: Record = {}; - - // Convert context to string map - if (context.time_of_day) contextMap.time_of_day = context.time_of_day; - if (context.day_type) contextMap.day_type = context.day_type; - if (context.device_type) contextMap.device_type = context.device_type; - if (context.connection_type) contextMap.connection_type = context.connection_type; - if (context.user_agent) contextMap.user_agent = context.user_agent; - - // Add any additional context - if (additionalContext) { - Object.assign(contextMap, additionalContext); - } - - return this.rbacManager.checkPermission({ - action, - resource, - context: contextMap - }); - } - - /** - * Check if current time is within business hours - */ - isBusinessHours(): boolean { - const now = new Date(); - const hour = now.getHours(); - const dayOfWeek = now.getDay(); - - return hour >= 9 && hour < 17 && dayOfWeek >= 1 && dayOfWeek <= 5; - } - - /** - * Check if current day is weekend - */ - isWeekend(): boolean { - const dayOfWeek = new Date().getDay(); - return dayOfWeek === 0 || dayOfWeek === 6; - } - - /** - * Get device type from user agent - */ - getDeviceType(): 'desktop' | 'mobile' | 'tablet' | 'unknown' { - if (!navigator.userAgent) return 'unknown'; - - const ua = navigator.userAgent.toLowerCase(); - if (ua.includes('mobile') || ua.includes('android') || ua.includes('iphone')) { - return 'mobile'; - } else if (ua.includes('tablet') || ua.includes('ipad')) { - return 'tablet'; - } else if (ua.includes('mozilla') || ua.includes('chrome') || ua.includes('firefox')) { - return 'desktop'; - } else { - return 'unknown'; - } - } - - /** - * Calculate basic risk score based on context - */ - calculateRiskScore(context?: ConditionalContext): number { - let risk = 0; - const ctx = context || this.buildContext(); - - // Time-based risk - if (ctx.time_of_day === 'after_hours') risk += 10; - if (ctx.day_type === 'weekend') risk += 5; - - // Device-based risk - if (ctx.device_type === 'mobile') risk += 5; - if (ctx.device_type === 'unknown') risk += 15; - - // Connection-based risk - if (ctx.connection_type === 'proxy') risk += 20; - if (ctx.connection_type === 'tor') risk += 50; - - return Math.min(risk, 100); // Cap at 100 - } - - /** - * Create context for high-security operations - */ - createHighSecurityContext(): Record { - const context = this.buildContext(); - const contextMap: Record = {}; - - // Convert to string map - if (context.time_of_day) contextMap.time_of_day = context.time_of_day; - if (context.day_type) contextMap.day_type = context.day_type; - if (context.device_type) contextMap.device_type = context.device_type; - if (context.connection_type) contextMap.connection_type = context.connection_type; - - // Add security requirements - contextMap.security_level = 'high'; - contextMap.require_business_hours = 'true'; - contextMap.block_vpn = 'true'; - contextMap.max_risk_score = '20'; - - return contextMap; - } -} -"#; - - Ok(conditional_code.to_string()) - } - - /// Generate audit module - fn generate_audit_module(&self) -> Result> { - let audit_code = format!( - r#" -/** - * Audit Logging Module - * Provides comprehensive audit trail functionality - */ - -import {{ {} }} from './client'; -import {{ AuditLogResponse, AuditQuery, AuditEntry, ApiResponse }} from './types'; - -export class AuditManager {{ - constructor(private client: {}) {{}} - - /** - * Get audit logs with filtering and pagination - */ - async getAuditLogs(query?: AuditQuery): Promise> {{ - const params = new URLSearchParams(); - - if (query?.user_id) params.append('user_id', query.user_id); - if (query?.action) params.append('action', query.action); - if (query?.resource) params.append('resource', query.resource); - if (query?.start_time) params.append('start_time', query.start_time); - if (query?.end_time) params.append('end_time', query.end_time); - if (query?.page) params.append('page', query.page.toString()); - if (query?.per_page) params.append('per_page', query.per_page.toString()); - - const queryString = params.toString(); - const path = queryString ? `/{}/rbac/audit?${{queryString}}` : '/{}/rbac/audit'; - - return this.client.get(path); - }} - - /** - * Get audit logs for specific user - */ - async getUserAuditLogs( - userId: string, - options?: {{ startTime?: string; endTime?: string; page?: number; perPage?: number }} - ): Promise> {{ - return this.getAuditLogs({{ - user_id: userId, - start_time: options?.startTime, - end_time: options?.endTime, - page: options?.page, - per_page: options?.perPage - }}); - }} - - /** - * Get audit logs for specific action - */ - async getActionAuditLogs( - action: string, - options?: {{ startTime?: string; endTime?: string; page?: number; perPage?: number }} - ): Promise> {{ - return this.getAuditLogs({{ - action, - start_time: options?.startTime, - end_time: options?.endTime, - page: options?.page, - per_page: options?.perPage - }}); - }} - - /** - * Get audit logs for specific resource - */ - async getResourceAuditLogs( - resource: string, - options?: {{ startTime?: string; endTime?: string; page?: number; perPage?: number }} - ): Promise> {{ - return this.getAuditLogs({{ - resource, - start_time: options?.startTime, - end_time: options?.endTime, - page: options?.page, - per_page: options?.perPage - }}); - }} - - /** - * Get audit logs for the last N hours - */ - async getRecentAuditLogs(hours: number = 24): Promise> {{ - const endTime = new Date(); - const startTime = new Date(endTime.getTime() - (hours * 60 * 60 * 1000)); - - return this.getAuditLogs({{ - start_time: startTime.toISOString(), - end_time: endTime.toISOString() - }}); - }} - - /** - * Export audit logs as CSV - */ - async exportAuditLogs(query?: AuditQuery): Promise {{ - try {{ - const response = await this.getAuditLogs(query); - if (!response.data?.entries) {{ - return ''; - }} - - const headers = ['Timestamp', 'User ID', 'Action', 'Resource', 'Result', 'Context']; - const rows = [headers.join(',')]; - - for (const entry of response.data.entries) {{ - const row = [ - entry.timestamp, - entry.user_id || '', - entry.action, - entry.resource || '', - entry.result, - JSON.stringify(entry.context).replace(/"/g, '""') // Escape quotes for CSV - ]; - rows.push(row.map(field => `"${{field}}"`).join(',')); - }} - - return rows.join('\\n'); - }} catch (error) {{ - console.error('Failed to export audit logs:', error); - throw error; - }} - }} - - /** - * Get audit statistics - */ - async getAuditStatistics( - startTime?: string, - endTime?: string - ): Promise<{{ - totalEntries: number; - actionCounts: Record; - userCounts: Record; - successRate: number; - }}> {{ - try {{ - const response = await this.getAuditLogs({{ - start_time: startTime, - end_time: endTime, - per_page: 1000 // Get a large sample - }}); - - if (!response.data?.entries) {{ - return {{ - totalEntries: 0, - actionCounts: {{}}, - userCounts: {{}}, - successRate: 0 - }}; - }} - - const entries = response.data.entries; - const actionCounts: Record = {{}}; - const userCounts: Record = {{}}; - let successCount = 0; - - for (const entry of entries) {{ - // Count actions - actionCounts[entry.action] = (actionCounts[entry.action] || 0) + 1; - - // Count users - if (entry.user_id) {{ - userCounts[entry.user_id] = (userCounts[entry.user_id] || 0) + 1; - }} - - // Count successes - if (entry.result === 'success' || entry.result === 'granted') {{ - successCount++; - }} - }} - - return {{ - totalEntries: response.data.total_count, - actionCounts, - userCounts, - successRate: entries.length > 0 ? (successCount / entries.length) * 100 : 0 - }}; - }} catch (error) {{ - console.error('Failed to get audit statistics:', error); - throw error; - }} - }} - - /** - * Monitor real-time audit events (if supported by backend) - */ - monitorAuditEvents( - callback: (entry: AuditEntry) => void, - filters?: {{ userId?: string; action?: string; resource?: string }} - ): () => void {{ - // This would typically use WebSockets or Server-Sent Events - // For now, we'll implement polling as a fallback - - let isMonitoring = true; - let lastTimestamp = new Date().toISOString(); - - const poll = async () => {{ - if (!isMonitoring) return; - - try {{ - const response = await this.getAuditLogs({{ - user_id: filters?.userId, - action: filters?.action, - resource: filters?.resource, - start_time: lastTimestamp, - per_page: 50 - }}); - - if (response.data?.entries) {{ - for (const entry of response.data.entries) {{ - callback(entry); - if (entry.timestamp > lastTimestamp) {{ - lastTimestamp = entry.timestamp; - }} - }} - }} - }} catch (error) {{ - console.error('Error polling audit events:', error); - }} - - // Poll every 5 seconds - setTimeout(poll, 5000); - }}; - - // Start polling - poll(); - - // Return cleanup function - return () => {{ - isMonitoring = false; - }}; - }} -}} -"#, - self.config.client_name, - self.config.client_name, - self.config.version, - self.config.version - ); - - Ok(audit_code) - } - - /// Generate utilities - fn generate_utils(&self) -> Result> { - let utils_code = r#" -/** - * Utility functions for AuthFramework SDK - */ - -/** - * Sleep for specified milliseconds - */ -export function sleep(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -/** - * Retry a function with exponential backoff - */ -export async function retryWithBackoff( - fn: () => Promise, - maxAttempts: number = 3, - baseDelay: number = 1000 -): Promise { - let lastError: Error | null = null; - - for (let attempt = 0; attempt < maxAttempts; attempt++) { - try { - return await fn(); - } catch (error) { - lastError = error as Error; - - if (attempt < maxAttempts - 1) { - const delay = baseDelay * Math.pow(2, attempt); - await sleep(delay); - } - } - } - - throw lastError || new Error('All retry attempts failed'); -} - -/** - * Debounce function calls - */ -export function debounce any>( - func: T, - wait: number -): (...args: Parameters) => void { - let timeout: NodeJS.Timeout | null = null; - - return function(this: any, ...args: Parameters) { - const later = () => { - timeout = null; - func.apply(this, args); - }; - - if (timeout) { - clearTimeout(timeout); - } - timeout = setTimeout(later, wait); - }; -} - -/** - * Throttle function calls - */ -export function throttle any>( - func: T, - limit: number -): (...args: Parameters) => void { - let inThrottle: boolean; - - return function(this: any, ...args: Parameters) { - if (!inThrottle) { - func.apply(this, args); - inThrottle = true; - setTimeout(() => inThrottle = false, limit); - } - }; -} - -/** - * Format date for API requests - */ -export function formatDateForApi(date: Date): string { - return date.toISOString(); -} - -/** - * Parse API date response - */ -export function parseApiDate(dateString: string): Date { - return new Date(dateString); -} - -/** - * Check if error is network-related - */ -export function isNetworkError(error: any): boolean { - return error instanceof TypeError && error.message.includes('fetch'); -} - -/** - * Check if error is authentication-related - */ -export function isAuthError(error: any): boolean { - return error.status === 401 || error.status === 403; -} - -/** - * Check if error is rate limit-related - */ -export function isRateLimitError(error: any): boolean { - return error.status === 429; -} - -/** - * Extract error message from API response - */ -export function extractErrorMessage(error: any): string { - if (error.body?.message) return error.body.message; - if (error.body?.error) return error.body.error; - if (error.message) return error.message; - return 'Unknown error occurred'; -} - -/** - * Validate email format - */ -export function isValidEmail(email: string): boolean { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return emailRegex.test(email); -} - -/** - * Generate UUID v4 - */ -export function generateUuid(): string { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - const r = Math.random() * 16 | 0; - const v = c === 'x' ? r : (r & 0x3 | 0x8); - return v.toString(16); - }); -} - -/** - * Deep merge objects - */ -export function deepMerge(target: T, source: Partial): T { - const output = { ...target }; - - if (isObject(target) && isObject(source)) { - Object.keys(source).forEach(key => { - if (isObject(source[key as keyof T])) { - if (!(key in target)) { - Object.assign(output, { [key]: source[key as keyof T] }); - } else { - (output as any)[key] = deepMerge(target[key as keyof T], source[key as keyof T]); - } - } else { - Object.assign(output, { [key]: source[key as keyof T] }); - } - }); - } - - return output; -} - -function isObject(item: any): boolean { - return item && typeof item === 'object' && !Array.isArray(item); -} - -/** - * Local storage wrapper with error handling - */ -export class SafeStorage { - static get(key: string): string | null { - try { - return localStorage.getItem(key); - } catch { - return null; - } - } - - static set(key: string, value: string): boolean { - try { - localStorage.setItem(key, value); - return true; - } catch { - return false; - } - } - - static remove(key: string): boolean { - try { - localStorage.removeItem(key); - return true; - } catch { - return false; - } - } - - static clear(): boolean { - try { - localStorage.clear(); - return true; - } catch { - return false; - } - } -} -"#; - - Ok(utils_code.to_string()) - } - - /// Generate main index file - fn generate_index(&self) -> Result> { - let index_code = format!( - r#" -/** - * AuthFramework Enhanced SDK with RBAC Support - * - * @version {version} - * @description Comprehensive authentication and authorization client library - */ - -// Core client -export {{ {client_name}, HttpError }} from './client'; -export type {{ ClientConfig, ApiResponse }} from './client'; - -// Type definitions -export * from './types'; - -// RBAC module -{rbac_export} - -// Conditional permissions -{conditional_export} - -// Audit logging -{audit_export} - -// Utilities -export * from './utils'; - -// Main SDK class that combines all functionality -export class AuthFrameworkSdk {{ - public readonly client: {client_name}; - {rbac_property} - {conditional_property} - {audit_property} - - constructor(config: ClientConfig) {{ - this.client = new {client_name}(config); - {rbac_init} - {conditional_init} - {audit_init} - }} - - /** - * Set authentication token for all requests - */ - setAccessToken(token: string): void {{ - this.client.setAccessToken(token); - }} - - /** - * Clear authentication token - */ - clearAccessToken(): void {{ - this.client.clearAccessToken(); - }} - - /** - * Set API key for authentication - */ - setApiKey(apiKey: string): void {{ - this.client.setApiKey(apiKey); - }} - - /** - * Check if user is authenticated - */ - isAuthenticated(): boolean {{ - // This would check if we have a valid token - // Implementation depends on your token storage strategy - return false; // Placeholder - }} - - /** - * Refresh authentication token - */ - async refreshToken(refreshToken: string): Promise<{{ accessToken: string; refreshToken: string }}> {{ - // Implementation would depend on your refresh token flow - throw new Error('Not implemented'); - }} -}} - -// Default export -export default AuthFrameworkSdk; - -// Create convenience function -export function createAuthFrameworkClient(config: ClientConfig): AuthFrameworkSdk {{ - return new AuthFrameworkSdk(config); -}} -"#, - version = self.config.version, - client_name = self.config.client_name, - rbac_export = if self.config.include_rbac { - "export { RbacManager } from './rbac';" - } else { - "" - }, - conditional_export = if self.config.include_conditional_permissions { - "export { ConditionalPermissions } from './conditional';" - } else { - "" - }, - audit_export = if self.config.include_audit { - "export { AuditManager } from './audit';" - } else { - "" - }, - rbac_property = if self.config.include_rbac { - "public readonly rbac: RbacManager;" - } else { - "" - }, - conditional_property = if self.config.include_conditional_permissions { - "public readonly conditional: ConditionalPermissions;" - } else { - "" - }, - audit_property = if self.config.include_audit { - "public readonly audit: AuditManager;" - } else { - "" - }, - rbac_init = if self.config.include_rbac { - "this.rbac = new RbacManager(this.client);" - } else { - "" - }, - conditional_init = if self.config.include_conditional_permissions { - "this.conditional = new ConditionalPermissions(this.rbac);" - } else { - "" - }, - audit_init = if self.config.include_audit { - "this.audit = new AuditManager(this.client);" - } else { - "" - }, - ); - - Ok(index_code) - } - - /// Generate package.json - fn generate_package_json(&self) -> Result> { - let package_json = r#"{{ - "name": "@authframework/client", - "version": "1.0.0", - "description": "Enhanced AuthFramework client library with RBAC support", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "scripts": {{ - "build": "tsc", - "build:watch": "tsc --watch", - "test": "jest", - "test:watch": "jest --watch", - "lint": "eslint src/**/*.ts", - "lint:fix": "eslint src/**/*.ts --fix", - "clean": "rimraf dist", - "prepublishOnly": "npm run clean && npm run build" - }}, - "keywords": [ - "auth", - "authentication", - "authorization", - "rbac", - "oauth", - "jwt", - "typescript" - ], - "author": "AuthFramework", - "license": "MIT", - "dependencies": {{ - "uuid": "^9.0.0" - }}, - "devDependencies": {{ - "@types/jest": "^29.0.0", - "@types/node": "^18.0.0", - "@types/uuid": "^9.0.0", - "@typescript-eslint/eslint-plugin": "^5.0.0", - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^8.0.0", - "jest": "^29.0.0", - "rimraf": "^3.0.0", - "ts-jest": "^29.0.0", - "typescript": "^4.9.0" - }}, - "files": [ - "dist/**/*" - ], - "repository": {{ - "type": "git", - "url": "https://github.com/authframework/client-js.git" - }}, - "bugs": {{ - "url": "https://github.com/authframework/client-js/issues" - }}, - "homepage": "https://github.com/authframework/client-js#readme" -}} -"# - .to_string(); - - Ok(package_json) - } - - /// Generate README - fn generate_readme(&self) -> Result> { - let readme = r#"# AuthFramework JavaScript/TypeScript SDK - -Enhanced client library for AuthFramework with comprehensive RBAC support powered by role-system v1.0. - -## Features - -- 🔐 **Complete Authentication** - JWT, OAuth, API keys -- 👥 **Enterprise RBAC** - Hierarchical roles and permissions -- ⏰ **Conditional Permissions** - Time, location, and context-aware access control -- 📊 **Audit Logging** - Comprehensive activity tracking -- 🚀 **TypeScript Support** - Full type safety and IntelliSense -- 🔄 **Automatic Retries** - Built-in error handling and retry logic -- 📱 **Cross-Platform** - Works in browsers and Node.js - -## Installation - -```bash -npm install @authframework/client -# or -yarn add @authframework/client -``` - -## Quick Start - -```typescript -import {{ AuthFrameworkSdk }} from '@authframework/client'; - -// Initialize the client -const auth = new AuthFrameworkSdk({{ - baseUrl: 'https://your-api.example.com', - accessToken: 'your-jwt-token' -}}); - -// Check permissions -const canEdit = await auth.rbac.hasPermission('edit', 'documents'); - -// Get user roles -const userRoles = await auth.rbac.getUserRoles('user123'); - -// Conditional permission check -const canAccessAfterHours = await auth.conditional.checkPermissionWithContext( - 'access', - 'admin-panel' -); -``` - -## RBAC (Role-Based Access Control) - -### Role Management - -```typescript -// Create a new role -await auth.rbac.createRole({{ - name: 'Editor', - description: 'Can edit content', - permissions: ['edit:documents', 'read:documents'] -}}); - -// Assign role to user -await auth.rbac.assignUserRole('user123', {{ - role_id: 'editor-role-id', - expires_at: '2024-12-31T23:59:59Z' -}}); - -// Check user permissions -const permissions = await auth.rbac.getUserRoles('user123'); -console.log(permissions.effective_permissions); -``` - -### Permission Checking - -```typescript -// Simple permission check -const hasPermission = await auth.rbac.hasPermission('delete', 'documents'); - -// Detailed permission check with context -const result = await auth.rbac.checkPermission({{ - action: 'access', - resource: 'admin-panel', - context: {{ - ip_address: '192.168.1.100', - time_of_day: 'business_hours' - }} -}}); - -console.log(result.data?.granted); // boolean -console.log(result.data?.reason); // explanation -``` - -## Conditional Permissions - -Context-aware permissions based on time, location, device, and custom attributes. - -```typescript -// Check permission with environmental context -const canAccess = await auth.conditional.checkPermissionWithContext( - 'access', - 'sensitive-data' -); - -// Build custom context -const context = auth.conditional.buildContext(); -console.log(context.time_of_day); // 'business_hours' | 'after_hours' | 'weekend' -console.log(context.device_type); // 'desktop' | 'mobile' | 'tablet' -console.log(context.connection_type); // 'direct' | 'vpn' | 'proxy' - -// High-security context for sensitive operations -const highSecContext = auth.conditional.createHighSecurityContext(); -const result = await auth.rbac.checkPermission({{ - action: 'delete', - resource: 'user-accounts', - context: highSecContext -}}); -``` - -## Audit Logging - -Comprehensive activity tracking and analysis. - -```typescript -// Get audit logs -const logs = await auth.audit.getAuditLogs({{ - user_id: 'user123', - start_time: '2024-01-01T00:00:00Z', - end_time: '2024-01-31T23:59:59Z', - page: 1, - per_page: 50 -}}); - -// Get recent activity -const recent = await auth.audit.getRecentAuditLogs(24); // Last 24 hours - -// Export audit data -const csvData = await auth.audit.exportAuditLogs({{ - action: 'login', - start_time: '2024-01-01T00:00:00Z' -}}); - -// Real-time monitoring -const stopMonitoring = auth.audit.monitorAuditEvents( - (entry) => console.log('New audit entry:', entry), - {{ action: 'permission_check' }} -); - -// Stop monitoring when done -stopMonitoring(); -``` - -## Role Elevation - -Temporary privilege escalation for administrative tasks. - -```typescript -// Request elevated permissions -await auth.rbac.elevateRole({{ - target_role: 'admin', - duration_minutes: 30, - justification: 'Emergency system maintenance' -}}); - -// Check if user has elevated permissions -const hasElevated = await auth.rbac.hasPermission('elevated', 'admin'); -``` - -## Advanced Usage - -### Bulk Operations - -```typescript -// Bulk role assignment -await auth.rbac.bulkAssignRoles({{ - assignments: [ - {{ user_id: 'user1', role_id: 'editor' }}, - {{ user_id: 'user2', role_id: 'viewer' }}, - {{ user_id: 'user3', role_id: 'admin', expires_at: '2024-12-31T23:59:59Z' }} - ] -}}); -``` - -### Error Handling - -```typescript -import {{ HttpError, isAuthError, isRateLimitError }} from '@authframework/client'; - -try {{ - await auth.rbac.createRole(roleData); -}} catch (error) {{ - if (error instanceof HttpError) {{ - if (isAuthError(error)) {{ - // Handle authentication error - console.log('Authentication required'); - }} else if (isRateLimitError(error)) {{ - // Handle rate limit - console.log('Rate limit exceeded'); - }} else {{ - console.log('HTTP error:', error.status, error.statusText); - }} - }} -}} -``` - -### Custom Configuration - -```typescript -const auth = new AuthFrameworkSdk({{ - baseUrl: 'https://api.example.com', - accessToken: 'your-token', - timeout: 30000, // 30 second timeout - retryAttempts: 3, // Retry failed requests 3 times -}}); - -// Dynamic token updates -auth.setAccessToken('new-token'); - -// API key authentication -auth.setApiKey('your-api-key'); -``` - -## Type Safety - -Full TypeScript support with comprehensive type definitions: - -```typescript -import type {{ - Role, - Permission, - UserRolesResponse, - PermissionCheckResponse, - ConditionalContext -}} from '@authframework/client'; - -const role: Role = {{ - id: 'role-123', - name: 'Editor', - description: 'Content editor role', - parent_id: 'base-user', - permissions: ['edit:content', 'read:content'], - created_at: '2024-01-01T00:00:00Z', - updated_at: '2024-01-01T00:00:00Z' -}}; -``` - -## Browser Support - -- Chrome 60+ -- Firefox 60+ -- Safari 12+ -- Edge 79+ - -## Node.js Support - -- Node.js 14+ - -## License - -MIT License - see LICENSE file for details. - -## Support - -- 📖 [Documentation](https://docs.authframework.com) -- 🐛 [Issue Tracker](https://github.com/authframework/client-js/issues) -- 💬 [Discord Community](https://discord.gg/authframework) -"#.to_string(); - - Ok(readme) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_sdk_generation() { - let config = EnhancedSdkConfig::default(); - let generator = JsSdkGenerator::new(config); - - let result = generator.generate_sdk(); - assert!(result.is_ok()); - - let files = result.unwrap(); - assert!(files.contains_key("client.ts")); - assert!(files.contains_key("types.ts")); - assert!(files.contains_key("rbac.ts")); - assert!(files.contains_key("index.ts")); - assert!(files.contains_key("package.json")); - } - - #[test] - fn test_typescript_generation() { - let config = EnhancedSdkConfig { - typescript: true, - include_rbac: true, - include_conditional_permissions: true, - include_audit: true, - ..Default::default() - }; - - let generator = JsSdkGenerator::new(config); - let files = generator.generate_sdk().unwrap(); - - // Verify all modules are included - assert!(files.contains_key("types.ts")); - assert!(files.contains_key("rbac.ts")); - assert!(files.contains_key("conditional.ts")); - assert!(files.contains_key("audit.ts")); - } -} diff --git a/src/sdks/mod.rs b/src/sdks/mod.rs deleted file mode 100644 index 6312828..0000000 --- a/src/sdks/mod.rs +++ /dev/null @@ -1,232 +0,0 @@ -//! SDK Generation Module -//! -//! This module provides SDK generators for multiple programming languages, -//! enabling easy integration of AuthFramework's enhanced RBAC capabilities. - -#[cfg(feature = "enhanced-rbac")] -pub mod javascript; - -#[cfg(feature = "enhanced-rbac")] -pub mod python; - -#[cfg(feature = "enhanced-rbac")] -pub use javascript::{EnhancedSdkConfig as JsConfig, JsSdkGenerator}; - -#[cfg(feature = "enhanced-rbac")] -pub use python::{PythonSdkConfig, PythonSdkGenerator}; - -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -/// SDK generation configuration -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SdkGenerationConfig { - /// Base API URL - pub base_url: String, - /// API version - pub version: String, - /// Languages to generate SDKs for - pub languages: Vec, - /// Include RBAC functionality - pub include_rbac: bool, - /// Include conditional permissions - pub include_conditional_permissions: bool, - /// Include audit logging - pub include_audit: bool, - /// Custom client names per language - pub client_names: HashMap, -} - -/// Supported SDK languages -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum SdkLanguage { - JavaScript, - TypeScript, - Python, - Rust, - Go, - Java, - CSharp, -} - -impl Default for SdkGenerationConfig { - fn default() -> Self { - let mut client_names = HashMap::new(); - client_names.insert(SdkLanguage::JavaScript, "AuthFrameworkClient".to_string()); - client_names.insert(SdkLanguage::TypeScript, "AuthFrameworkClient".to_string()); - client_names.insert(SdkLanguage::Python, "AuthFrameworkClient".to_string()); - - Self { - base_url: "https://api.example.com".to_string(), - version: "v1".to_string(), - languages: vec![SdkLanguage::TypeScript, SdkLanguage::Python], - include_rbac: true, - include_conditional_permissions: true, - include_audit: true, - client_names, - } - } -} - -/// SDK generation result -#[derive(Debug)] -pub struct SdkGenerationResult { - /// Generated files by language - pub files: HashMap>, - /// Generation errors - pub errors: Vec, -} - -/// SDK generation error -#[derive(Debug, thiserror::Error)] -pub enum SdkGenerationError { - #[error("Language {0:?} not supported")] - UnsupportedLanguage(SdkLanguage), - #[error("Generation failed for {language:?}: {error}")] - GenerationFailed { - language: SdkLanguage, - error: String, - }, - #[error("Configuration error: {0}")] - ConfigurationError(String), -} - -/// Multi-language SDK generator -pub struct SdkGenerator { - config: SdkGenerationConfig, -} - -impl SdkGenerator { - /// Create new SDK generator - pub fn new(config: SdkGenerationConfig) -> Self { - Self { config } - } - - /// Generate SDKs for all configured languages - pub fn generate_all(&self) -> Result> { - let mut result = SdkGenerationResult { - files: HashMap::new(), - errors: Vec::new(), - }; - - for &language in &self.config.languages { - match self.generate_for_language(language) { - Ok(files) => { - result.files.insert(language, files); - } - Err(error) => { - result.errors.push(SdkGenerationError::GenerationFailed { - language, - error: error.to_string(), - }); - } - } - } - - Ok(result) - } - - /// Generate SDK for specific language - pub fn generate_for_language( - &self, - language: SdkLanguage, - ) -> Result, Box> { - match language { - #[cfg(feature = "enhanced-rbac")] - SdkLanguage::JavaScript | SdkLanguage::TypeScript => { - let js_config = javascript::EnhancedSdkConfig { - base_url: self.config.base_url.clone(), - version: self.config.version.clone(), - typescript: language == SdkLanguage::TypeScript, - include_rbac: self.config.include_rbac, - include_conditional_permissions: self.config.include_conditional_permissions, - include_audit: self.config.include_audit, - client_name: self - .config - .client_names - .get(&language) - .cloned() - .unwrap_or_else(|| "AuthFrameworkClient".to_string()), - }; - - let generator = javascript::JsSdkGenerator::new(js_config); - generator.generate_sdk() - } - - #[cfg(feature = "enhanced-rbac")] - SdkLanguage::Python => { - let python_config = python::PythonSdkConfig { - base_url: self.config.base_url.clone(), - version: self.config.version.clone(), - include_rbac: self.config.include_rbac, - include_conditional_permissions: self.config.include_conditional_permissions, - include_audit: self.config.include_audit, - client_name: self - .config - .client_names - .get(&language) - .cloned() - .unwrap_or_else(|| "AuthFrameworkClient".to_string()), - async_support: true, - type_hints: true, - }; - - let generator = python::PythonSdkGenerator::new(python_config); - generator.generate_sdk() - } - - _ => Err(Box::new(SdkGenerationError::UnsupportedLanguage(language))), - } - } - - /// Get supported languages - pub fn supported_languages() -> Vec { - vec![ - #[cfg(feature = "enhanced-rbac")] - SdkLanguage::JavaScript, - #[cfg(feature = "enhanced-rbac")] - SdkLanguage::TypeScript, - #[cfg(feature = "enhanced-rbac")] - SdkLanguage::Python, - ] - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_sdk_generation_config() { - let config = SdkGenerationConfig::default(); - assert_eq!(config.version, "v1"); - assert!(config.include_rbac); - assert!(config.include_conditional_permissions); - assert!(config.include_audit); - } - - #[test] - fn test_supported_languages() { - let languages = SdkGenerator::supported_languages(); - assert!(!languages.is_empty()); - } - - #[cfg(feature = "enhanced-rbac")] - #[test] - fn test_multi_language_generation() { - let config = SdkGenerationConfig { - languages: vec![SdkLanguage::TypeScript, SdkLanguage::Python], - ..Default::default() - }; - - let generator = SdkGenerator::new(config); - let result = generator.generate_all(); - - assert!(result.is_ok()); - let sdk_result = result.unwrap(); - - // Should have generated files for both languages - assert!(sdk_result.files.contains_key(&SdkLanguage::TypeScript)); - assert!(sdk_result.files.contains_key(&SdkLanguage::Python)); - } -} diff --git a/src/sdks/python.rs b/src/sdks/python.rs deleted file mode 100644 index b6ce1e3..0000000 --- a/src/sdks/python.rs +++ /dev/null @@ -1,813 +0,0 @@ -//! Python SDK generator for enhanced RBAC functionality -//! -//! This module provides Python SDK generation with comprehensive -//! role-system integration and async/await support. - -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -/// Python SDK configuration -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PythonSdkConfig { - /// Base API URL - pub base_url: String, - /// API version - pub version: String, - /// Include RBAC functionality - pub include_rbac: bool, - /// Include conditional permissions - pub include_conditional_permissions: bool, - /// Include audit logging - pub include_audit: bool, - /// Client class name - pub client_name: String, - /// Enable async/await support - pub async_support: bool, - /// Include type hints - pub type_hints: bool, -} - -impl Default for PythonSdkConfig { - fn default() -> Self { - Self { - base_url: "https://api.example.com".to_string(), - version: "v1".to_string(), - include_rbac: true, - include_conditional_permissions: true, - include_audit: true, - client_name: "AuthFrameworkClient".to_string(), - async_support: true, - type_hints: true, - } - } -} - -/// Python SDK generator -pub struct PythonSdkGenerator { - config: PythonSdkConfig, -} - -impl PythonSdkGenerator { - /// Create new Python SDK generator - pub fn new(config: PythonSdkConfig) -> Self { - Self { config } - } - - /// Generate complete Python SDK - pub fn generate_sdk(&self) -> Result, Box> { - let mut files = HashMap::new(); - - // Generate main client - files.insert("client.py".to_string(), self.generate_base_client()?); - - // Generate type definitions - if self.config.type_hints { - files.insert("types.py".to_string(), self.generate_types()?); - } - - // Generate RBAC module - if self.config.include_rbac { - files.insert("rbac.py".to_string(), self.generate_rbac_module()?); - } - - // Generate conditional permissions - if self.config.include_conditional_permissions { - files.insert( - "conditional.py".to_string(), - self.generate_conditional_module()?, - ); - } - - // Generate audit module - if self.config.include_audit { - files.insert("audit.py".to_string(), self.generate_audit_module()?); - } - - // Generate utilities - files.insert("utils.py".to_string(), self.generate_utils()?); - - // Generate main package - files.insert("__init__.py".to_string(), self.generate_init()?); - - // Generate setup.py - files.insert("setup.py".to_string(), self.generate_setup()?); - - // Generate requirements - files.insert( - "requirements.txt".to_string(), - self.generate_requirements()?, - ); - - // Generate README - files.insert("README.md".to_string(), self.generate_readme()?); - - Ok(files) - } - - /// Generate base HTTP client - fn generate_base_client(&self) -> Result> { - let client_code = format!( - r#""" -Enhanced {} with RBAC Support -Generated by AuthFramework SDK Generator -""" - -import asyncio -import json -import time -from typing import Dict, Any, Optional, Union -{} -import aiohttp -import requests -from urllib.parse import urljoin, urlencode - - -class HttpError(Exception): - """HTTP error with status code and response data""" - - def __init__(self, status: int, message: str, data: Optional[Dict[str, Any]] = None): - self.status = status - self.message = message - self.data = data - super().__init__(f"HTTP {{status}}: {{message}}") - - -class ApiResponse: - """Wrapper for API responses""" - - def __init__(self, success: bool, data: Optional[Any] = None, - error: Optional[str] = None, message: Optional[str] = None): - self.success = success - self.data = data - self.error = error - self.message = message - - -class {}: - """Enhanced AuthFramework client with comprehensive RBAC support""" - - def __init__(self, base_url: str, access_token: Optional[str] = None, - api_key: Optional[str] = None, timeout: int = 30, - retry_attempts: int = 3): - self.base_url = base_url.rstrip('/') - self.timeout = timeout - self.retry_attempts = retry_attempts - self.headers = {{ - 'Content-Type': 'application/json', - 'User-Agent': 'AuthFramework-Python-SDK/1.0.0' - }} - - if access_token: - self.headers['Authorization'] = f'Bearer {{access_token}}' - - if api_key: - self.headers['X-API-Key'] = api_key - - def set_access_token(self, token: str) -> None: - """Set authentication token""" - self.headers['Authorization'] = f'Bearer {{token}}' - - def clear_access_token(self) -> None: - """Clear authentication token""" - self.headers.pop('Authorization', None) - - def set_api_key(self, api_key: str) -> None: - """Set API key""" - self.headers['X-API-Key'] = api_key - - def _make_request_sync(self, method: str, path: str, data: Optional[Dict] = None, - headers: Optional[Dict[str, str]] = None) -> ApiResponse: - """Make synchronous HTTP request with retry logic""" - url = urljoin(self.base_url, path) - request_headers = {{**self.headers, **(headers or {{}})}} - - last_error = None - - for attempt in range(self.retry_attempts + 1): - try: - response = requests.request( - method=method, - url=url, - headers=request_headers, - json=data, - timeout=self.timeout - ) - - response_data = response.json() if response.content else {{}} - - if not response.ok: - raise HttpError( - status=response.status_code, - message=response.reason or 'Request failed', - data=response_data - ) - - return ApiResponse( - success=response_data.get('success', True), - data=response_data.get('data'), - error=response_data.get('error'), - message=response_data.get('message') - ) - - except (requests.RequestException, HttpError) as e: - last_error = e - - # Don't retry on client errors (4xx) - if isinstance(e, HttpError) and 400 <= e.status < 500: - raise e - - # Wait before retry (exponential backoff) - if attempt < self.retry_attempts: - time.sleep(2 ** attempt) - - raise last_error or Exception('Request failed after all retries') - - async def _make_request_async(self, method: str, path: str, data: Optional[Dict] = None, - headers: Optional[Dict[str, str]] = None) -> ApiResponse: - """Make asynchronous HTTP request with retry logic""" - url = urljoin(self.base_url, path) - request_headers = {{**self.headers, **(headers or {{}})}} - - last_error = None - - for attempt in range(self.retry_attempts + 1): - try: - timeout = aiohttp.ClientTimeout(total=self.timeout) - - async with aiohttp.ClientSession(timeout=timeout) as session: - async with session.request( - method=method, - url=url, - headers=request_headers, - json=data - ) as response: - response_data = await response.json() if response.content_length else {{}} - - if not response.ok: - raise HttpError( - status=response.status, - message=response.reason or 'Request failed', - data=response_data - ) - - return ApiResponse( - success=response_data.get('success', True), - data=response_data.get('data'), - error=response_data.get('error'), - message=response_data.get('message') - ) - - except (aiohttp.ClientError, HttpError) as e: - last_error = e - - # Don't retry on client errors (4xx) - if isinstance(e, HttpError) and 400 <= e.status < 500: - raise e - - # Wait before retry (exponential backoff) - if attempt < self.retry_attempts: - await asyncio.sleep(2 ** attempt) - - raise last_error or Exception('Request failed after all retries') - - def get(self, path: str, headers: Optional[Dict[str, str]] = None) -> ApiResponse: - """Synchronous GET request""" - return self._make_request_sync('GET', path, headers=headers) - - def post(self, path: str, data: Optional[Dict] = None, headers: Optional[Dict[str, str]] = None) -> ApiResponse: - """Synchronous POST request""" - return self._make_request_sync('POST', path, data=data, headers=headers) - - def put(self, path: str, data: Optional[Dict] = None, headers: Optional[Dict[str, str]] = None) -> ApiResponse: - """Synchronous PUT request""" - return self._make_request_sync('PUT', path, data=data, headers=headers) - - def delete(self, path: str, headers: Optional[Dict[str, str]] = None) -> ApiResponse: - """Synchronous DELETE request""" - return self._make_request_sync('DELETE', path, headers=headers) - - async def get_async(self, path: str, headers: Optional[Dict[str, str]] = None) -> ApiResponse: - """Asynchronous GET request""" - return await self._make_request_async('GET', path, headers=headers) - - async def post_async(self, path: str, data: Optional[Dict] = None, headers: Optional[Dict[str, str]] = None) -> ApiResponse: - """Asynchronous POST request""" - return await self._make_request_async('POST', path, data=data, headers=headers) - - async def put_async(self, path: str, data: Optional[Dict] = None, headers: Optional[Dict[str, str]] = None) -> ApiResponse: - """Asynchronous PUT request""" - return await self._make_request_async('PUT', path, data=data, headers=headers) - - async def delete_async(self, path: str, headers: Optional[Dict[str, str]] = None) -> ApiResponse: - """Asynchronous DELETE request""" - return await self._make_request_async('DELETE', path, headers=headers) -"#, - self.config.client_name, - if self.config.type_hints { ", List" } else { "" }, - self.config.client_name - ); - - Ok(client_code) - } - - /// Generate Python type definitions - fn generate_types(&self) -> Result> { - let types_code = r#""" -Type definitions for AuthFramework RBAC -""" - -from typing import Dict, List, Optional, Union, Any -from datetime import datetime -from dataclasses import dataclass -from enum import Enum - - -class TimeOfDay(Enum): - """Time of day classification""" - BUSINESS_HOURS = "business_hours" - AFTER_HOURS = "after_hours" - WEEKEND = "weekend" - HOLIDAY = "holiday" - - -class DayType(Enum): - """Day type classification""" - WEEKDAY = "weekday" - WEEKEND = "weekend" - HOLIDAY = "holiday" - - -class DeviceType(Enum): - """Device type classification""" - DESKTOP = "desktop" - MOBILE = "mobile" - TABLET = "tablet" - UNKNOWN = "unknown" - - -class ConnectionType(Enum): - """Connection type classification""" - DIRECT = "direct" - VPN = "vpn" - PROXY = "proxy" - TOR = "tor" - CORPORATE = "corporate" - UNKNOWN = "unknown" - - -class SecurityLevel(Enum): - """Security level assessment""" - LOW = "low" - MEDIUM = "medium" - HIGH = "high" - CRITICAL = "critical" - - -@dataclass -class Role: - """Role definition""" - id: str - name: str - description: Optional[str] = None - parent_id: Optional[str] = None - permissions: List[str] = None - created_at: Optional[datetime] = None - updated_at: Optional[datetime] = None - - def __post_init__(self): - if self.permissions is None: - self.permissions = [] - - -@dataclass -class Permission: - """Permission definition""" - id: str - action: str - resource: str - conditions: Optional[Dict[str, str]] = None - created_at: Optional[datetime] = None - - -@dataclass -class UserRole: - """User role assignment""" - role_id: str - role_name: str - assigned_at: datetime - assigned_by: Optional[str] = None - expires_at: Optional[datetime] = None - - -@dataclass -class UserRolesResponse: - """User roles and effective permissions""" - user_id: str - roles: List[UserRole] - effective_permissions: List[str] - - -@dataclass -class CreateRoleRequest: - """Request to create a new role""" - name: str - description: Optional[str] = None - parent_id: Optional[str] = None - permissions: Optional[List[str]] = None - - -@dataclass -class UpdateRoleRequest: - """Request to update an existing role""" - name: Optional[str] = None - description: Optional[str] = None - parent_id: Optional[str] = None - - -@dataclass -class AssignRoleRequest: - """Request to assign a role to a user""" - role_id: str - expires_at: Optional[datetime] = None - reason: Optional[str] = None - - -@dataclass -class BulkAssignment: - """Single assignment in bulk operation""" - user_id: str - role_id: str - expires_at: Optional[datetime] = None - - -@dataclass -class BulkAssignRequest: - """Request for bulk role assignment""" - assignments: List[BulkAssignment] - - -@dataclass -class ElevateRoleRequest: - """Request for role elevation""" - target_role: str - duration_minutes: Optional[int] = None - justification: str = "" - - -@dataclass -class PermissionCheckRequest: - """Request to check permissions""" - action: str - resource: str - context: Optional[Dict[str, str]] = None - - -@dataclass -class PermissionCheckResponse: - """Response from permission check""" - granted: bool - reason: str - required_roles: List[str] - missing_permissions: List[str] - - -@dataclass -class AuditEntry: - """Audit log entry""" - id: str - user_id: Optional[str] - action: str - resource: Optional[str] - result: str - context: Dict[str, str] - timestamp: datetime - - -@dataclass -class AuditLogResponse: - """Response containing audit logs""" - entries: List[AuditEntry] - total_count: int - page: int - per_page: int - - -@dataclass -class AuditQuery: - """Query parameters for audit logs""" - user_id: Optional[str] = None - action: Optional[str] = None - resource: Optional[str] = None - start_time: Optional[datetime] = None - end_time: Optional[datetime] = None - page: Optional[int] = None - per_page: Optional[int] = None - - -@dataclass -class ConditionalContext: - """Context for conditional permissions""" - time_of_day: Optional[TimeOfDay] = None - day_type: Optional[DayType] = None - device_type: Optional[DeviceType] = None - connection_type: Optional[ConnectionType] = None - security_level: Optional[SecurityLevel] = None - risk_score: Optional[int] = None - ip_address: Optional[str] = None - user_agent: Optional[str] = None - custom_attributes: Optional[Dict[str, str]] = None - - -@dataclass -class RoleListQuery: - """Query parameters for listing roles""" - page: Optional[int] = None - per_page: Optional[int] = None - parent_id: Optional[str] = None - include_permissions: Optional[bool] = None -"#; - - Ok(types_code.to_string()) - } - - /// Generate RBAC module - fn generate_rbac_module(&self) -> Result> { - let rbac_code = format!( - r#""" -RBAC (Role-Based Access Control) Module -Provides comprehensive role and permission management -""" - -from typing import Dict, List, Optional, Union -{} -from .types import ( - Role, CreateRoleRequest, UpdateRoleRequest, UserRolesResponse, - AssignRoleRequest, BulkAssignRequest, ElevateRoleRequest, - PermissionCheckRequest, PermissionCheckResponse, RoleListQuery -) -from .client import ApiResponse - - -class RbacManager: - """RBAC management client""" - - def __init__(self, client): - self.client = client - - # ============================================================================ - # ROLE MANAGEMENT - # ============================================================================ - - def create_role(self, request: CreateRoleRequest) -> ApiResponse: - """Create a new role""" - data = {{ - 'name': request.name, - 'description': request.description, - 'parent_id': request.parent_id, - 'permissions': request.permissions - }} - return self.client.post('/{}/rbac/roles', data) - - def get_role(self, role_id: str) -> ApiResponse: - """Get role by ID""" - return self.client.get(f'/{}/rbac/roles/{{role_id}}') - - def list_roles(self, query: Optional[RoleListQuery] = None) -> ApiResponse: - """List all roles with pagination""" - path = '/{}/rbac/roles' - - if query: - params = [] - if query.page is not None: - params.append(f'page={{query.page}}') - if query.per_page is not None: - params.append(f'per_page={{query.per_page}}') - if query.parent_id is not None: - params.append(f'parent_id={{query.parent_id}}') - if query.include_permissions is not None: - params.append(f'include_permissions={{str(query.include_permissions).lower()}}') - - if params: - path += '?' + '&'.join(params) - - return self.client.get(path) - - def update_role(self, role_id: str, request: UpdateRoleRequest) -> ApiResponse: - """Update an existing role""" - data = {{}} - if request.name is not None: - data['name'] = request.name - if request.description is not None: - data['description'] = request.description - if request.parent_id is not None: - data['parent_id'] = request.parent_id - - return self.client.put(f'/{}/rbac/roles/{{role_id}}', data) - - def delete_role(self, role_id: str) -> ApiResponse: - """Delete a role""" - return self.client.delete(f'/{}/rbac/roles/{{role_id}}') - - # ============================================================================ - # USER ROLE ASSIGNMENTS - # ============================================================================ - - def assign_user_role(self, user_id: str, request: AssignRoleRequest) -> ApiResponse: - """Assign role to user""" - data = {{ - 'role_id': request.role_id, - 'expires_at': request.expires_at.isoformat() if request.expires_at else None, - 'reason': request.reason - }} - return self.client.post(f'/{}/rbac/users/{{user_id}}/roles', data) - - def revoke_user_role(self, user_id: str, role_id: str) -> ApiResponse: - """Revoke role from user""" - return self.client.delete(f'/{}/rbac/users/{{user_id}}/roles/{{role_id}}') - - def get_user_roles(self, user_id: str) -> ApiResponse: - """Get user's roles and effective permissions""" - return self.client.get(f'/{}/rbac/users/{{user_id}}/roles') - - def bulk_assign_roles(self, request: BulkAssignRequest) -> ApiResponse: - """Bulk assign roles to multiple users""" - data = {{ - 'assignments': [ - {{ - 'user_id': assignment.user_id, - 'role_id': assignment.role_id, - 'expires_at': assignment.expires_at.isoformat() if assignment.expires_at else None - }} - for assignment in request.assignments - ] - }} - return self.client.post('/{}/rbac/bulk/assign', data) - - # ============================================================================ - # PERMISSION CHECKING - # ============================================================================ - - def check_permission(self, request: PermissionCheckRequest) -> ApiResponse: - """Check if current user has permission""" - data = {{ - 'action': request.action, - 'resource': request.resource, - 'context': request.context - }} - return self.client.post('/{}/rbac/check-permission', data) - - def has_permission(self, action: str, resource: str, context: Optional[Dict[str, str]] = None) -> bool: - """Quick permission check (returns boolean)""" - try: - request = PermissionCheckRequest(action=action, resource=resource, context=context) - response = self.check_permission(request) - return response.data and response.data.get('granted', False) - except Exception: - return False - - def elevate_role(self, request: ElevateRoleRequest) -> ApiResponse: - """Request role elevation""" - data = {{ - 'target_role': request.target_role, - 'duration_minutes': request.duration_minutes, - 'justification': request.justification - }} - return self.client.post('/{}/rbac/elevate', data) - - # ============================================================================ - # ASYNC METHODS - # ============================================================================ - - async def create_role_async(self, request: CreateRoleRequest) -> ApiResponse: - """Create a new role (async)""" - data = {{ - 'name': request.name, - 'description': request.description, - 'parent_id': request.parent_id, - 'permissions': request.permissions - }} - return await self.client.post_async('/{}/rbac/roles', data) - - async def get_role_async(self, role_id: str) -> ApiResponse: - """Get role by ID (async)""" - return await self.client.get_async(f'/{}/rbac/roles/{{role_id}}') - - async def has_permission_async(self, action: str, resource: str, context: Optional[Dict[str, str]] = None) -> bool: - """Quick permission check (async, returns boolean)""" - try: - request = PermissionCheckRequest(action=action, resource=resource, context=context) - data = {{ - 'action': request.action, - 'resource': request.resource, - 'context': request.context - }} - response = await self.client.post_async('/{}/rbac/check-permission', data) - return response.data and response.data.get('granted', False) - except Exception: - return False - - # ============================================================================ - # CONVENIENCE METHODS - # ============================================================================ - - def user_has_any_role(self, user_id: str, role_names: List[str]) -> bool: - """Check if user has any of the specified roles""" - try: - response = self.get_user_roles(user_id) - if not response.data: - return False - - user_role_names = [role['role_name'] for role in response.data.get('roles', [])] - return any(role in user_role_names for role in role_names) - except Exception: - return False - - def user_has_all_roles(self, user_id: str, role_names: List[str]) -> bool: - """Check if user has all of the specified roles""" - try: - response = self.get_user_roles(user_id) - if not response.data: - return False - - user_role_names = [role['role_name'] for role in response.data.get('roles', [])] - return all(role in user_role_names for role in role_names) - except Exception: - return False - - def get_role_hierarchy(self) -> Dict[str, List[str]]: - """Get role hierarchy (parent-child relationships)""" - try: - response = self.list_roles(RoleListQuery(include_permissions=False)) - if not response.data: - return {{}} - - hierarchy = {{}} - - for role in response.data: - parent_id = role.get('parent_id') - if parent_id: - if parent_id not in hierarchy: - hierarchy[parent_id] = [] - hierarchy[parent_id].append(role['id']) - - return hierarchy - except Exception: - return {{}} - - def get_child_roles(self, parent_role_id: str) -> List[Role]: - """Get all child roles for a given parent role""" - try: - response = self.list_roles(RoleListQuery(parent_id=parent_role_id)) - return response.data or [] - except Exception: - return [] -"#, - "", // Type hints placeholder - both branches were identical - self.config.version, - self.config.version, - self.config.version, - self.config.version, - self.config.version, - self.config.version, - self.config.version, - self.config.version, - self.config.version, - self.config.version, - self.config.version, - self.config.version, - self.config.version, - self.config.version - ); - - Ok(rbac_code) - } - - /// Generate other modules (conditional, audit, utils) - fn generate_conditional_module(&self) -> Result> { - Ok("# Conditional permissions module - placeholder".to_string()) - } - - fn generate_audit_module(&self) -> Result> { - Ok("# Audit module - placeholder".to_string()) - } - - fn generate_utils(&self) -> Result> { - Ok("# Utils module - placeholder".to_string()) - } - - fn generate_init(&self) -> Result> { - Ok("# Package init - placeholder".to_string()) - } - - fn generate_setup(&self) -> Result> { - Ok("# Setup.py - placeholder".to_string()) - } - - fn generate_requirements(&self) -> Result> { - Ok("aiohttp>=3.8.0\nrequests>=2.28.0".to_string()) - } - - fn generate_readme(&self) -> Result> { - Ok("# AuthFramework Python SDK - placeholder".to_string()) - } -} diff --git a/src/server/security/mod.rs b/src/server/security/mod.rs index ea7fb2c..bb8d073 100644 --- a/src/server/security/mod.rs +++ b/src/server/security/mod.rs @@ -62,7 +62,7 @@ //! //! // FAPI compliance validation requires proper manager setup //! # let config = todo!(); // FAPI config implementation -//! # let dpop_manager_arc = Arc::new(dpop_manager); +//! # let dpop_manager_arc = Arc::new(dpop_manager); //! # let mutual_tls_manager = todo!(); // MutualTlsManager implementation //! # let par_manager = todo!(); // PARManager implementation //! # let private_key_jwt_manager = todo!(); // PrivateKeyJwtManager implementation diff --git a/src/tokens/mod.rs b/src/tokens/mod.rs index 33f3793..412ecf9 100644 --- a/src/tokens/mod.rs +++ b/src/tokens/mod.rs @@ -468,7 +468,7 @@ impl TokenManager { /// use auth_framework::tokens::TokenManager; /// /// // Both PKCS#1 and PKCS#8 formats work - /// let private_key = include_bytes!("../../tests/fixtures/test_private_key.pem"); // Test key + /// let private_key = include_bytes!("../../tests/fixtures/test_private_key.pem"); // Test key /// let public_key = include_bytes!("../../tests/fixtures/test_public_key.pem"); // Test key /// /// let manager = TokenManager::new_rsa( diff --git a/templates/base.html b/templates/base.html index 7619491..1bd2313 100644 --- a/templates/base.html +++ b/templates/base.html @@ -168,4 +168,4 @@
{% block scripts %}{% endblock %} - \ No newline at end of file + diff --git a/templates/config.html b/templates/config.html index c695c58..5c50a74 100644 --- a/templates/config.html +++ b/templates/config.html @@ -447,4 +447,4 @@
} }); -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/dashboard.html b/templates/dashboard.html index 7d435f0..45bf7b7 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -279,4 +279,4 @@
} }); -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/login.html b/templates/login.html index 9c7b109..2e95376 100644 --- a/templates/login.html +++ b/templates/login.html @@ -124,4 +124,4 @@

} }); -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/logs.html b/templates/logs.html index b7a18df..039c594 100644 --- a/templates/logs.html +++ b/templates/logs.html @@ -579,4 +579,4 @@

text-decoration: underline; } -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/security.html b/templates/security.html index 5d1c5df..c6989dc 100644 --- a/templates/security.html +++ b/templates/security.html @@ -628,4 +628,4 @@ document.getElementById('blockIpForm').addEventListener('submit', blockIp); }); -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/servers.html b/templates/servers.html index 678c908..9b2a607 100644 --- a/templates/servers.html +++ b/templates/servers.html @@ -296,4 +296,4 @@
Recent Server Logs
} }); -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/simple_config.html b/templates/simple_config.html index 88ea0b3..95d756e 100644 --- a/templates/simple_config.html +++ b/templates/simple_config.html @@ -10,4 +10,4 @@

Configuration

Configuration management will be implemented here.

- \ No newline at end of file + diff --git a/templates/simple_dashboard.html b/templates/simple_dashboard.html index dbed932..970bc42 100644 --- a/templates/simple_dashboard.html +++ b/templates/simple_dashboard.html @@ -21,4 +21,4 @@

Recent Events

{% endif %} - \ No newline at end of file + diff --git a/templates/simple_login.html b/templates/simple_login.html index 63c2889..bb4445e 100644 --- a/templates/simple_login.html +++ b/templates/simple_login.html @@ -14,4 +14,4 @@

Login

- \ No newline at end of file + diff --git a/templates/simple_logs.html b/templates/simple_logs.html index 3154781..cd7e3d5 100644 --- a/templates/simple_logs.html +++ b/templates/simple_logs.html @@ -10,4 +10,4 @@

Logs

Log viewer will be implemented here.

- \ No newline at end of file + diff --git a/templates/simple_security.html b/templates/simple_security.html index 9336296..018f9f4 100644 --- a/templates/simple_security.html +++ b/templates/simple_security.html @@ -10,4 +10,4 @@

Security

Security monitoring will be implemented here.

- \ No newline at end of file + diff --git a/templates/simple_servers.html b/templates/simple_servers.html index 6801f5d..cc1e69b 100644 --- a/templates/simple_servers.html +++ b/templates/simple_servers.html @@ -10,4 +10,4 @@

Servers

Server management will be implemented here.

- \ No newline at end of file + diff --git a/templates/simple_users.html b/templates/simple_users.html index a8228b9..b6387db 100644 --- a/templates/simple_users.html +++ b/templates/simple_users.html @@ -11,4 +11,4 @@

Users

User management will be implemented here.

- \ No newline at end of file + diff --git a/templates/users.html b/templates/users.html index 111b86b..01d3e1a 100644 --- a/templates/users.html +++ b/templates/users.html @@ -560,4 +560,4 @@ document.getElementById('createUserForm').addEventListener('submit', createUser); document.getElementById('editUserForm').addEventListener('submit', updateUser); -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/tests/fixtures/README.md b/tests/fixtures/README.md index b60c168..4ad3726 100644 --- a/tests/fixtures/README.md +++ b/tests/fixtures/README.md @@ -31,4 +31,4 @@ Generated using OpenSSL: ```bash openssl genrsa -out test_private_key.pem 2048 openssl rsa -in test_private_key.pem -pubout -out test_public_key.pem -``` \ No newline at end of file +```