├── backend ├── src │ ├── handlers │ │ ├── mod.rs │ │ └── auth.rs │ ├── middleware │ │ ├── mod.rs │ │ └── auth.rs │ ├── services │ │ ├── mod.rs │ │ └── auth.rs │ ├── models │ │ ├── mod.rs │ │ ├── user.rs │ │ ├── auth_token.rs │ │ ├── email.rs │ │ ├── sync_status.rs │ │ ├── link.rs │ │ └── tests.rs │ ├── database.rs │ └── main.rs ├── .gitignore ├── .env.example ├── Cargo.toml ├── migrations │ └── 001_initial_schema.sql └── Cargo.lock ├── frontend ├── src │ ├── app │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── layout.tsx │ │ └── page.tsx │ └── types │ │ └── index.ts ├── postcss.config.mjs ├── public │ ├── vercel.svg │ ├── window.svg │ ├── file.svg │ ├── globe.svg │ └── next.svg ├── next.config.ts ├── eslint.config.mjs ├── .gitignore ├── package.json ├── tsconfig.json └── README.md └── .kiro └── specs └── gmail-link-queue ├── requirements.md ├── tasks.md └── design.md /backend/src/handlers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod auth; -------------------------------------------------------------------------------- /backend/src/middleware/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod auth; -------------------------------------------------------------------------------- /backend/src/services/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod auth; -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .env 3 | *.db 4 | -------------------------------------------------------------------------------- /frontend/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcoury/etomys/master/frontend/src/app/favicon.ico -------------------------------------------------------------------------------- /frontend/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /frontend/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /backend/src/models/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod user; 2 | pub mod auth_token; 3 | pub mod link; 4 | pub mod email; 5 | pub mod sync_status; 6 | 7 | #[cfg(test)] 8 | mod tests; 9 | 10 | pub use user::User; 11 | pub use auth_token::AuthToken; 12 | pub use link::Link; 13 | pub use email::Email; 14 | pub use sync_status::{SyncStatus, SyncStatusType}; -------------------------------------------------------------------------------- /frontend/public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL=sqlite:gmail_link_queue.db 2 | 3 | # Server Configuration 4 | PORT=3002 5 | 6 | # Google OAuth2 Configuration 7 | GOOGLE_CLIENT_ID=your_google_client_id_here 8 | GOOGLE_CLIENT_SECRET=your_google_client_secret_here 9 | GOOGLE_REDIRECT_URI=http://localhost:3002/api/auth/callback 10 | 11 | # Token encryption key (change in production) 12 | TOKEN_ENCRYPTION_KEY=your_secure_encryption_key_here -------------------------------------------------------------------------------- /frontend/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | ]; 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /frontend/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | :root { 4 | --background: #ffffff; 5 | --foreground: #171717; 6 | } 7 | 8 | @theme inline { 9 | --color-background: var(--background); 10 | --color-foreground: var(--foreground); 11 | --font-sans: var(--font-geist-sans); 12 | --font-mono: var(--font-geist-mono); 13 | } 14 | 15 | @media (prefers-color-scheme: dark) { 16 | :root { 17 | --background: #0a0a0a; 18 | --foreground: #ededed; 19 | } 20 | } 21 | 22 | body { 23 | background: var(--background); 24 | color: var(--foreground); 25 | font-family: Arial, Helvetica, sans-serif; 26 | } 27 | -------------------------------------------------------------------------------- /backend/src/database.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use sqlx::{sqlite::SqlitePool, Pool, Sqlite}; 3 | 4 | pub type Database = Pool; 5 | 6 | pub async fn create_connection_pool(database_url: &str) -> Result { 7 | let pool = SqlitePool::connect(database_url).await?; 8 | Ok(pool) 9 | } 10 | 11 | pub async fn run_migrations(pool: &Database) -> Result<()> { 12 | sqlx::migrate!("./migrations").run(pool).await?; 13 | Ok(()) 14 | } 15 | 16 | pub async fn initialize_database(database_url: &str) -> Result { 17 | let pool = create_connection_pool(database_url).await?; 18 | run_migrations(&pool).await?; 19 | Ok(pool) 20 | } -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "react": "^19.0.0", 13 | "react-dom": "^19.0.0", 14 | "next": "15.3.5" 15 | }, 16 | "devDependencies": { 17 | "typescript": "^5", 18 | "@types/node": "^20", 19 | "@types/react": "^19", 20 | "@types/react-dom": "^19", 21 | "@tailwindcss/postcss": "^4", 22 | "tailwindcss": "^4", 23 | "eslint": "^9", 24 | "eslint-config-next": "15.3.5", 25 | "@eslint/eslintrc": "^3" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /backend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gmail-link-queue-backend" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | axum = "0.7" 8 | sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "sqlite", "chrono", "uuid", "migrate"] } 9 | tokio = { version = "1.0", features = ["full"] } 10 | serde = { version = "1.0", features = ["derive"] } 11 | serde_json = "1.0" 12 | anyhow = "1.0" 13 | thiserror = "1.0" 14 | chrono = { version = "0.4", features = ["serde"] } 15 | uuid = { version = "1.0", features = ["v4", "serde"] } 16 | tower = "0.4" 17 | tower-http = { version = "0.5", features = ["cors"] } 18 | dotenv = "0.15" 19 | oauth2 = "4.4" 20 | reqwest = { version = "0.11", features = ["json"] } 21 | url = "2.4" 22 | base64 = "0.21" 23 | rand = "0.8" 24 | 25 | [dev-dependencies] 26 | serde_urlencoded = "0.7" 27 | -------------------------------------------------------------------------------- /frontend/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Geist, Geist_Mono } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const geistSans = Geist({ 6 | variable: "--font-geist-sans", 7 | subsets: ["latin"], 8 | }); 9 | 10 | const geistMono = Geist_Mono({ 11 | variable: "--font-geist-mono", 12 | subsets: ["latin"], 13 | }); 14 | 15 | export const metadata: Metadata = { 16 | title: "Create Next App", 17 | description: "Generated by create next app", 18 | }; 19 | 20 | export default function RootLayout({ 21 | children, 22 | }: Readonly<{ 23 | children: React.ReactNode; 24 | }>) { 25 | return ( 26 | 27 | 30 | {children} 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /frontend/public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | id: string; 3 | email: string; 4 | created_at: string; 5 | updated_at: string; 6 | } 7 | 8 | export interface Link { 9 | id: string; 10 | user_id: string; 11 | email_id: string; 12 | url: string; 13 | title?: string; 14 | favicon_url?: string; 15 | is_read: boolean; 16 | read_at?: string; 17 | extracted_at: string; 18 | } 19 | 20 | export interface Email { 21 | id: string; 22 | user_id: string; 23 | gmail_message_id: string; 24 | subject?: string; 25 | sent_date: string; 26 | processed_at: string; 27 | } 28 | 29 | export enum SyncStatusType { 30 | Success = 'success', 31 | Error = 'error', 32 | InProgress = 'in_progress', 33 | } 34 | 35 | export interface SyncStatus { 36 | user_id: string; 37 | last_sync_at?: string; 38 | last_sync_status?: string; 39 | last_error_message?: string; 40 | next_sync_at?: string; 41 | } 42 | 43 | export interface AuthTokens { 44 | access_token: string; 45 | refresh_token: string; 46 | expires_at: string; 47 | } 48 | 49 | export interface ApiResponse { 50 | data?: T; 51 | error?: string; 52 | message?: string; 53 | } 54 | 55 | export interface LinkStats { 56 | total: number; 57 | read: number; 58 | unread: number; 59 | } -------------------------------------------------------------------------------- /frontend/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /backend/migrations/001_initial_schema.sql: -------------------------------------------------------------------------------- 1 | -- Users table for authentication data 2 | CREATE TABLE users ( 3 | id TEXT PRIMARY KEY, 4 | email TEXT NOT NULL UNIQUE, 5 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 6 | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP 7 | ); 8 | 9 | -- OAuth tokens storage 10 | CREATE TABLE auth_tokens ( 11 | user_id TEXT PRIMARY KEY, 12 | access_token TEXT NOT NULL, 13 | refresh_token TEXT NOT NULL, 14 | expires_at DATETIME NOT NULL, 15 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 16 | FOREIGN KEY (user_id) REFERENCES users(id) 17 | ); 18 | 19 | -- Email metadata 20 | CREATE TABLE emails ( 21 | id TEXT PRIMARY KEY, 22 | user_id TEXT NOT NULL, 23 | gmail_message_id TEXT NOT NULL UNIQUE, 24 | subject TEXT, 25 | sent_date DATETIME NOT NULL, 26 | processed_at DATETIME DEFAULT CURRENT_TIMESTAMP, 27 | FOREIGN KEY (user_id) REFERENCES users(id) 28 | ); 29 | 30 | -- Extracted links 31 | CREATE TABLE links ( 32 | id TEXT PRIMARY KEY, 33 | user_id TEXT NOT NULL, 34 | email_id TEXT NOT NULL, 35 | url TEXT NOT NULL, 36 | title TEXT, 37 | favicon_url TEXT, 38 | is_read BOOLEAN DEFAULT FALSE, 39 | read_at DATETIME, 40 | extracted_at DATETIME DEFAULT CURRENT_TIMESTAMP, 41 | FOREIGN KEY (user_id) REFERENCES users(id), 42 | FOREIGN KEY (email_id) REFERENCES emails(id) 43 | ); 44 | 45 | -- Sync status tracking 46 | CREATE TABLE sync_status ( 47 | user_id TEXT PRIMARY KEY, 48 | last_sync_at DATETIME, 49 | last_sync_status TEXT, -- 'success', 'error', 'in_progress' 50 | last_error_message TEXT, 51 | next_sync_at DATETIME, 52 | FOREIGN KEY (user_id) REFERENCES users(id) 53 | ); -------------------------------------------------------------------------------- /backend/src/main.rs: -------------------------------------------------------------------------------- 1 | mod database; 2 | mod handlers; 3 | mod middleware; 4 | mod models; 5 | mod services; 6 | 7 | use anyhow::Result; 8 | use axum::{routing::{get, post}, Router}; 9 | use std::sync::Arc; 10 | use tower_http::cors::CorsLayer; 11 | 12 | use handlers::auth::AppState; 13 | use services::auth::AuthService; 14 | 15 | #[tokio::main] 16 | async fn main() -> Result<()> { 17 | // Load environment variables 18 | dotenv::dotenv().ok(); 19 | 20 | // Initialize database 21 | let database_url = std::env::var("DATABASE_URL") 22 | .unwrap_or_else(|_| "sqlite:./gmail_link_queue.db".to_string()); 23 | let db_pool = database::initialize_database(&database_url).await?; 24 | 25 | // Initialize auth service 26 | let auth_service = Arc::new(AuthService::new(Arc::new(db_pool))?); 27 | 28 | // Create app state 29 | let app_state = AppState { 30 | auth_service: auth_service.clone(), 31 | }; 32 | 33 | // Create router with auth routes 34 | let app = Router::new() 35 | .route("/", get(|| async { "Gmail Link Queue Backend" })) 36 | .route("/api/auth/initiate", get(handlers::auth::initiate_auth)) 37 | .route("/api/auth/callback", get(handlers::auth::oauth_callback)) 38 | .route("/api/auth/refresh", post(handlers::auth::refresh_token)) 39 | .route("/api/auth/status", get(handlers::auth::auth_status)) 40 | .with_state(app_state) 41 | .layer(CorsLayer::permissive()); 42 | 43 | // Start server 44 | let port = std::env::var("PORT").unwrap_or_else(|_| "3001".to_string()); 45 | let bind_address = format!("0.0.0.0:{}", port); 46 | let listener = tokio::net::TcpListener::bind(&bind_address).await?; 47 | println!("Server running on http://localhost:{}", port); 48 | println!("Auth endpoints:"); 49 | println!(" GET /api/auth/initiate - Start OAuth flow"); 50 | println!(" GET /api/auth/callback - OAuth callback"); 51 | println!(" POST /api/auth/refresh - Refresh token"); 52 | println!(" GET /api/auth/status - Check auth status"); 53 | 54 | axum::serve(listener, app).await?; 55 | 56 | Ok(()) 57 | } 58 | -------------------------------------------------------------------------------- /backend/src/models/user.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use serde::{Deserialize, Serialize}; 3 | use sqlx::{FromRow, SqlitePool}; 4 | use anyhow::Result; 5 | 6 | #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] 7 | pub struct User { 8 | pub id: String, 9 | pub email: String, 10 | pub created_at: DateTime, 11 | pub updated_at: DateTime, 12 | } 13 | 14 | impl User { 15 | pub fn new(email: String) -> Self { 16 | let now = Utc::now(); 17 | Self { 18 | id: uuid::Uuid::new_v4().to_string(), 19 | email, 20 | created_at: now, 21 | updated_at: now, 22 | } 23 | } 24 | 25 | pub async fn create(&self, pool: &SqlitePool) -> Result<()> { 26 | sqlx::query("INSERT INTO users (id, email, created_at, updated_at) VALUES (?, ?, ?, ?)") 27 | .bind(&self.id) 28 | .bind(&self.email) 29 | .bind(&self.created_at) 30 | .bind(&self.updated_at) 31 | .execute(pool) 32 | .await?; 33 | Ok(()) 34 | } 35 | 36 | pub async fn find_by_id(pool: &SqlitePool, id: &str) -> Result> { 37 | let user = sqlx::query_as::<_, User>("SELECT id, email, created_at, updated_at FROM users WHERE id = ?") 38 | .bind(id) 39 | .fetch_optional(pool) 40 | .await?; 41 | Ok(user) 42 | } 43 | 44 | pub async fn find_by_email(pool: &SqlitePool, email: &str) -> Result> { 45 | let user = sqlx::query_as::<_, User>("SELECT id, email, created_at, updated_at FROM users WHERE email = ?") 46 | .bind(email) 47 | .fetch_optional(pool) 48 | .await?; 49 | Ok(user) 50 | } 51 | 52 | pub async fn update(&mut self, pool: &SqlitePool) -> Result<()> { 53 | self.updated_at = Utc::now(); 54 | sqlx::query("UPDATE users SET email = ?, updated_at = ? WHERE id = ?") 55 | .bind(&self.email) 56 | .bind(&self.updated_at) 57 | .bind(&self.id) 58 | .execute(pool) 59 | .await?; 60 | Ok(()) 61 | } 62 | 63 | pub async fn delete(pool: &SqlitePool, id: &str) -> Result<()> { 64 | sqlx::query("DELETE FROM users WHERE id = ?") 65 | .bind(id) 66 | .execute(pool) 67 | .await?; 68 | Ok(()) 69 | } 70 | } -------------------------------------------------------------------------------- /backend/src/models/auth_token.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use serde::{Deserialize, Serialize}; 3 | use sqlx::{FromRow, SqlitePool}; 4 | use anyhow::Result; 5 | 6 | #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] 7 | pub struct AuthToken { 8 | pub user_id: String, 9 | pub access_token: String, 10 | pub refresh_token: String, 11 | pub expires_at: DateTime, 12 | pub created_at: DateTime, 13 | } 14 | 15 | impl AuthToken { 16 | pub fn new( 17 | user_id: String, 18 | access_token: String, 19 | refresh_token: String, 20 | expires_at: DateTime, 21 | ) -> Self { 22 | Self { 23 | user_id, 24 | access_token, 25 | refresh_token, 26 | expires_at, 27 | created_at: Utc::now(), 28 | } 29 | } 30 | 31 | pub fn is_expired(&self) -> bool { 32 | Utc::now() > self.expires_at 33 | } 34 | 35 | pub fn update_tokens(&mut self, access_token: String, expires_at: DateTime) { 36 | self.access_token = access_token; 37 | self.expires_at = expires_at; 38 | } 39 | 40 | pub async fn create(&self, pool: &SqlitePool) -> Result<()> { 41 | sqlx::query("INSERT INTO auth_tokens (user_id, access_token, refresh_token, expires_at, created_at) VALUES (?, ?, ?, ?, ?)") 42 | .bind(&self.user_id) 43 | .bind(&self.access_token) 44 | .bind(&self.refresh_token) 45 | .bind(&self.expires_at) 46 | .bind(&self.created_at) 47 | .execute(pool) 48 | .await?; 49 | Ok(()) 50 | } 51 | 52 | pub async fn find_by_user_id(pool: &SqlitePool, user_id: &str) -> Result> { 53 | let token = sqlx::query_as::<_, AuthToken>("SELECT user_id, access_token, refresh_token, expires_at, created_at FROM auth_tokens WHERE user_id = ?") 54 | .bind(user_id) 55 | .fetch_optional(pool) 56 | .await?; 57 | Ok(token) 58 | } 59 | 60 | pub async fn update(&self, pool: &SqlitePool) -> Result<()> { 61 | sqlx::query("UPDATE auth_tokens SET access_token = ?, refresh_token = ?, expires_at = ? WHERE user_id = ?") 62 | .bind(&self.access_token) 63 | .bind(&self.refresh_token) 64 | .bind(&self.expires_at) 65 | .bind(&self.user_id) 66 | .execute(pool) 67 | .await?; 68 | Ok(()) 69 | } 70 | 71 | pub async fn delete(pool: &SqlitePool, user_id: &str) -> Result<()> { 72 | sqlx::query("DELETE FROM auth_tokens WHERE user_id = ?") 73 | .bind(user_id) 74 | .execute(pool) 75 | .await?; 76 | Ok(()) 77 | } 78 | } -------------------------------------------------------------------------------- /backend/src/models/email.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use serde::{Deserialize, Serialize}; 3 | use sqlx::{FromRow, SqlitePool}; 4 | use anyhow::Result; 5 | 6 | #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] 7 | pub struct Email { 8 | pub id: String, 9 | pub user_id: String, 10 | pub gmail_message_id: String, 11 | pub subject: Option, 12 | pub sent_date: DateTime, 13 | pub processed_at: DateTime, 14 | } 15 | 16 | impl Email { 17 | pub fn new(user_id: String, gmail_message_id: String, subject: Option, sent_date: DateTime) -> Self { 18 | Self { 19 | id: uuid::Uuid::new_v4().to_string(), 20 | user_id, 21 | gmail_message_id, 22 | subject, 23 | sent_date, 24 | processed_at: Utc::now(), 25 | } 26 | } 27 | 28 | pub async fn create(&self, pool: &SqlitePool) -> Result<()> { 29 | sqlx::query("INSERT INTO emails (id, user_id, gmail_message_id, subject, sent_date, processed_at) VALUES (?, ?, ?, ?, ?, ?)") 30 | .bind(&self.id) 31 | .bind(&self.user_id) 32 | .bind(&self.gmail_message_id) 33 | .bind(&self.subject) 34 | .bind(&self.sent_date) 35 | .bind(&self.processed_at) 36 | .execute(pool) 37 | .await?; 38 | Ok(()) 39 | } 40 | 41 | pub async fn find_by_id(pool: &SqlitePool, id: &str) -> Result> { 42 | let email = sqlx::query_as::<_, Email>("SELECT id, user_id, gmail_message_id, subject, sent_date, processed_at FROM emails WHERE id = ?") 43 | .bind(id) 44 | .fetch_optional(pool) 45 | .await?; 46 | Ok(email) 47 | } 48 | 49 | pub async fn find_by_gmail_message_id(pool: &SqlitePool, gmail_message_id: &str) -> Result> { 50 | let email = sqlx::query_as::<_, Email>("SELECT id, user_id, gmail_message_id, subject, sent_date, processed_at FROM emails WHERE gmail_message_id = ?") 51 | .bind(gmail_message_id) 52 | .fetch_optional(pool) 53 | .await?; 54 | Ok(email) 55 | } 56 | 57 | pub async fn find_by_user_id(pool: &SqlitePool, user_id: &str) -> Result> { 58 | let emails = sqlx::query_as::<_, Email>("SELECT id, user_id, gmail_message_id, subject, sent_date, processed_at FROM emails WHERE user_id = ? ORDER BY sent_date DESC") 59 | .bind(user_id) 60 | .fetch_all(pool) 61 | .await?; 62 | Ok(emails) 63 | } 64 | 65 | pub async fn delete(pool: &SqlitePool, id: &str) -> Result<()> { 66 | sqlx::query("DELETE FROM emails WHERE id = ?") 67 | .bind(id) 68 | .execute(pool) 69 | .await?; 70 | Ok(()) 71 | } 72 | } -------------------------------------------------------------------------------- /backend/src/models/sync_status.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use serde::{Deserialize, Serialize}; 3 | use sqlx::{FromRow, SqlitePool}; 4 | use anyhow::Result; 5 | 6 | #[derive(Debug, Clone, Serialize, Deserialize, sqlx::Type)] 7 | #[sqlx(type_name = "TEXT")] 8 | pub enum SyncStatusType { 9 | Success, 10 | Error, 11 | InProgress, 12 | } 13 | 14 | impl std::fmt::Display for SyncStatusType { 15 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 16 | match self { 17 | SyncStatusType::Success => write!(f, "success"), 18 | SyncStatusType::Error => write!(f, "error"), 19 | SyncStatusType::InProgress => write!(f, "in_progress"), 20 | } 21 | } 22 | } 23 | 24 | #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] 25 | pub struct SyncStatus { 26 | pub user_id: String, 27 | pub last_sync_at: Option>, 28 | pub last_sync_status: Option, 29 | pub last_error_message: Option, 30 | pub next_sync_at: Option>, 31 | } 32 | 33 | impl SyncStatus { 34 | pub fn new(user_id: String) -> Self { 35 | Self { 36 | user_id, 37 | last_sync_at: None, 38 | last_sync_status: None, 39 | last_error_message: None, 40 | next_sync_at: None, 41 | } 42 | } 43 | 44 | pub fn update_status(&mut self, status: SyncStatusType, error_message: Option) { 45 | self.last_sync_at = Some(Utc::now()); 46 | self.last_sync_status = Some(status.to_string()); 47 | self.last_error_message = error_message; 48 | } 49 | 50 | pub async fn create(&self, pool: &SqlitePool) -> Result<()> { 51 | sqlx::query("INSERT INTO sync_status (user_id, last_sync_at, last_sync_status, last_error_message, next_sync_at) VALUES (?, ?, ?, ?, ?)") 52 | .bind(&self.user_id) 53 | .bind(&self.last_sync_at) 54 | .bind(&self.last_sync_status) 55 | .bind(&self.last_error_message) 56 | .bind(&self.next_sync_at) 57 | .execute(pool) 58 | .await?; 59 | Ok(()) 60 | } 61 | 62 | pub async fn find_by_user_id(pool: &SqlitePool, user_id: &str) -> Result> { 63 | let sync_status = sqlx::query_as::<_, SyncStatus>("SELECT user_id, last_sync_at, last_sync_status, last_error_message, next_sync_at FROM sync_status WHERE user_id = ?") 64 | .bind(user_id) 65 | .fetch_optional(pool) 66 | .await?; 67 | Ok(sync_status) 68 | } 69 | 70 | pub async fn update(&self, pool: &SqlitePool) -> Result<()> { 71 | sqlx::query("UPDATE sync_status SET last_sync_at = ?, last_sync_status = ?, last_error_message = ?, next_sync_at = ? WHERE user_id = ?") 72 | .bind(&self.last_sync_at) 73 | .bind(&self.last_sync_status) 74 | .bind(&self.last_error_message) 75 | .bind(&self.next_sync_at) 76 | .bind(&self.user_id) 77 | .execute(pool) 78 | .await?; 79 | Ok(()) 80 | } 81 | 82 | pub async fn upsert(&self, pool: &SqlitePool) -> Result<()> { 83 | sqlx::query("INSERT INTO sync_status (user_id, last_sync_at, last_sync_status, last_error_message, next_sync_at) VALUES (?, ?, ?, ?, ?) ON CONFLICT(user_id) DO UPDATE SET last_sync_at = excluded.last_sync_at, last_sync_status = excluded.last_sync_status, last_error_message = excluded.last_error_message, next_sync_at = excluded.next_sync_at") 84 | .bind(&self.user_id) 85 | .bind(&self.last_sync_at) 86 | .bind(&self.last_sync_status) 87 | .bind(&self.last_error_message) 88 | .bind(&self.next_sync_at) 89 | .execute(pool) 90 | .await?; 91 | Ok(()) 92 | } 93 | 94 | pub async fn delete(pool: &SqlitePool, user_id: &str) -> Result<()> { 95 | sqlx::query("DELETE FROM sync_status WHERE user_id = ?") 96 | .bind(user_id) 97 | .execute(pool) 98 | .await?; 99 | Ok(()) 100 | } 101 | } -------------------------------------------------------------------------------- /backend/src/models/link.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use serde::{Deserialize, Serialize}; 3 | use sqlx::{FromRow, SqlitePool}; 4 | use anyhow::Result; 5 | 6 | #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] 7 | pub struct Link { 8 | pub id: String, 9 | pub user_id: String, 10 | pub email_id: String, 11 | pub url: String, 12 | pub title: Option, 13 | pub favicon_url: Option, 14 | pub is_read: bool, 15 | pub read_at: Option>, 16 | pub extracted_at: DateTime, 17 | } 18 | 19 | impl Link { 20 | pub fn new(user_id: String, email_id: String, url: String) -> Self { 21 | Self { 22 | id: uuid::Uuid::new_v4().to_string(), 23 | user_id, 24 | email_id, 25 | url, 26 | title: None, 27 | favicon_url: None, 28 | is_read: false, 29 | read_at: None, 30 | extracted_at: Utc::now(), 31 | } 32 | } 33 | 34 | pub fn mark_as_read(&mut self) { 35 | self.is_read = true; 36 | self.read_at = Some(Utc::now()); 37 | } 38 | 39 | pub async fn create(&self, pool: &SqlitePool) -> Result<()> { 40 | sqlx::query("INSERT INTO links (id, user_id, email_id, url, title, favicon_url, is_read, read_at, extracted_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)") 41 | .bind(&self.id) 42 | .bind(&self.user_id) 43 | .bind(&self.email_id) 44 | .bind(&self.url) 45 | .bind(&self.title) 46 | .bind(&self.favicon_url) 47 | .bind(&self.is_read) 48 | .bind(&self.read_at) 49 | .bind(&self.extracted_at) 50 | .execute(pool) 51 | .await?; 52 | Ok(()) 53 | } 54 | 55 | pub async fn find_by_id(pool: &SqlitePool, id: &str) -> Result> { 56 | let link = sqlx::query_as::<_, Link>("SELECT id, user_id, email_id, url, title, favicon_url, is_read, read_at, extracted_at FROM links WHERE id = ?") 57 | .bind(id) 58 | .fetch_optional(pool) 59 | .await?; 60 | Ok(link) 61 | } 62 | 63 | pub async fn find_by_user_id(pool: &SqlitePool, user_id: &str, include_read: bool) -> Result> { 64 | let links = if include_read { 65 | sqlx::query_as::<_, Link>("SELECT id, user_id, email_id, url, title, favicon_url, is_read, read_at, extracted_at FROM links WHERE user_id = ? ORDER BY extracted_at DESC") 66 | .bind(user_id) 67 | .fetch_all(pool) 68 | .await? 69 | } else { 70 | sqlx::query_as::<_, Link>("SELECT id, user_id, email_id, url, title, favicon_url, is_read, read_at, extracted_at FROM links WHERE user_id = ? AND is_read = 0 ORDER BY extracted_at DESC") 71 | .bind(user_id) 72 | .fetch_all(pool) 73 | .await? 74 | }; 75 | Ok(links) 76 | } 77 | 78 | pub async fn find_by_email_id(pool: &SqlitePool, email_id: &str) -> Result> { 79 | let links = sqlx::query_as::<_, Link>("SELECT id, user_id, email_id, url, title, favicon_url, is_read, read_at, extracted_at FROM links WHERE email_id = ?") 80 | .bind(email_id) 81 | .fetch_all(pool) 82 | .await?; 83 | Ok(links) 84 | } 85 | 86 | pub async fn update(&self, pool: &SqlitePool) -> Result<()> { 87 | sqlx::query("UPDATE links SET title = ?, favicon_url = ?, is_read = ?, read_at = ? WHERE id = ?") 88 | .bind(&self.title) 89 | .bind(&self.favicon_url) 90 | .bind(&self.is_read) 91 | .bind(&self.read_at) 92 | .bind(&self.id) 93 | .execute(pool) 94 | .await?; 95 | Ok(()) 96 | } 97 | 98 | pub async fn mark_as_read_by_id(pool: &SqlitePool, id: &str) -> Result<()> { 99 | let now = Utc::now(); 100 | sqlx::query("UPDATE links SET is_read = 1, read_at = ? WHERE id = ?") 101 | .bind(&now) 102 | .bind(id) 103 | .execute(pool) 104 | .await?; 105 | Ok(()) 106 | } 107 | 108 | pub async fn delete(pool: &SqlitePool, id: &str) -> Result<()> { 109 | sqlx::query("DELETE FROM links WHERE id = ?") 110 | .bind(id) 111 | .execute(pool) 112 | .await?; 113 | Ok(()) 114 | } 115 | } -------------------------------------------------------------------------------- /frontend/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | export default function Home() { 4 | return ( 5 |
6 |
7 | Next.js logo 15 |
    16 |
  1. 17 | Get started by editing{" "} 18 | 19 | src/app/page.tsx 20 | 21 | . 22 |
  2. 23 |
  3. 24 | Save and see your changes instantly. 25 |
  4. 26 |
27 | 28 | 53 |
54 | 101 |
102 | ); 103 | } 104 | -------------------------------------------------------------------------------- /.kiro/specs/gmail-link-queue/requirements.md: -------------------------------------------------------------------------------- 1 | # Requirements Document 2 | 3 | ## Introduction 4 | 5 | This feature implements a Gmail link aggregator application that connects to a user's Gmail account, extracts links from emails sent to themselves, and provides a web interface to manage and visit these links. The system consists of a Rust backend for Gmail integration and data management, and a Next.js/React frontend for user interaction. 6 | 7 | ## Requirements 8 | 9 | ### Requirement 1 10 | 11 | **User Story:** As a user, I want to connect my Gmail account to the application, so that I can access emails I've sent to myself containing links. 12 | 13 | #### Acceptance Criteria 14 | 15 | 1. WHEN the user initiates Gmail authentication THEN the system SHALL redirect to Google OAuth2 authorization flow 16 | 2. WHEN the user grants permission THEN the system SHALL store the authentication tokens securely 17 | 3. WHEN authentication expires THEN the system SHALL automatically refresh tokens using the refresh token 18 | 4. IF authentication fails THEN the system SHALL display an appropriate error message and allow retry 19 | 20 | ### Requirement 2 21 | 22 | **User Story:** As a user, I want the system to automatically fetch emails I've sent to myself, so that I don't have to manually import them. 23 | 24 | #### Acceptance Criteria 25 | 26 | 1. WHEN the system runs email sync THEN it SHALL query Gmail API for emails where sender equals recipient 27 | 2. WHEN new emails are found THEN the system SHALL extract all HTTP/HTTPS links from email content 28 | 3. WHEN processing emails THEN the system SHALL store email metadata (date, subject, message ID) in the database 29 | 4. WHEN duplicate emails are encountered THEN the system SHALL skip processing to avoid duplicates 30 | 5. WHEN API rate limits are reached THEN the system SHALL implement exponential backoff retry logic 31 | 32 | ### Requirement 3 33 | 34 | **User Story:** As a user, I want all extracted links to be cached in a local database, so that I can access them quickly without repeated API calls. 35 | 36 | #### Acceptance Criteria 37 | 38 | 1. WHEN links are extracted THEN the system SHALL store them with associated email metadata 39 | 2. WHEN storing links THEN the system SHALL capture URL, title (if available), extraction date, and read status 40 | 3. WHEN the same link appears in multiple emails THEN the system SHALL maintain separate records with different contexts 41 | 4. WHEN database operations fail THEN the system SHALL log errors and continue processing other items 42 | 43 | ### Requirement 4 44 | 45 | **User Story:** As a user, I want a web interface showing all my links as icons in a queue format, so that I can easily browse and access them. 46 | 47 | #### Acceptance Criteria 48 | 49 | 1. WHEN the user loads the web interface THEN the system SHALL display all unread links as clickable icons 50 | 2. WHEN displaying links THEN the system SHALL show website favicons or default icons 51 | 3. WHEN links are displayed THEN the system SHALL show relevant metadata (source email subject, date) 52 | 4. WHEN the interface loads THEN the system SHALL sort links by most recent first 53 | 5. WHEN there are no links THEN the system SHALL display an appropriate empty state message 54 | 55 | ### Requirement 5 56 | 57 | **User Story:** As a user, I want to mark links as read when I visit them, so that I can track which links I've already processed. 58 | 59 | #### Acceptance Criteria 60 | 61 | 1. WHEN the user clicks a link THEN the system SHALL open the link in a new tab/window 62 | 2. WHEN a link is clicked THEN the system SHALL mark it as read in the database 63 | 3. WHEN a link is marked as read THEN the system SHALL update the UI to reflect the read status 64 | 4. WHEN viewing the queue THEN the system SHALL provide an option to show/hide read links 65 | 5. WHEN a link is marked as read THEN the system SHALL timestamp the read action 66 | 67 | ### Requirement 6 68 | 69 | **User Story:** As a user, I want the system to periodically sync new emails, so that I always have the latest links available. 70 | 71 | #### Acceptance Criteria 72 | 73 | 1. WHEN the application is running THEN the system SHALL automatically sync emails at configurable intervals 74 | 2. WHEN manual sync is triggered THEN the system SHALL immediately fetch new emails 75 | 3. WHEN sync is in progress THEN the system SHALL display sync status to the user 76 | 4. WHEN sync completes THEN the system SHALL update the link count and refresh the display 77 | 5. IF sync fails THEN the system SHALL log the error and retry according to configured policy 78 | 79 | ### Requirement 7 80 | 81 | **User Story:** As a user, I want the system to handle errors gracefully, so that temporary issues don't break the application. 82 | 83 | #### Acceptance Criteria 84 | 85 | 1. WHEN Gmail API is unavailable THEN the system SHALL display cached data and show connection status 86 | 2. WHEN database operations fail THEN the system SHALL log errors and attempt recovery 87 | 3. WHEN network connectivity is lost THEN the system SHALL queue operations for retry when connection is restored 88 | 4. WHEN authentication tokens expire THEN the system SHALL attempt automatic refresh before prompting re-authentication 89 | 5. WHEN unexpected errors occur THEN the system SHALL log detailed error information for debugging -------------------------------------------------------------------------------- /backend/src/middleware/auth.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use axum::{ 3 | extract::{Request, State}, 4 | http::{header::AUTHORIZATION, StatusCode}, 5 | middleware::Next, 6 | response::{IntoResponse, Response}, 7 | Json, 8 | }; 9 | use serde_json::json; 10 | use std::sync::Arc; 11 | 12 | use crate::services::auth::AuthService; 13 | 14 | #[derive(Clone)] 15 | pub struct AuthState { 16 | pub auth_service: Arc, 17 | } 18 | 19 | /// Middleware to validate and refresh authentication tokens 20 | pub async fn auth_middleware( 21 | State(auth_state): State, 22 | mut request: Request, 23 | next: Next, 24 | ) -> Result { 25 | // Extract user ID from request headers or query params 26 | let user_id = extract_user_id(&request)?; 27 | 28 | // Get valid access token (refresh if necessary) 29 | let access_token = auth_state 30 | .auth_service 31 | .get_valid_access_token(&user_id) 32 | .await 33 | .map_err(|e| AuthError::TokenRefreshFailed(e.to_string()))?; 34 | 35 | // Add the valid access token to request extensions for use in handlers 36 | request.extensions_mut().insert(AuthContext { 37 | user_id: user_id.clone(), 38 | access_token, 39 | }); 40 | 41 | Ok(next.run(request).await) 42 | } 43 | 44 | /// Optional middleware that doesn't fail if auth is missing 45 | pub async fn optional_auth_middleware( 46 | State(auth_state): State, 47 | mut request: Request, 48 | next: Next, 49 | ) -> Response { 50 | // Try to extract user ID, but don't fail if missing 51 | if let Ok(user_id) = extract_user_id(&request) { 52 | // Try to get valid access token 53 | if let Ok(access_token) = auth_state 54 | .auth_service 55 | .get_valid_access_token(&user_id) 56 | .await 57 | { 58 | request.extensions_mut().insert(AuthContext { 59 | user_id, 60 | access_token, 61 | }); 62 | } 63 | } 64 | 65 | next.run(request).await 66 | } 67 | 68 | /// Extract user ID from request 69 | fn extract_user_id(request: &Request) -> Result { 70 | // Try to get user ID from Authorization header 71 | if let Some(auth_header) = request.headers().get(AUTHORIZATION) { 72 | let auth_str = auth_header 73 | .to_str() 74 | .map_err(|_| anyhow!("Invalid authorization header"))?; 75 | 76 | if auth_str.starts_with("Bearer ") { 77 | // Extract user ID from bearer token (this is a simplified approach) 78 | // In a real implementation, you might decode a JWT or lookup a session 79 | let token_part = &auth_str[7..]; // Remove "Bearer " 80 | 81 | // For now, we'll assume the token contains the user ID 82 | // In production, you'd validate the token and extract the user ID 83 | return Ok(token_part.to_string()); 84 | } 85 | } 86 | 87 | // Try to get user ID from query parameter 88 | if let Some(query) = request.uri().query() { 89 | for param in query.split('&') { 90 | if let Some((key, value)) = param.split_once('=') { 91 | if key == "user_id" { 92 | return Ok(value.to_string()); 93 | } 94 | } 95 | } 96 | } 97 | 98 | Err(anyhow!("No user ID found in request")) 99 | } 100 | 101 | /// Authentication context added to request extensions 102 | #[derive(Debug, Clone)] 103 | pub struct AuthContext { 104 | pub user_id: String, 105 | pub access_token: String, 106 | } 107 | 108 | /// Authentication errors 109 | #[derive(Debug, thiserror::Error)] 110 | pub enum AuthError { 111 | #[error("Missing authentication")] 112 | MissingAuth, 113 | 114 | #[error("Invalid authentication: {0}")] 115 | InvalidAuth(String), 116 | 117 | #[error("Token refresh failed: {0}")] 118 | TokenRefreshFailed(String), 119 | 120 | #[error("User not found")] 121 | UserNotFound, 122 | } 123 | 124 | impl From for AuthError { 125 | fn from(err: anyhow::Error) -> Self { 126 | AuthError::InvalidAuth(err.to_string()) 127 | } 128 | } 129 | 130 | impl IntoResponse for AuthError { 131 | fn into_response(self) -> Response { 132 | let (status, message) = match self { 133 | AuthError::MissingAuth => (StatusCode::UNAUTHORIZED, "Missing authentication"), 134 | AuthError::InvalidAuth(_) => (StatusCode::UNAUTHORIZED, "Invalid authentication"), 135 | AuthError::TokenRefreshFailed(_) => (StatusCode::UNAUTHORIZED, "Token refresh failed"), 136 | AuthError::UserNotFound => (StatusCode::NOT_FOUND, "User not found"), 137 | }; 138 | 139 | let body = Json(json!({ 140 | "error": message, 141 | "details": self.to_string() 142 | })); 143 | 144 | (status, body).into_response() 145 | } 146 | } 147 | 148 | /// Helper function to extract auth context from request extensions 149 | pub fn get_auth_context(request: &Request) -> Option<&AuthContext> { 150 | request.extensions().get::() 151 | } 152 | 153 | #[cfg(test)] 154 | mod tests { 155 | use super::*; 156 | use axum::{ 157 | body::Body, 158 | http::{Method, Request}, 159 | }; 160 | 161 | #[test] 162 | fn test_extract_user_id_from_bearer_token() { 163 | let request = Request::builder() 164 | .method(Method::GET) 165 | .uri("/test") 166 | .header(AUTHORIZATION, "Bearer user123") 167 | .body(Body::empty()) 168 | .unwrap(); 169 | 170 | let user_id = extract_user_id(&request).unwrap(); 171 | assert_eq!(user_id, "user123"); 172 | } 173 | 174 | #[test] 175 | fn test_extract_user_id_from_query_param() { 176 | let request = Request::builder() 177 | .method(Method::GET) 178 | .uri("/test?user_id=user456&other=value") 179 | .body(Body::empty()) 180 | .unwrap(); 181 | 182 | let user_id = extract_user_id(&request).unwrap(); 183 | assert_eq!(user_id, "user456"); 184 | } 185 | 186 | #[test] 187 | fn test_extract_user_id_missing() { 188 | let request = Request::builder() 189 | .method(Method::GET) 190 | .uri("/test") 191 | .body(Body::empty()) 192 | .unwrap(); 193 | 194 | let result = extract_user_id(&request); 195 | assert!(result.is_err()); 196 | } 197 | } -------------------------------------------------------------------------------- /backend/src/handlers/auth.rs: -------------------------------------------------------------------------------- 1 | use axum::{ 2 | extract::{Query, State}, 3 | http::StatusCode, 4 | response::IntoResponse, 5 | Json, 6 | }; 7 | use serde::{Deserialize, Serialize}; 8 | use serde_json::json; 9 | use std::sync::Arc; 10 | 11 | use crate::services::auth::AuthService; 12 | 13 | #[derive(Clone)] 14 | pub struct AppState { 15 | pub auth_service: Arc, 16 | } 17 | 18 | /// Request to initiate OAuth flow 19 | #[derive(Debug, Deserialize)] 20 | pub struct InitiateAuthRequest { 21 | pub user_id: Option, 22 | } 23 | 24 | /// Response for auth initiation 25 | #[derive(Debug, Serialize)] 26 | pub struct InitiateAuthResponse { 27 | pub auth_url: String, 28 | pub csrf_token: String, 29 | } 30 | 31 | /// OAuth callback query parameters 32 | #[derive(Debug, Deserialize)] 33 | pub struct OAuthCallbackQuery { 34 | pub code: Option, 35 | pub state: Option, 36 | pub error: Option, 37 | pub error_description: Option, 38 | } 39 | 40 | /// Response for successful authentication 41 | #[derive(Debug, Serialize)] 42 | pub struct AuthSuccessResponse { 43 | pub user_id: String, 44 | pub message: String, 45 | } 46 | 47 | /// Request to refresh token 48 | #[derive(Debug, Deserialize)] 49 | pub struct RefreshTokenRequest { 50 | pub user_id: String, 51 | } 52 | 53 | /// Response for token refresh 54 | #[derive(Debug, Serialize)] 55 | pub struct RefreshTokenResponse { 56 | pub message: String, 57 | pub expires_at: String, 58 | } 59 | 60 | /// Initiate OAuth flow 61 | pub async fn initiate_auth( 62 | State(state): State, 63 | Query(params): Query, 64 | ) -> Result { 65 | let (auth_url, csrf_token) = state 66 | .auth_service 67 | .generate_auth_url(params.user_id) 68 | .await 69 | .map_err(|e| AuthHandlerError::InternalError(e.to_string()))?; 70 | 71 | let response = InitiateAuthResponse { 72 | auth_url, 73 | csrf_token, 74 | }; 75 | 76 | Ok(Json(response)) 77 | } 78 | 79 | /// Handle OAuth callback 80 | pub async fn oauth_callback( 81 | State(state): State, 82 | Query(params): Query, 83 | ) -> Result { 84 | // Check for OAuth errors 85 | if let Some(error) = params.error { 86 | let error_desc = params.error_description.unwrap_or_else(|| "Unknown error".to_string()); 87 | return Err(AuthHandlerError::OAuthError(format!("{}: {}", error, error_desc))); 88 | } 89 | 90 | // Extract required parameters 91 | let authorization_code = params 92 | .code 93 | .ok_or_else(|| AuthHandlerError::MissingParameter("code".to_string()))?; 94 | 95 | let csrf_token = params 96 | .state 97 | .ok_or_else(|| AuthHandlerError::MissingParameter("state".to_string()))?; 98 | 99 | // Handle the OAuth callback 100 | let (_token_set, user_id) = state 101 | .auth_service 102 | .handle_oauth_callback(&authorization_code, &csrf_token) 103 | .await 104 | .map_err(|e| AuthHandlerError::AuthenticationFailed(e.to_string()))?; 105 | 106 | // In a real application, you might redirect to a success page 107 | // For now, we'll return JSON response 108 | let response = AuthSuccessResponse { 109 | user_id, 110 | message: "Authentication successful".to_string(), 111 | }; 112 | 113 | Ok(Json(response)) 114 | } 115 | 116 | /// Refresh access token 117 | pub async fn refresh_token( 118 | State(state): State, 119 | Json(request): Json, 120 | ) -> Result { 121 | let token_set = state 122 | .auth_service 123 | .refresh_access_token(&request.user_id) 124 | .await 125 | .map_err(|e| AuthHandlerError::TokenRefreshFailed(e.to_string()))?; 126 | 127 | let response = RefreshTokenResponse { 128 | message: "Token refreshed successfully".to_string(), 129 | expires_at: token_set.expires_at.to_rfc3339(), 130 | }; 131 | 132 | Ok(Json(response)) 133 | } 134 | 135 | /// Check authentication status 136 | pub async fn auth_status( 137 | State(state): State, 138 | Query(params): Query, 139 | ) -> Result { 140 | let has_valid_tokens = state 141 | .auth_service 142 | .has_valid_tokens(¶ms.user_id) 143 | .await 144 | .map_err(|e| AuthHandlerError::InternalError(e.to_string()))?; 145 | 146 | let response = json!({ 147 | "user_id": params.user_id, 148 | "authenticated": has_valid_tokens, 149 | "message": if has_valid_tokens { "User is authenticated" } else { "User needs to authenticate" } 150 | }); 151 | 152 | Ok(Json(response)) 153 | } 154 | 155 | /// Authentication handler errors 156 | #[derive(Debug, thiserror::Error)] 157 | pub enum AuthHandlerError { 158 | #[error("Missing parameter: {0}")] 159 | MissingParameter(String), 160 | 161 | #[error("OAuth error: {0}")] 162 | OAuthError(String), 163 | 164 | #[error("Authentication failed: {0}")] 165 | AuthenticationFailed(String), 166 | 167 | #[error("Token refresh failed: {0}")] 168 | TokenRefreshFailed(String), 169 | 170 | #[error("Internal error: {0}")] 171 | InternalError(String), 172 | } 173 | 174 | impl IntoResponse for AuthHandlerError { 175 | fn into_response(self) -> axum::response::Response { 176 | let (status, message) = match &self { 177 | AuthHandlerError::MissingParameter(_) => (StatusCode::BAD_REQUEST, "Missing parameter"), 178 | AuthHandlerError::OAuthError(_) => (StatusCode::BAD_REQUEST, "OAuth error"), 179 | AuthHandlerError::AuthenticationFailed(_) => (StatusCode::UNAUTHORIZED, "Authentication failed"), 180 | AuthHandlerError::TokenRefreshFailed(_) => (StatusCode::UNAUTHORIZED, "Token refresh failed"), 181 | AuthHandlerError::InternalError(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Internal error"), 182 | }; 183 | 184 | let body = Json(json!({ 185 | "error": message, 186 | "details": self.to_string() 187 | })); 188 | 189 | (status, body).into_response() 190 | } 191 | } 192 | 193 | #[cfg(test)] 194 | mod tests { 195 | use super::*; 196 | 197 | #[test] 198 | fn test_oauth_callback_query_parsing() { 199 | // Test successful callback 200 | let query = "code=auth_code_123&state=csrf_token_456"; 201 | let parsed: OAuthCallbackQuery = serde_urlencoded::from_str(query).unwrap(); 202 | 203 | assert_eq!(parsed.code, Some("auth_code_123".to_string())); 204 | assert_eq!(parsed.state, Some("csrf_token_456".to_string())); 205 | assert_eq!(parsed.error, None); 206 | } 207 | 208 | #[test] 209 | fn test_oauth_callback_error_parsing() { 210 | // Test error callback 211 | let query = "error=access_denied&error_description=User%20denied%20access"; 212 | let parsed: OAuthCallbackQuery = serde_urlencoded::from_str(query).unwrap(); 213 | 214 | assert_eq!(parsed.error, Some("access_denied".to_string())); 215 | assert_eq!(parsed.error_description, Some("User denied access".to_string())); 216 | assert_eq!(parsed.code, None); 217 | } 218 | } -------------------------------------------------------------------------------- /.kiro/specs/gmail-link-queue/tasks.md: -------------------------------------------------------------------------------- 1 | # Implementation Plan 2 | 3 | - [x] 1. Set up project structure and core interfaces 4 | 5 | - Create Rust backend project with Cargo.toml dependencies (axum, sqlx, tokio, serde, anyhow, thiserror) 6 | - Create Next.js frontend project with TypeScript and Tailwind CSS 7 | - Define core data structures and interfaces for User, Link, Email, and SyncStatus models 8 | - Set up database connection utilities and migration system 9 | - _Requirements: 3.1, 3.2, 3.3_ 10 | 11 | - [x] 2. Implement database schema and models 12 | 13 | - Create SQLite database schema with tables for users, auth_tokens, emails, links, and sync_status 14 | - Implement Rust structs with SQLx derive macros for database operations 15 | - Create database migration scripts and initialization logic 16 | - Write unit tests for database model serialization and basic CRUD operations 17 | - _Requirements: 3.1, 3.2, 3.3_ 18 | 19 | - [x] 3. Build authentication service 20 | 21 | - [x] 3.1 Implement OAuth2 client configuration for Google APIs 22 | 23 | - Set up Google OAuth2 client with proper scopes for Gmail access 24 | - Create authentication URL generation with state parameter for security 25 | - Implement secure token storage in database with encryption 26 | - _Requirements: 1.1, 1.2_ 27 | 28 | - [x] 3.2 Create authentication flow handlers 29 | - Build OAuth callback handler to exchange authorization code for tokens 30 | - Implement automatic token refresh logic with error handling 31 | - Create middleware for validating and refreshing expired tokens 32 | - Write unit tests for authentication flows with mock OAuth responses 33 | - _Requirements: 1.1, 1.2, 1.3, 1.4_ 34 | 35 | - [ ] 4. Develop Gmail API integration service 36 | 37 | - [ ] 4.1 Implement Gmail client wrapper 38 | 39 | - Create Gmail API client with proper authentication headers 40 | - Implement query logic to fetch emails where sender equals recipient 41 | - Add pagination support for large email collections 42 | - Create email content parsing to extract message body and metadata 43 | - _Requirements: 2.1, 2.4_ 44 | 45 | - [ ] 4.2 Build link extraction functionality 46 | - Implement regex-based HTTP/HTTPS link extraction from email content 47 | - Create link metadata fetching (title, favicon) with timeout handling 48 | - Add duplicate link detection within same email and across emails 49 | - Write comprehensive tests for link extraction with various email formats 50 | - _Requirements: 2.2, 3.3_ 51 | 52 | - [ ] 5. Create email sync service with scheduling 53 | 54 | - [ ] 5.1 Implement core sync logic 55 | 56 | - Build email synchronization service that processes batches of emails 57 | - Create database operations to store email metadata and extracted links 58 | - Implement duplicate email detection using Gmail message IDs 59 | - Add sync status tracking with timestamps and error logging 60 | - _Requirements: 2.1, 2.2, 2.3, 2.4, 3.1, 3.2_ 61 | 62 | - [ ] 5.2 Add background scheduling and rate limiting 63 | - Implement periodic sync using Tokio scheduler with configurable intervals 64 | - Create exponential backoff logic for Gmail API rate limit handling 65 | - Add manual sync trigger endpoint with immediate execution 66 | - Build sync status reporting for frontend consumption 67 | - Write integration tests for sync process with mock Gmail responses 68 | - _Requirements: 2.5, 6.1, 6.2, 6.3, 6.4, 6.5_ 69 | 70 | - [ ] 6. Build link management service 71 | 72 | - Implement link retrieval with filtering options (read/unread, date ranges) 73 | - Create link read status update functionality with timestamp tracking 74 | - Add link statistics calculation (total, read, unread counts) 75 | - Build link metadata caching to avoid repeated external requests 76 | - Write unit tests for link management operations 77 | - _Requirements: 4.4, 5.2, 5.3, 5.5_ 78 | 79 | - [ ] 7. Create REST API endpoints 80 | 81 | - [ ] 7.1 Build authentication endpoints 82 | 83 | - Create POST /api/auth/initiate endpoint to start OAuth flow 84 | - Implement POST /api/auth/callback for handling OAuth responses 85 | - Add POST /api/auth/refresh for token refresh operations 86 | - Include proper error responses and status codes for auth failures 87 | - _Requirements: 1.1, 1.2, 1.3, 1.4_ 88 | 89 | - [ ] 7.2 Implement sync and link management endpoints 90 | - Create GET /api/links endpoint with query parameters for filtering 91 | - Build POST /api/links/:id/read endpoint for marking links as read 92 | - Add POST /api/sync/trigger for manual sync initiation 93 | - Implement GET /api/sync/status for sync progress reporting 94 | - Create GET /api/links/stats for link statistics 95 | - Write API integration tests with test database 96 | - _Requirements: 4.1, 4.2, 4.3, 5.1, 5.2, 5.3, 6.2, 6.3, 6.4_ 97 | 98 | - [ ] 8. Develop frontend components and state management 99 | 100 | - [ ] 8.1 Create core UI components 101 | 102 | - Build LinkItem component with favicon display and click handling 103 | - Implement LinkQueue component with grid layout and sorting 104 | - Create authentication flow components (login button, callback handler) 105 | - Add loading states and skeleton screens for better UX 106 | - _Requirements: 4.1, 4.2, 4.3, 4.5_ 107 | 108 | - [ ] 8.2 Implement state management and API integration 109 | - Set up React Query for server state management and caching 110 | - Create custom hooks for authentication, link fetching, and sync operations 111 | - Implement real-time sync status updates with polling or WebSocket 112 | - Add show/hide read links toggle functionality 113 | - Build error boundary components for graceful error handling 114 | - _Requirements: 4.4, 5.4, 6.3, 6.4_ 115 | 116 | - [ ] 9. Add comprehensive error handling 117 | 118 | - [ ] 9.1 Implement backend error handling 119 | 120 | - Create comprehensive error types for different failure scenarios 121 | - Add circuit breaker pattern for Gmail API failures 122 | - Implement retry queues for failed operations with exponential backoff 123 | - Create structured logging for debugging and monitoring 124 | - _Requirements: 7.1, 7.2, 7.3, 7.5_ 125 | 126 | - [ ] 9.2 Build frontend error handling 127 | - Implement global error boundary for unhandled React errors 128 | - Add toast notifications for user-facing error messages 129 | - Create offline state detection and appropriate UI feedback 130 | - Build retry mechanisms for failed API calls with user controls 131 | - _Requirements: 7.1, 7.4_ 132 | 133 | - [ ] 10. Create comprehensive test suite 134 | 135 | - [ ] 10.1 Write backend tests 136 | 137 | - Create unit tests for all service layer components with mocked dependencies 138 | - Build integration tests for API endpoints with test database 139 | - Add Gmail API integration tests with test account or mocks 140 | - Implement database migration and schema validation tests 141 | - _Requirements: All requirements for backend functionality_ 142 | 143 | - [ ] 10.2 Develop frontend tests 144 | - Write component tests for all UI components using React Testing Library 145 | - Create API integration tests using Mock Service Worker (MSW) 146 | - Build end-to-end tests for authentication flow and link management 147 | - Add responsive design tests across different screen sizes 148 | - Test error states and recovery mechanisms 149 | - _Requirements: All requirements for frontend functionality_ 150 | 151 | - [ ] 11. Integrate and wire components together 152 | - Connect frontend authentication flow to backend OAuth endpoints 153 | - Wire link queue display to backend link retrieval API 154 | - Integrate sync status updates between backend scheduler and frontend UI 155 | - Connect link click handling to read status update API 156 | - Test complete user workflow from authentication through link management 157 | - Verify error handling works end-to-end across all components 158 | - _Requirements: All requirements integrated together_ 159 | -------------------------------------------------------------------------------- /.kiro/specs/gmail-link-queue/design.md: -------------------------------------------------------------------------------- 1 | # Design Document 2 | 3 | ## Overview 4 | 5 | The Gmail Link Queue application is a full-stack solution that automatically extracts and manages links from emails a user sends to themselves. The system consists of a Rust backend for robust Gmail API integration and data persistence, paired with a Next.js/React frontend for an intuitive user experience. 6 | 7 | The core workflow involves authenticating with Gmail, periodically syncing emails sent to oneself, extracting HTTP/HTTPS links, storing them locally, and presenting them in a queue-like interface where users can browse and mark links as read. 8 | 9 | ## Architecture 10 | 11 | ### System Architecture 12 | 13 | ```mermaid 14 | graph TB 15 | subgraph "Frontend (Next.js/React)" 16 | UI[Web Interface] 17 | API_CLIENT[API Client] 18 | end 19 | 20 | subgraph "Backend (Rust)" 21 | WEB_SERVER[Web Server/API] 22 | AUTH_SERVICE[Auth Service] 23 | EMAIL_SERVICE[Email Sync Service] 24 | LINK_SERVICE[Link Management Service] 25 | SCHEDULER[Background Scheduler] 26 | end 27 | 28 | subgraph "External Services" 29 | GMAIL[Gmail API] 30 | GOOGLE_AUTH[Google OAuth2] 31 | end 32 | 33 | subgraph "Data Layer" 34 | DATABASE[(SQLite Database)] 35 | end 36 | 37 | UI --> API_CLIENT 38 | API_CLIENT --> WEB_SERVER 39 | WEB_SERVER --> AUTH_SERVICE 40 | WEB_SERVER --> EMAIL_SERVICE 41 | WEB_SERVER --> LINK_SERVICE 42 | SCHEDULER --> EMAIL_SERVICE 43 | AUTH_SERVICE --> GOOGLE_AUTH 44 | EMAIL_SERVICE --> GMAIL 45 | AUTH_SERVICE --> DATABASE 46 | EMAIL_SERVICE --> DATABASE 47 | LINK_SERVICE --> DATABASE 48 | ``` 49 | 50 | ### Technology Stack 51 | 52 | **Backend (Rust):** 53 | - **Web Framework**: Axum for HTTP server and API endpoints 54 | - **Database**: SQLite with SQLx for type-safe database operations 55 | - **Gmail Integration**: Google APIs Client Library for Rust 56 | - **Authentication**: OAuth2 with secure token storage 57 | - **Background Tasks**: Tokio for async scheduling and task management 58 | - **Error Handling**: anyhow for comprehensive error management 59 | 60 | **Frontend (Next.js/React):** 61 | - **Framework**: Next.js 14 with App Router for modern React patterns 62 | - **UI Components**: Tailwind CSS for styling with custom components 63 | - **State Management**: React Query for server state and caching 64 | - **HTTP Client**: Fetch API with custom hooks for backend communication 65 | - **Icons**: Lucide React for consistent iconography 66 | 67 | **Rationale**: Rust provides excellent performance and reliability for backend services, especially for API integrations and data processing. Next.js offers a mature ecosystem for React applications with built-in optimizations. 68 | 69 | ## Components and Interfaces 70 | 71 | ### Backend Components 72 | 73 | #### Authentication Service 74 | ```rust 75 | pub struct AuthService { 76 | oauth_client: OAuth2Client, 77 | token_storage: TokenStorage, 78 | } 79 | 80 | impl AuthService { 81 | pub async fn initiate_auth(&self) -> Result; 82 | pub async fn handle_callback(&self, code: String) -> Result; 83 | pub async fn refresh_token(&self, refresh_token: String) -> Result; 84 | pub async fn get_valid_token(&self, user_id: String) -> Result; 85 | } 86 | ``` 87 | 88 | #### Email Sync Service 89 | ```rust 90 | pub struct EmailSyncService { 91 | gmail_client: GmailClient, 92 | auth_service: Arc, 93 | database: Arc, 94 | } 95 | 96 | impl EmailSyncService { 97 | pub async fn sync_emails(&self, user_id: String) -> Result; 98 | pub async fn extract_links_from_email(&self, email: Email) -> Result>; 99 | pub async fn process_email_batch(&self, emails: Vec) -> Result<()>; 100 | } 101 | ``` 102 | 103 | #### Link Management Service 104 | ```rust 105 | pub struct LinkService { 106 | database: Arc, 107 | } 108 | 109 | impl LinkService { 110 | pub async fn get_links(&self, user_id: String, include_read: bool) -> Result>; 111 | pub async fn mark_link_read(&self, link_id: String, user_id: String) -> Result<()>; 112 | pub async fn get_link_metadata(&self, url: String) -> Result; 113 | } 114 | ``` 115 | 116 | ### Frontend Components 117 | 118 | #### Main Queue Interface 119 | ```typescript 120 | interface LinkQueueProps { 121 | links: Link[]; 122 | onLinkClick: (linkId: string) => void; 123 | showRead: boolean; 124 | onToggleShowRead: () => void; 125 | } 126 | 127 | const LinkQueue: React.FC = ({ ... }) => { 128 | // Renders grid of link icons with metadata 129 | }; 130 | ``` 131 | 132 | #### Link Item Component 133 | ```typescript 134 | interface LinkItemProps { 135 | link: Link; 136 | onClick: (linkId: string) => void; 137 | } 138 | 139 | const LinkItem: React.FC = ({ link, onClick }) => { 140 | // Individual link representation with favicon and metadata 141 | }; 142 | ``` 143 | 144 | ### API Endpoints 145 | 146 | ```typescript 147 | // Authentication 148 | POST /api/auth/initiate // Start OAuth flow 149 | POST /api/auth/callback // Handle OAuth callback 150 | POST /api/auth/refresh // Refresh access token 151 | 152 | // Email Sync 153 | POST /api/sync/trigger // Manual sync trigger 154 | GET /api/sync/status // Get sync status 155 | 156 | // Links 157 | GET /api/links // Get user's links (query: include_read) 158 | POST /api/links/:id/read // Mark link as read 159 | GET /api/links/stats // Get link statistics 160 | ``` 161 | 162 | ## Data Models 163 | 164 | ### Database Schema 165 | 166 | ```sql 167 | -- Users table for authentication data 168 | CREATE TABLE users ( 169 | id TEXT PRIMARY KEY, 170 | email TEXT NOT NULL UNIQUE, 171 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 172 | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP 173 | ); 174 | 175 | -- OAuth tokens storage 176 | CREATE TABLE auth_tokens ( 177 | user_id TEXT PRIMARY KEY, 178 | access_token TEXT NOT NULL, 179 | refresh_token TEXT NOT NULL, 180 | expires_at DATETIME NOT NULL, 181 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 182 | FOREIGN KEY (user_id) REFERENCES users(id) 183 | ); 184 | 185 | -- Email metadata 186 | CREATE TABLE emails ( 187 | id TEXT PRIMARY KEY, 188 | user_id TEXT NOT NULL, 189 | gmail_message_id TEXT NOT NULL UNIQUE, 190 | subject TEXT, 191 | sent_date DATETIME NOT NULL, 192 | processed_at DATETIME DEFAULT CURRENT_TIMESTAMP, 193 | FOREIGN KEY (user_id) REFERENCES users(id) 194 | ); 195 | 196 | -- Extracted links 197 | CREATE TABLE links ( 198 | id TEXT PRIMARY KEY, 199 | user_id TEXT NOT NULL, 200 | email_id TEXT NOT NULL, 201 | url TEXT NOT NULL, 202 | title TEXT, 203 | favicon_url TEXT, 204 | is_read BOOLEAN DEFAULT FALSE, 205 | read_at DATETIME, 206 | extracted_at DATETIME DEFAULT CURRENT_TIMESTAMP, 207 | FOREIGN KEY (user_id) REFERENCES users(id), 208 | FOREIGN KEY (email_id) REFERENCES emails(id) 209 | ); 210 | 211 | -- Sync status tracking 212 | CREATE TABLE sync_status ( 213 | user_id TEXT PRIMARY KEY, 214 | last_sync_at DATETIME, 215 | last_sync_status TEXT, -- 'success', 'error', 'in_progress' 216 | last_error_message TEXT, 217 | next_sync_at DATETIME, 218 | FOREIGN KEY (user_id) REFERENCES users(id) 219 | ); 220 | ``` 221 | 222 | ### Rust Data Models 223 | 224 | ```rust 225 | #[derive(Debug, Clone, Serialize, Deserialize)] 226 | pub struct User { 227 | pub id: String, 228 | pub email: String, 229 | pub created_at: DateTime, 230 | pub updated_at: DateTime, 231 | } 232 | 233 | #[derive(Debug, Clone, Serialize, Deserialize)] 234 | pub struct Link { 235 | pub id: String, 236 | pub user_id: String, 237 | pub email_id: String, 238 | pub url: String, 239 | pub title: Option, 240 | pub favicon_url: Option, 241 | pub is_read: bool, 242 | pub read_at: Option>, 243 | pub extracted_at: DateTime, 244 | } 245 | 246 | #[derive(Debug, Clone, Serialize, Deserialize)] 247 | pub struct SyncStatus { 248 | pub user_id: String, 249 | pub last_sync_at: Option>, 250 | pub status: SyncStatusType, 251 | pub last_error_message: Option, 252 | pub next_sync_at: Option>, 253 | } 254 | ``` 255 | 256 | ## Error Handling 257 | 258 | ### Backend Error Strategy 259 | 260 | ```rust 261 | #[derive(Debug, thiserror::Error)] 262 | pub enum AppError { 263 | #[error("Authentication error: {0}")] 264 | Auth(#[from] AuthError), 265 | 266 | #[error("Gmail API error: {0}")] 267 | Gmail(#[from] GmailError), 268 | 269 | #[error("Database error: {0}")] 270 | Database(#[from] sqlx::Error), 271 | 272 | #[error("Network error: {0}")] 273 | Network(#[from] reqwest::Error), 274 | 275 | #[error("Rate limit exceeded, retry after {retry_after} seconds")] 276 | RateLimit { retry_after: u64 }, 277 | } 278 | ``` 279 | 280 | ### Error Recovery Mechanisms 281 | 282 | 1. **Exponential Backoff**: For Gmail API rate limits and temporary failures 283 | 2. **Circuit Breaker**: Prevent cascading failures when Gmail API is down 284 | 3. **Graceful Degradation**: Show cached data when sync fails 285 | 4. **Retry Queues**: Queue failed operations for later retry 286 | 5. **Comprehensive Logging**: Structured logging for debugging and monitoring 287 | 288 | ### Frontend Error Handling 289 | 290 | ```typescript 291 | interface ErrorBoundaryState { 292 | hasError: boolean; 293 | error?: Error; 294 | } 295 | 296 | // Global error boundary for unhandled errors 297 | // Toast notifications for user-facing errors 298 | // Retry mechanisms for failed API calls 299 | // Offline state detection and handling 300 | ``` 301 | 302 | ## Testing Strategy 303 | 304 | ### Backend Testing 305 | 306 | **Unit Tests:** 307 | - Service layer logic testing with mocked dependencies 308 | - Database operations with test database 309 | - Authentication flow testing with mock OAuth responses 310 | - Link extraction logic with sample email content 311 | 312 | **Integration Tests:** 313 | - End-to-end API endpoint testing 314 | - Gmail API integration with test account 315 | - Database migration and schema validation 316 | - Background sync process testing 317 | 318 | **Test Structure:** 319 | ```rust 320 | #[cfg(test)] 321 | mod tests { 322 | use super::*; 323 | 324 | #[tokio::test] 325 | async fn test_email_sync_service() { 326 | // Test email sync with mock Gmail client 327 | } 328 | 329 | #[tokio::test] 330 | async fn test_link_extraction() { 331 | // Test link extraction from various email formats 332 | } 333 | } 334 | ``` 335 | 336 | ### Frontend Testing 337 | 338 | **Component Tests:** 339 | - Link queue rendering with various data states 340 | - User interaction handling (clicking, marking as read) 341 | - Error state display and recovery 342 | - Loading states and skeleton screens 343 | 344 | **Integration Tests:** 345 | - API integration with mock backend 346 | - Authentication flow testing 347 | - Real-time sync status updates 348 | - Responsive design across devices 349 | 350 | **Test Tools:** 351 | - Jest and React Testing Library for component tests 352 | - MSW (Mock Service Worker) for API mocking 353 | - Playwright for end-to-end testing 354 | 355 | ### Performance Testing 356 | 357 | - Gmail API rate limit handling under load 358 | - Database query performance with large datasets 359 | - Frontend rendering performance with many links 360 | - Memory usage monitoring for long-running sync processes 361 | 362 | **Rationale**: Comprehensive testing ensures reliability, especially important for an application handling user authentication and external API integration. The testing strategy covers both happy paths and error scenarios to ensure robust operation. -------------------------------------------------------------------------------- /backend/src/models/tests.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use chrono::Utc; 3 | use sqlx::SqlitePool; 4 | use anyhow::Result; 5 | 6 | // Helper function to create an in-memory test database 7 | async fn create_test_db() -> Result { 8 | let pool = SqlitePool::connect("sqlite::memory:").await?; 9 | 10 | // Run migrations 11 | sqlx::migrate!("./migrations").run(&pool).await?; 12 | 13 | Ok(pool) 14 | } 15 | 16 | #[cfg(test)] 17 | mod user_tests { 18 | use super::*; 19 | 20 | #[tokio::test] 21 | async fn test_user_creation_and_retrieval() -> Result<()> { 22 | let pool = create_test_db().await?; 23 | 24 | let user = User::new("test@example.com".to_string()); 25 | let user_id = user.id.clone(); 26 | 27 | // Test creation 28 | user.create(&pool).await?; 29 | 30 | // Test find by id 31 | let found_user = User::find_by_id(&pool, &user_id).await?; 32 | assert!(found_user.is_some()); 33 | let found_user = found_user.unwrap(); 34 | assert_eq!(found_user.email, "test@example.com"); 35 | assert_eq!(found_user.id, user_id); 36 | 37 | // Test find by email 38 | let found_by_email = User::find_by_email(&pool, "test@example.com").await?; 39 | assert!(found_by_email.is_some()); 40 | assert_eq!(found_by_email.unwrap().id, user_id); 41 | 42 | Ok(()) 43 | } 44 | 45 | #[tokio::test] 46 | async fn test_user_update() -> Result<()> { 47 | let pool = create_test_db().await?; 48 | 49 | let mut user = User::new("test@example.com".to_string()); 50 | user.create(&pool).await?; 51 | 52 | // Update email 53 | user.email = "updated@example.com".to_string(); 54 | user.update(&pool).await?; 55 | 56 | // Verify update 57 | let updated_user = User::find_by_id(&pool, &user.id).await?; 58 | assert!(updated_user.is_some()); 59 | assert_eq!(updated_user.unwrap().email, "updated@example.com"); 60 | 61 | Ok(()) 62 | } 63 | 64 | #[tokio::test] 65 | async fn test_user_deletion() -> Result<()> { 66 | let pool = create_test_db().await?; 67 | 68 | let user = User::new("test@example.com".to_string()); 69 | let user_id = user.id.clone(); 70 | user.create(&pool).await?; 71 | 72 | // Delete user 73 | User::delete(&pool, &user_id).await?; 74 | 75 | // Verify deletion 76 | let deleted_user = User::find_by_id(&pool, &user_id).await?; 77 | assert!(deleted_user.is_none()); 78 | 79 | Ok(()) 80 | } 81 | 82 | #[tokio::test] 83 | async fn test_user_serialization() { 84 | let user = User::new("test@example.com".to_string()); 85 | 86 | // Test serialization 87 | let json = serde_json::to_string(&user).unwrap(); 88 | assert!(json.contains("test@example.com")); 89 | 90 | // Test deserialization 91 | let deserialized: User = serde_json::from_str(&json).unwrap(); 92 | assert_eq!(deserialized.email, user.email); 93 | assert_eq!(deserialized.id, user.id); 94 | } 95 | } 96 | 97 | #[cfg(test)] 98 | mod auth_token_tests { 99 | use super::*; 100 | 101 | #[tokio::test] 102 | async fn test_auth_token_creation_and_retrieval() -> Result<()> { 103 | let pool = create_test_db().await?; 104 | 105 | // Create a user first 106 | let user = User::new("test@example.com".to_string()); 107 | user.create(&pool).await?; 108 | 109 | let expires_at = Utc::now() + chrono::Duration::hours(1); 110 | let token = AuthToken::new( 111 | user.id.clone(), 112 | "access_token".to_string(), 113 | "refresh_token".to_string(), 114 | expires_at, 115 | ); 116 | 117 | // Test creation 118 | token.create(&pool).await?; 119 | 120 | // Test retrieval 121 | let found_token = AuthToken::find_by_user_id(&pool, &user.id).await?; 122 | assert!(found_token.is_some()); 123 | let found_token = found_token.unwrap(); 124 | assert_eq!(found_token.access_token, "access_token"); 125 | assert_eq!(found_token.refresh_token, "refresh_token"); 126 | 127 | Ok(()) 128 | } 129 | 130 | #[tokio::test] 131 | async fn test_auth_token_update() -> Result<()> { 132 | let pool = create_test_db().await?; 133 | 134 | let user = User::new("test@example.com".to_string()); 135 | user.create(&pool).await?; 136 | 137 | let expires_at = Utc::now() + chrono::Duration::hours(1); 138 | let mut token = AuthToken::new( 139 | user.id.clone(), 140 | "access_token".to_string(), 141 | "refresh_token".to_string(), 142 | expires_at, 143 | ); 144 | token.create(&pool).await?; 145 | 146 | // Update token 147 | token.update_tokens("new_access_token".to_string(), Utc::now() + chrono::Duration::hours(2)); 148 | token.update(&pool).await?; 149 | 150 | // Verify update 151 | let updated_token = AuthToken::find_by_user_id(&pool, &user.id).await?; 152 | assert!(updated_token.is_some()); 153 | assert_eq!(updated_token.unwrap().access_token, "new_access_token"); 154 | 155 | Ok(()) 156 | } 157 | 158 | #[tokio::test] 159 | async fn test_auth_token_expiration() { 160 | let past_time = Utc::now() - chrono::Duration::hours(1); 161 | let future_time = Utc::now() + chrono::Duration::hours(1); 162 | 163 | let expired_token = AuthToken::new( 164 | "user_id".to_string(), 165 | "access_token".to_string(), 166 | "refresh_token".to_string(), 167 | past_time, 168 | ); 169 | 170 | let valid_token = AuthToken::new( 171 | "user_id".to_string(), 172 | "access_token".to_string(), 173 | "refresh_token".to_string(), 174 | future_time, 175 | ); 176 | 177 | assert!(expired_token.is_expired()); 178 | assert!(!valid_token.is_expired()); 179 | } 180 | } 181 | 182 | #[cfg(test)] 183 | mod email_tests { 184 | use super::*; 185 | 186 | #[tokio::test] 187 | async fn test_email_creation_and_retrieval() -> Result<()> { 188 | let pool = create_test_db().await?; 189 | 190 | let user = User::new("test@example.com".to_string()); 191 | user.create(&pool).await?; 192 | 193 | let sent_date = Utc::now() - chrono::Duration::hours(1); 194 | let email = Email::new( 195 | user.id.clone(), 196 | "gmail_msg_123".to_string(), 197 | Some("Test Subject".to_string()), 198 | sent_date, 199 | ); 200 | let email_id = email.id.clone(); 201 | 202 | // Test creation 203 | email.create(&pool).await?; 204 | 205 | // Test find by id 206 | let found_email = Email::find_by_id(&pool, &email_id).await?; 207 | assert!(found_email.is_some()); 208 | let found_email = found_email.unwrap(); 209 | assert_eq!(found_email.subject, Some("Test Subject".to_string())); 210 | assert_eq!(found_email.gmail_message_id, "gmail_msg_123"); 211 | 212 | // Test find by gmail message id 213 | let found_by_gmail_id = Email::find_by_gmail_message_id(&pool, "gmail_msg_123").await?; 214 | assert!(found_by_gmail_id.is_some()); 215 | assert_eq!(found_by_gmail_id.unwrap().id, email_id); 216 | 217 | Ok(()) 218 | } 219 | 220 | #[tokio::test] 221 | async fn test_email_find_by_user_id() -> Result<()> { 222 | let pool = create_test_db().await?; 223 | 224 | let user = User::new("test@example.com".to_string()); 225 | user.create(&pool).await?; 226 | 227 | // Create multiple emails 228 | let email1 = Email::new( 229 | user.id.clone(), 230 | "gmail_msg_1".to_string(), 231 | Some("Subject 1".to_string()), 232 | Utc::now() - chrono::Duration::hours(2), 233 | ); 234 | let email2 = Email::new( 235 | user.id.clone(), 236 | "gmail_msg_2".to_string(), 237 | Some("Subject 2".to_string()), 238 | Utc::now() - chrono::Duration::hours(1), 239 | ); 240 | 241 | email1.create(&pool).await?; 242 | email2.create(&pool).await?; 243 | 244 | // Test retrieval by user id 245 | let user_emails = Email::find_by_user_id(&pool, &user.id).await?; 246 | assert_eq!(user_emails.len(), 2); 247 | // Should be ordered by sent_date DESC 248 | assert_eq!(user_emails[0].gmail_message_id, "gmail_msg_2"); 249 | assert_eq!(user_emails[1].gmail_message_id, "gmail_msg_1"); 250 | 251 | Ok(()) 252 | } 253 | } 254 | 255 | #[cfg(test)] 256 | mod link_tests { 257 | use super::*; 258 | 259 | #[tokio::test] 260 | async fn test_link_creation_and_retrieval() -> Result<()> { 261 | let pool = create_test_db().await?; 262 | 263 | let user = User::new("test@example.com".to_string()); 264 | user.create(&pool).await?; 265 | 266 | let email = Email::new( 267 | user.id.clone(), 268 | "gmail_msg_123".to_string(), 269 | Some("Test Subject".to_string()), 270 | Utc::now(), 271 | ); 272 | email.create(&pool).await?; 273 | 274 | let link = Link::new( 275 | user.id.clone(), 276 | email.id.clone(), 277 | "https://example.com".to_string(), 278 | ); 279 | let link_id = link.id.clone(); 280 | 281 | // Test creation 282 | link.create(&pool).await?; 283 | 284 | // Test find by id 285 | let found_link = Link::find_by_id(&pool, &link_id).await?; 286 | assert!(found_link.is_some()); 287 | let found_link = found_link.unwrap(); 288 | assert_eq!(found_link.url, "https://example.com"); 289 | assert!(!found_link.is_read); 290 | assert!(found_link.read_at.is_none()); 291 | 292 | Ok(()) 293 | } 294 | 295 | #[tokio::test] 296 | async fn test_link_mark_as_read() -> Result<()> { 297 | let pool = create_test_db().await?; 298 | 299 | let user = User::new("test@example.com".to_string()); 300 | user.create(&pool).await?; 301 | 302 | let email = Email::new( 303 | user.id.clone(), 304 | "gmail_msg_123".to_string(), 305 | Some("Test Subject".to_string()), 306 | Utc::now(), 307 | ); 308 | email.create(&pool).await?; 309 | 310 | let mut link = Link::new( 311 | user.id.clone(), 312 | email.id.clone(), 313 | "https://example.com".to_string(), 314 | ); 315 | link.create(&pool).await?; 316 | 317 | // Mark as read using instance method 318 | link.mark_as_read(); 319 | link.update(&pool).await?; 320 | 321 | // Verify 322 | let updated_link = Link::find_by_id(&pool, &link.id).await?; 323 | assert!(updated_link.is_some()); 324 | let updated_link = updated_link.unwrap(); 325 | assert!(updated_link.is_read); 326 | assert!(updated_link.read_at.is_some()); 327 | 328 | Ok(()) 329 | } 330 | 331 | #[tokio::test] 332 | async fn test_link_mark_as_read_by_id() -> Result<()> { 333 | let pool = create_test_db().await?; 334 | 335 | let user = User::new("test@example.com".to_string()); 336 | user.create(&pool).await?; 337 | 338 | let email = Email::new( 339 | user.id.clone(), 340 | "gmail_msg_123".to_string(), 341 | Some("Test Subject".to_string()), 342 | Utc::now(), 343 | ); 344 | email.create(&pool).await?; 345 | 346 | let link = Link::new( 347 | user.id.clone(), 348 | email.id.clone(), 349 | "https://example.com".to_string(), 350 | ); 351 | link.create(&pool).await?; 352 | 353 | // Mark as read using static method 354 | Link::mark_as_read_by_id(&pool, &link.id).await?; 355 | 356 | // Verify 357 | let updated_link = Link::find_by_id(&pool, &link.id).await?; 358 | assert!(updated_link.is_some()); 359 | let updated_link = updated_link.unwrap(); 360 | assert!(updated_link.is_read); 361 | assert!(updated_link.read_at.is_some()); 362 | 363 | Ok(()) 364 | } 365 | 366 | #[tokio::test] 367 | async fn test_link_find_by_user_id_filtering() -> Result<()> { 368 | let pool = create_test_db().await?; 369 | 370 | let user = User::new("test@example.com".to_string()); 371 | user.create(&pool).await?; 372 | 373 | let email = Email::new( 374 | user.id.clone(), 375 | "gmail_msg_123".to_string(), 376 | Some("Test Subject".to_string()), 377 | Utc::now(), 378 | ); 379 | email.create(&pool).await?; 380 | 381 | // Create read and unread links 382 | let mut read_link = Link::new( 383 | user.id.clone(), 384 | email.id.clone(), 385 | "https://read.com".to_string(), 386 | ); 387 | read_link.mark_as_read(); 388 | read_link.create(&pool).await?; 389 | 390 | let unread_link = Link::new( 391 | user.id.clone(), 392 | email.id.clone(), 393 | "https://unread.com".to_string(), 394 | ); 395 | unread_link.create(&pool).await?; 396 | 397 | // Test including read links 398 | let all_links = Link::find_by_user_id(&pool, &user.id, true).await?; 399 | assert_eq!(all_links.len(), 2); 400 | 401 | // Test excluding read links 402 | let unread_links = Link::find_by_user_id(&pool, &user.id, false).await?; 403 | assert_eq!(unread_links.len(), 1); 404 | assert_eq!(unread_links[0].url, "https://unread.com"); 405 | 406 | Ok(()) 407 | } 408 | } 409 | 410 | #[cfg(test)] 411 | mod sync_status_tests { 412 | use super::*; 413 | 414 | #[tokio::test] 415 | async fn test_sync_status_creation_and_retrieval() -> Result<()> { 416 | let pool = create_test_db().await?; 417 | 418 | let user = User::new("test@example.com".to_string()); 419 | user.create(&pool).await?; 420 | 421 | let sync_status = SyncStatus::new(user.id.clone()); 422 | 423 | // Test creation 424 | sync_status.create(&pool).await?; 425 | 426 | // Test retrieval 427 | let found_status = SyncStatus::find_by_user_id(&pool, &user.id).await?; 428 | assert!(found_status.is_some()); 429 | let found_status = found_status.unwrap(); 430 | assert_eq!(found_status.user_id, user.id); 431 | assert!(found_status.last_sync_at.is_none()); 432 | 433 | Ok(()) 434 | } 435 | 436 | #[tokio::test] 437 | async fn test_sync_status_update() -> Result<()> { 438 | let pool = create_test_db().await?; 439 | 440 | let user = User::new("test@example.com".to_string()); 441 | user.create(&pool).await?; 442 | 443 | let mut sync_status = SyncStatus::new(user.id.clone()); 444 | sync_status.create(&pool).await?; 445 | 446 | // Update status 447 | sync_status.update_status(SyncStatusType::Success, None); 448 | sync_status.update(&pool).await?; 449 | 450 | // Verify update 451 | let updated_status = SyncStatus::find_by_user_id(&pool, &user.id).await?; 452 | assert!(updated_status.is_some()); 453 | let updated_status = updated_status.unwrap(); 454 | assert_eq!(updated_status.last_sync_status, Some("success".to_string())); 455 | assert!(updated_status.last_sync_at.is_some()); 456 | 457 | Ok(()) 458 | } 459 | 460 | #[tokio::test] 461 | async fn test_sync_status_upsert() -> Result<()> { 462 | let pool = create_test_db().await?; 463 | 464 | let user = User::new("test@example.com".to_string()); 465 | user.create(&pool).await?; 466 | 467 | let mut sync_status = SyncStatus::new(user.id.clone()); 468 | sync_status.update_status(SyncStatusType::Success, None); 469 | 470 | // Test upsert (insert) 471 | sync_status.upsert(&pool).await?; 472 | 473 | let found_status = SyncStatus::find_by_user_id(&pool, &user.id).await?; 474 | assert!(found_status.is_some()); 475 | assert_eq!(found_status.unwrap().last_sync_status, Some("success".to_string())); 476 | 477 | // Test upsert (update) 478 | sync_status.update_status(SyncStatusType::Error, Some("Test error".to_string())); 479 | sync_status.upsert(&pool).await?; 480 | 481 | let updated_status = SyncStatus::find_by_user_id(&pool, &user.id).await?; 482 | assert!(updated_status.is_some()); 483 | let updated_status = updated_status.unwrap(); 484 | assert_eq!(updated_status.last_sync_status, Some("error".to_string())); 485 | assert_eq!(updated_status.last_error_message, Some("Test error".to_string())); 486 | 487 | Ok(()) 488 | } 489 | 490 | #[tokio::test] 491 | async fn test_sync_status_type_display() { 492 | assert_eq!(SyncStatusType::Success.to_string(), "success"); 493 | assert_eq!(SyncStatusType::Error.to_string(), "error"); 494 | assert_eq!(SyncStatusType::InProgress.to_string(), "in_progress"); 495 | } 496 | } -------------------------------------------------------------------------------- /backend/src/services/auth.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use base64::{engine::general_purpose, Engine as _}; 3 | use chrono::{DateTime, Duration, Utc}; 4 | use oauth2::{ 5 | basic::BasicClient, AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, 6 | PkceCodeChallenge, PkceCodeVerifier, RedirectUrl, Scope, TokenResponse, TokenUrl, 7 | }; 8 | use rand::{distributions::Alphanumeric, Rng}; 9 | use serde::{Deserialize, Serialize}; 10 | use sqlx::SqlitePool; 11 | use std::collections::HashMap; 12 | use std::sync::Arc; 13 | use tokio::sync::RwLock; 14 | use crate::models::auth_token::AuthToken; 15 | use crate::models::user::User; 16 | 17 | #[derive(Debug, Clone, Serialize, Deserialize)] 18 | pub struct AuthConfig { 19 | pub client_id: String, 20 | pub client_secret: String, 21 | pub redirect_uri: String, 22 | pub auth_url: String, 23 | pub token_url: String, 24 | } 25 | 26 | impl Default for AuthConfig { 27 | fn default() -> Self { 28 | Self { 29 | client_id: std::env::var("GOOGLE_CLIENT_ID") 30 | .expect("GOOGLE_CLIENT_ID must be set"), 31 | client_secret: std::env::var("GOOGLE_CLIENT_SECRET") 32 | .expect("GOOGLE_CLIENT_SECRET must be set"), 33 | redirect_uri: std::env::var("GOOGLE_REDIRECT_URI") 34 | .unwrap_or_else(|_| "http://localhost:3000/api/auth/callback".to_string()), 35 | auth_url: "https://accounts.google.com/o/oauth2/v2/auth".to_string(), 36 | token_url: "https://www.googleapis.com/oauth2/v4/token".to_string(), 37 | } 38 | } 39 | } 40 | 41 | #[derive(Debug, Clone, Serialize, Deserialize)] 42 | pub struct AuthState { 43 | pub csrf_token: String, 44 | pub pkce_verifier: String, 45 | pub user_id: Option, 46 | pub created_at: DateTime, 47 | } 48 | 49 | #[derive(Debug, Clone, Serialize, Deserialize)] 50 | pub struct TokenSet { 51 | pub access_token: String, 52 | pub refresh_token: String, 53 | pub expires_at: DateTime, 54 | pub scope: Option, 55 | } 56 | 57 | pub struct AuthService { 58 | config: AuthConfig, 59 | oauth_client: BasicClient, 60 | db_pool: Arc, 61 | // In-memory state storage for CSRF tokens and PKCE verifiers 62 | // In production, this should be Redis or similar 63 | auth_states: Arc>>, 64 | } 65 | 66 | impl AuthService { 67 | pub fn new(db_pool: Arc) -> Result { 68 | let config = AuthConfig::default(); 69 | 70 | let oauth_client = BasicClient::new( 71 | ClientId::new(config.client_id.clone()), 72 | Some(ClientSecret::new(config.client_secret.clone())), 73 | AuthUrl::new(config.auth_url.clone())?, 74 | Some(TokenUrl::new(config.token_url.clone())?), 75 | ) 76 | .set_redirect_uri(RedirectUrl::new(config.redirect_uri.clone())?); 77 | 78 | Ok(Self { 79 | config, 80 | oauth_client, 81 | db_pool, 82 | auth_states: Arc::new(RwLock::new(HashMap::new())), 83 | }) 84 | } 85 | 86 | /// Generate authentication URL with CSRF protection and PKCE 87 | pub async fn generate_auth_url(&self, user_id: Option) -> Result<(String, String)> { 88 | // Generate CSRF token 89 | let csrf_token = self.generate_secure_token(); 90 | 91 | // Generate PKCE challenge 92 | let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); 93 | 94 | // Store auth state 95 | let auth_state = AuthState { 96 | csrf_token: csrf_token.clone(), 97 | pkce_verifier: pkce_verifier.secret().clone(), 98 | user_id, 99 | created_at: Utc::now(), 100 | }; 101 | 102 | { 103 | let mut states = self.auth_states.write().await; 104 | states.insert(csrf_token.clone(), auth_state); 105 | } 106 | 107 | // Clean up expired states (older than 10 minutes) 108 | self.cleanup_expired_states().await; 109 | 110 | // Build authorization URL 111 | let (auth_url, _) = self 112 | .oauth_client 113 | .authorize_url(|| CsrfToken::new(csrf_token.clone())) 114 | .add_scope(Scope::new("https://www.googleapis.com/auth/gmail.readonly".to_string())) 115 | .add_scope(Scope::new("https://www.googleapis.com/auth/userinfo.email".to_string())) 116 | .add_scope(Scope::new("https://www.googleapis.com/auth/userinfo.profile".to_string())) 117 | .set_pkce_challenge(pkce_challenge) 118 | .add_extra_param("access_type", "offline") 119 | .add_extra_param("prompt", "consent") 120 | .url(); 121 | 122 | Ok((auth_url.to_string(), csrf_token)) 123 | } 124 | 125 | /// Validate CSRF token and retrieve auth state 126 | pub async fn validate_and_get_auth_state(&self, csrf_token: &str) -> Result { 127 | let mut states = self.auth_states.write().await; 128 | 129 | let auth_state = states 130 | .remove(csrf_token) 131 | .ok_or_else(|| anyhow!("Invalid or expired CSRF token"))?; 132 | 133 | // Check if state is expired (10 minutes) 134 | if Utc::now() - auth_state.created_at > Duration::minutes(10) { 135 | return Err(anyhow!("Auth state expired")); 136 | } 137 | 138 | Ok(auth_state) 139 | } 140 | 141 | /// Store encrypted tokens in database 142 | pub async fn store_tokens(&self, user_id: &str, token_set: &TokenSet) -> Result<()> { 143 | // Encrypt tokens before storing 144 | let encrypted_access_token = self.encrypt_token(&token_set.access_token)?; 145 | let encrypted_refresh_token = self.encrypt_token(&token_set.refresh_token)?; 146 | 147 | let auth_token = AuthToken::new( 148 | user_id.to_string(), 149 | encrypted_access_token, 150 | encrypted_refresh_token, 151 | token_set.expires_at, 152 | ); 153 | 154 | // Check if token already exists 155 | if let Some(existing_token) = AuthToken::find_by_user_id(&self.db_pool, user_id).await? { 156 | // Update existing token 157 | let mut updated_token = existing_token; 158 | updated_token.access_token = auth_token.access_token; 159 | updated_token.refresh_token = auth_token.refresh_token; 160 | updated_token.expires_at = auth_token.expires_at; 161 | updated_token.update(&self.db_pool).await?; 162 | } else { 163 | // Create new token 164 | auth_token.create(&self.db_pool).await?; 165 | } 166 | 167 | Ok(()) 168 | } 169 | 170 | /// Retrieve and decrypt tokens from database 171 | pub async fn get_tokens(&self, user_id: &str) -> Result> { 172 | if let Some(auth_token) = AuthToken::find_by_user_id(&self.db_pool, user_id).await? { 173 | let access_token = self.decrypt_token(&auth_token.access_token)?; 174 | let refresh_token = self.decrypt_token(&auth_token.refresh_token)?; 175 | 176 | Ok(Some(TokenSet { 177 | access_token, 178 | refresh_token, 179 | expires_at: auth_token.expires_at, 180 | scope: None, 181 | })) 182 | } else { 183 | Ok(None) 184 | } 185 | } 186 | 187 | /// Check if user has valid (non-expired) tokens 188 | pub async fn has_valid_tokens(&self, user_id: &str) -> Result { 189 | if let Some(token_set) = self.get_tokens(user_id).await? { 190 | Ok(Utc::now() < token_set.expires_at) 191 | } else { 192 | Ok(false) 193 | } 194 | } 195 | 196 | /// Generate a cryptographically secure random token 197 | fn generate_secure_token(&self) -> String { 198 | rand::thread_rng() 199 | .sample_iter(&Alphanumeric) 200 | .take(32) 201 | .map(char::from) 202 | .collect() 203 | } 204 | 205 | /// Simple encryption for tokens (in production, use proper encryption) 206 | fn encrypt_token(&self, token: &str) -> Result { 207 | // For demo purposes, using base64 encoding 208 | // In production, use proper encryption like AES-GCM 209 | let encryption_key = std::env::var("TOKEN_ENCRYPTION_KEY") 210 | .unwrap_or_else(|_| "default-key-change-in-production".to_string()); 211 | 212 | let combined = format!("{}:{}", encryption_key, token); 213 | Ok(general_purpose::STANDARD.encode(combined.as_bytes())) 214 | } 215 | 216 | /// Simple decryption for tokens 217 | fn decrypt_token(&self, encrypted_token: &str) -> Result { 218 | let encryption_key = std::env::var("TOKEN_ENCRYPTION_KEY") 219 | .unwrap_or_else(|_| "default-key-change-in-production".to_string()); 220 | 221 | let decoded = general_purpose::STANDARD.decode(encrypted_token)?; 222 | let combined = String::from_utf8(decoded)?; 223 | 224 | let parts: Vec<&str> = combined.splitn(2, ':').collect(); 225 | if parts.len() != 2 || parts[0] != encryption_key { 226 | return Err(anyhow!("Invalid encrypted token")); 227 | } 228 | 229 | Ok(parts[1].to_string()) 230 | } 231 | 232 | /// Clean up expired auth states 233 | async fn cleanup_expired_states(&self) { 234 | let mut states = self.auth_states.write().await; 235 | let now = Utc::now(); 236 | 237 | states.retain(|_, state| { 238 | now - state.created_at <= Duration::minutes(10) 239 | }); 240 | } 241 | 242 | /// Handle OAuth callback and exchange authorization code for tokens 243 | pub async fn handle_oauth_callback( 244 | &self, 245 | authorization_code: &str, 246 | csrf_token: &str, 247 | ) -> Result<(TokenSet, String)> { 248 | // Validate CSRF token and get auth state 249 | let auth_state = self.validate_and_get_auth_state(csrf_token).await?; 250 | 251 | // Exchange authorization code for tokens 252 | let token_result = self 253 | .oauth_client 254 | .exchange_code(AuthorizationCode::new(authorization_code.to_string())) 255 | .set_pkce_verifier(PkceCodeVerifier::new(auth_state.pkce_verifier)) 256 | .request_async(oauth2::reqwest::async_http_client) 257 | .await 258 | .map_err(|e| anyhow!("Failed to exchange authorization code: {}", e))?; 259 | 260 | // Calculate token expiration 261 | let expires_at = Utc::now() + Duration::seconds( 262 | token_result.expires_in() 263 | .map(|d| d.as_secs() as i64) 264 | .unwrap_or(3600) // Default to 1 hour if not specified 265 | ); 266 | 267 | let token_set = TokenSet { 268 | access_token: token_result.access_token().secret().clone(), 269 | refresh_token: token_result 270 | .refresh_token() 271 | .map(|rt| rt.secret().clone()) 272 | .unwrap_or_else(|| { 273 | // If no refresh token is provided, we'll use a placeholder 274 | // In a real app, you might want to force re-authentication 275 | println!("Warning: No refresh token received from Google OAuth"); 276 | "no_refresh_token".to_string() 277 | }), 278 | expires_at, 279 | scope: token_result.scopes().map(|scopes| { 280 | scopes 281 | .iter() 282 | .map(|s| s.to_string()) 283 | .collect::>() 284 | .join(" ") 285 | }), 286 | }; 287 | 288 | // Get user info to create/find user 289 | let user_email = self.get_user_email(&token_set.access_token).await?; 290 | let user_id = self.ensure_user_exists(&user_email).await?; 291 | 292 | // Store tokens 293 | self.store_tokens(&user_id, &token_set).await?; 294 | 295 | Ok((token_set, user_id)) 296 | } 297 | 298 | /// Refresh access token using refresh token 299 | pub async fn refresh_access_token(&self, user_id: &str) -> Result { 300 | // Get current tokens 301 | let current_tokens = self 302 | .get_tokens(user_id) 303 | .await? 304 | .ok_or_else(|| anyhow!("No tokens found for user"))?; 305 | 306 | // Clone the refresh token before using it 307 | let refresh_token_clone = current_tokens.refresh_token.clone(); 308 | 309 | // Use refresh token to get new access token 310 | let token_result = self 311 | .oauth_client 312 | .exchange_refresh_token(&oauth2::RefreshToken::new(current_tokens.refresh_token)) 313 | .request_async(oauth2::reqwest::async_http_client) 314 | .await 315 | .map_err(|e| anyhow!("Failed to refresh token: {}", e))?; 316 | 317 | // Calculate new expiration 318 | let expires_at = Utc::now() + Duration::seconds( 319 | token_result.expires_in() 320 | .map(|d| d.as_secs() as i64) 321 | .unwrap_or(3600) 322 | ); 323 | 324 | let new_token_set = TokenSet { 325 | access_token: token_result.access_token().secret().clone(), 326 | refresh_token: token_result 327 | .refresh_token() 328 | .map(|rt| rt.secret().clone()) 329 | .unwrap_or(refresh_token_clone), // Keep old refresh token if new one not provided 330 | expires_at, 331 | scope: current_tokens.scope, 332 | }; 333 | 334 | // Store updated tokens 335 | self.store_tokens(user_id, &new_token_set).await?; 336 | 337 | Ok(new_token_set) 338 | } 339 | 340 | /// Get valid access token, refreshing if necessary 341 | pub async fn get_valid_access_token(&self, user_id: &str) -> Result { 342 | // Check if we have valid tokens 343 | if self.has_valid_tokens(user_id).await? { 344 | let tokens = self.get_tokens(user_id).await?.unwrap(); 345 | return Ok(tokens.access_token); 346 | } 347 | 348 | // Try to refresh tokens 349 | let refreshed_tokens = self.refresh_access_token(user_id).await?; 350 | Ok(refreshed_tokens.access_token) 351 | } 352 | 353 | /// Get user email from Google using access token 354 | async fn get_user_email(&self, access_token: &str) -> Result { 355 | let client = reqwest::Client::new(); 356 | let response = client 357 | .get("https://www.googleapis.com/oauth2/v2/userinfo") 358 | .bearer_auth(access_token) 359 | .send() 360 | .await?; 361 | 362 | if !response.status().is_success() { 363 | return Err(anyhow!("Failed to get user info: {}", response.status())); 364 | } 365 | 366 | let user_info: serde_json::Value = response.json().await?; 367 | let email = user_info["email"] 368 | .as_str() 369 | .ok_or_else(|| anyhow!("No email in user info"))?; 370 | 371 | Ok(email.to_string()) 372 | } 373 | 374 | /// Ensure user exists in database, create if not 375 | async fn ensure_user_exists(&self, email: &str) -> Result { 376 | // Try to find existing user 377 | if let Some(user) = User::find_by_email(&self.db_pool, email).await? { 378 | return Ok(user.id); 379 | } 380 | 381 | // Create new user 382 | let user = User::new(email.to_string()); 383 | user.create(&self.db_pool).await?; 384 | Ok(user.id) 385 | } 386 | 387 | /// Get OAuth client for token exchange operations 388 | pub fn get_oauth_client(&self) -> &BasicClient { 389 | &self.oauth_client 390 | } 391 | } 392 | 393 | #[cfg(test)] 394 | mod tests { 395 | use super::*; 396 | use sqlx::SqlitePool; 397 | use std::env; 398 | 399 | async fn setup_test_db() -> SqlitePool { 400 | let pool = SqlitePool::connect(":memory:").await.unwrap(); 401 | 402 | // Run migrations 403 | sqlx::query( 404 | r#" 405 | CREATE TABLE users ( 406 | id TEXT PRIMARY KEY, 407 | email TEXT NOT NULL UNIQUE, 408 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 409 | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP 410 | ); 411 | 412 | CREATE TABLE auth_tokens ( 413 | user_id TEXT PRIMARY KEY, 414 | access_token TEXT NOT NULL, 415 | refresh_token TEXT NOT NULL, 416 | expires_at DATETIME NOT NULL, 417 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 418 | FOREIGN KEY (user_id) REFERENCES users(id) 419 | ); 420 | "#, 421 | ) 422 | .execute(&pool) 423 | .await 424 | .unwrap(); 425 | 426 | pool 427 | } 428 | 429 | #[tokio::test] 430 | async fn test_auth_service_creation() { 431 | // Set required environment variables for test 432 | env::set_var("GOOGLE_CLIENT_ID", "test_client_id"); 433 | env::set_var("GOOGLE_CLIENT_SECRET", "test_client_secret"); 434 | 435 | let pool = setup_test_db().await; 436 | let auth_service = AuthService::new(Arc::new(pool)); 437 | 438 | assert!(auth_service.is_ok()); 439 | } 440 | 441 | #[tokio::test] 442 | async fn test_generate_auth_url() { 443 | env::set_var("GOOGLE_CLIENT_ID", "test_client_id"); 444 | env::set_var("GOOGLE_CLIENT_SECRET", "test_client_secret"); 445 | 446 | let pool = setup_test_db().await; 447 | let auth_service = AuthService::new(Arc::new(pool)).unwrap(); 448 | 449 | let (auth_url, csrf_token) = auth_service.generate_auth_url(None).await.unwrap(); 450 | 451 | assert!(!auth_url.is_empty()); 452 | assert!(!csrf_token.is_empty()); 453 | assert!(auth_url.contains("accounts.google.com")); 454 | assert!(auth_url.contains("gmail.readonly")); 455 | } 456 | 457 | #[tokio::test] 458 | async fn test_token_encryption_decryption() { 459 | env::set_var("GOOGLE_CLIENT_ID", "test_client_id"); 460 | env::set_var("GOOGLE_CLIENT_SECRET", "test_client_secret"); 461 | 462 | let pool = setup_test_db().await; 463 | let auth_service = AuthService::new(Arc::new(pool)).unwrap(); 464 | 465 | let original_token = "test_access_token_12345"; 466 | let encrypted = auth_service.encrypt_token(original_token).unwrap(); 467 | let decrypted = auth_service.decrypt_token(&encrypted).unwrap(); 468 | 469 | assert_eq!(original_token, decrypted); 470 | assert_ne!(original_token, encrypted); 471 | } 472 | 473 | #[tokio::test] 474 | async fn test_csrf_token_validation() { 475 | env::set_var("GOOGLE_CLIENT_ID", "test_client_id"); 476 | env::set_var("GOOGLE_CLIENT_SECRET", "test_client_secret"); 477 | 478 | let pool = setup_test_db().await; 479 | let auth_service = AuthService::new(Arc::new(pool)).unwrap(); 480 | 481 | let (_, csrf_token) = auth_service.generate_auth_url(None).await.unwrap(); 482 | let auth_state = auth_service.validate_and_get_auth_state(&csrf_token).await.unwrap(); 483 | 484 | assert_eq!(auth_state.csrf_token, csrf_token); 485 | 486 | // Token should be consumed and not usable again 487 | let result = auth_service.validate_and_get_auth_state(&csrf_token).await; 488 | assert!(result.is_err()); 489 | } 490 | 491 | #[tokio::test] 492 | async fn test_token_storage_and_retrieval() { 493 | env::set_var("GOOGLE_CLIENT_ID", "test_client_id"); 494 | env::set_var("GOOGLE_CLIENT_SECRET", "test_client_secret"); 495 | 496 | let pool = setup_test_db().await; 497 | let auth_service = AuthService::new(Arc::new(pool)).unwrap(); 498 | 499 | // Create a test user first 500 | let user = User::new("test@example.com".to_string()); 501 | user.create(&auth_service.db_pool).await.unwrap(); 502 | 503 | let token_set = TokenSet { 504 | access_token: "test_access_token".to_string(), 505 | refresh_token: "test_refresh_token".to_string(), 506 | expires_at: Utc::now() + Duration::hours(1), 507 | scope: Some("gmail.readonly".to_string()), 508 | }; 509 | 510 | // Store tokens 511 | auth_service.store_tokens(&user.id, &token_set).await.unwrap(); 512 | 513 | // Retrieve tokens 514 | let retrieved_tokens = auth_service.get_tokens(&user.id).await.unwrap().unwrap(); 515 | 516 | assert_eq!(retrieved_tokens.access_token, token_set.access_token); 517 | assert_eq!(retrieved_tokens.refresh_token, token_set.refresh_token); 518 | // Note: scope is not stored in database, so it will be None when retrieved 519 | } 520 | 521 | #[tokio::test] 522 | async fn test_has_valid_tokens() { 523 | env::set_var("GOOGLE_CLIENT_ID", "test_client_id"); 524 | env::set_var("GOOGLE_CLIENT_SECRET", "test_client_secret"); 525 | 526 | let pool = setup_test_db().await; 527 | let auth_service = AuthService::new(Arc::new(pool)).unwrap(); 528 | 529 | // Create a test user 530 | let user = User::new("test@example.com".to_string()); 531 | user.create(&auth_service.db_pool).await.unwrap(); 532 | 533 | // User should not have valid tokens initially 534 | let has_valid = auth_service.has_valid_tokens(&user.id).await.unwrap(); 535 | assert!(!has_valid); 536 | 537 | // Store valid tokens 538 | let token_set = TokenSet { 539 | access_token: "test_access_token".to_string(), 540 | refresh_token: "test_refresh_token".to_string(), 541 | expires_at: Utc::now() + Duration::hours(1), // Valid for 1 hour 542 | scope: Some("gmail.readonly".to_string()), 543 | }; 544 | 545 | auth_service.store_tokens(&user.id, &token_set).await.unwrap(); 546 | 547 | // User should now have valid tokens 548 | let has_valid = auth_service.has_valid_tokens(&user.id).await.unwrap(); 549 | assert!(has_valid); 550 | 551 | // Store expired tokens 552 | let expired_token_set = TokenSet { 553 | access_token: "expired_access_token".to_string(), 554 | refresh_token: "expired_refresh_token".to_string(), 555 | expires_at: Utc::now() - Duration::hours(1), // Expired 1 hour ago 556 | scope: Some("gmail.readonly".to_string()), 557 | }; 558 | 559 | auth_service.store_tokens(&user.id, &expired_token_set).await.unwrap(); 560 | 561 | // User should not have valid tokens now 562 | let has_valid = auth_service.has_valid_tokens(&user.id).await.unwrap(); 563 | assert!(!has_valid); 564 | } 565 | 566 | #[tokio::test] 567 | async fn test_auth_state_cleanup() { 568 | env::set_var("GOOGLE_CLIENT_ID", "test_client_id"); 569 | env::set_var("GOOGLE_CLIENT_SECRET", "test_client_secret"); 570 | 571 | let pool = setup_test_db().await; 572 | let auth_service = AuthService::new(Arc::new(pool)).unwrap(); 573 | 574 | // Generate multiple auth URLs to create states 575 | let (_, csrf_token1) = auth_service.generate_auth_url(None).await.unwrap(); 576 | let (_, csrf_token2) = auth_service.generate_auth_url(None).await.unwrap(); 577 | 578 | // Both tokens should be valid initially 579 | assert!(auth_service.validate_and_get_auth_state(&csrf_token1).await.is_ok()); 580 | 581 | // The second token should still be valid (first was consumed) 582 | assert!(auth_service.validate_and_get_auth_state(&csrf_token2).await.is_ok()); 583 | 584 | // Both tokens should now be consumed 585 | assert!(auth_service.validate_and_get_auth_state(&csrf_token1).await.is_err()); 586 | assert!(auth_service.validate_and_get_auth_state(&csrf_token2).await.is_err()); 587 | } 588 | } -------------------------------------------------------------------------------- /backend/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 19 | 20 | [[package]] 21 | name = "ahash" 22 | version = "0.8.12" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" 25 | dependencies = [ 26 | "cfg-if", 27 | "getrandom 0.3.3", 28 | "once_cell", 29 | "version_check", 30 | "zerocopy", 31 | ] 32 | 33 | [[package]] 34 | name = "allocator-api2" 35 | version = "0.2.21" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 38 | 39 | [[package]] 40 | name = "android-tzdata" 41 | version = "0.1.1" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 44 | 45 | [[package]] 46 | name = "android_system_properties" 47 | version = "0.1.5" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 50 | dependencies = [ 51 | "libc", 52 | ] 53 | 54 | [[package]] 55 | name = "anyhow" 56 | version = "1.0.98" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 59 | 60 | [[package]] 61 | name = "async-trait" 62 | version = "0.1.88" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" 65 | dependencies = [ 66 | "proc-macro2", 67 | "quote", 68 | "syn 2.0.104", 69 | ] 70 | 71 | [[package]] 72 | name = "atoi" 73 | version = "2.0.0" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" 76 | dependencies = [ 77 | "num-traits", 78 | ] 79 | 80 | [[package]] 81 | name = "autocfg" 82 | version = "1.5.0" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 85 | 86 | [[package]] 87 | name = "axum" 88 | version = "0.7.9" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" 91 | dependencies = [ 92 | "async-trait", 93 | "axum-core", 94 | "bytes", 95 | "futures-util", 96 | "http 1.3.1", 97 | "http-body 1.0.1", 98 | "http-body-util", 99 | "hyper 1.6.0", 100 | "hyper-util", 101 | "itoa", 102 | "matchit", 103 | "memchr", 104 | "mime", 105 | "percent-encoding", 106 | "pin-project-lite", 107 | "rustversion", 108 | "serde", 109 | "serde_json", 110 | "serde_path_to_error", 111 | "serde_urlencoded", 112 | "sync_wrapper 1.0.2", 113 | "tokio", 114 | "tower 0.5.2", 115 | "tower-layer", 116 | "tower-service", 117 | "tracing", 118 | ] 119 | 120 | [[package]] 121 | name = "axum-core" 122 | version = "0.4.5" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" 125 | dependencies = [ 126 | "async-trait", 127 | "bytes", 128 | "futures-util", 129 | "http 1.3.1", 130 | "http-body 1.0.1", 131 | "http-body-util", 132 | "mime", 133 | "pin-project-lite", 134 | "rustversion", 135 | "sync_wrapper 1.0.2", 136 | "tower-layer", 137 | "tower-service", 138 | "tracing", 139 | ] 140 | 141 | [[package]] 142 | name = "backtrace" 143 | version = "0.3.75" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" 146 | dependencies = [ 147 | "addr2line", 148 | "cfg-if", 149 | "libc", 150 | "miniz_oxide", 151 | "object", 152 | "rustc-demangle", 153 | "windows-targets 0.52.6", 154 | ] 155 | 156 | [[package]] 157 | name = "base64" 158 | version = "0.13.1" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" 161 | 162 | [[package]] 163 | name = "base64" 164 | version = "0.21.7" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 167 | 168 | [[package]] 169 | name = "base64ct" 170 | version = "1.8.0" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" 173 | 174 | [[package]] 175 | name = "bitflags" 176 | version = "1.3.2" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 179 | 180 | [[package]] 181 | name = "bitflags" 182 | version = "2.9.1" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 185 | dependencies = [ 186 | "serde", 187 | ] 188 | 189 | [[package]] 190 | name = "block-buffer" 191 | version = "0.10.4" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 194 | dependencies = [ 195 | "generic-array", 196 | ] 197 | 198 | [[package]] 199 | name = "bumpalo" 200 | version = "3.19.0" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 203 | 204 | [[package]] 205 | name = "byteorder" 206 | version = "1.5.0" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 209 | 210 | [[package]] 211 | name = "bytes" 212 | version = "1.10.1" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 215 | 216 | [[package]] 217 | name = "cc" 218 | version = "1.2.29" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" 221 | dependencies = [ 222 | "shlex", 223 | ] 224 | 225 | [[package]] 226 | name = "cfg-if" 227 | version = "1.0.1" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" 230 | 231 | [[package]] 232 | name = "chrono" 233 | version = "0.4.41" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" 236 | dependencies = [ 237 | "android-tzdata", 238 | "iana-time-zone", 239 | "js-sys", 240 | "num-traits", 241 | "serde", 242 | "wasm-bindgen", 243 | "windows-link", 244 | ] 245 | 246 | [[package]] 247 | name = "const-oid" 248 | version = "0.9.6" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" 251 | 252 | [[package]] 253 | name = "core-foundation" 254 | version = "0.9.4" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 257 | dependencies = [ 258 | "core-foundation-sys", 259 | "libc", 260 | ] 261 | 262 | [[package]] 263 | name = "core-foundation-sys" 264 | version = "0.8.7" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 267 | 268 | [[package]] 269 | name = "cpufeatures" 270 | version = "0.2.17" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" 273 | dependencies = [ 274 | "libc", 275 | ] 276 | 277 | [[package]] 278 | name = "crc" 279 | version = "3.3.0" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" 282 | dependencies = [ 283 | "crc-catalog", 284 | ] 285 | 286 | [[package]] 287 | name = "crc-catalog" 288 | version = "2.4.0" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" 291 | 292 | [[package]] 293 | name = "crossbeam-queue" 294 | version = "0.3.12" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" 297 | dependencies = [ 298 | "crossbeam-utils", 299 | ] 300 | 301 | [[package]] 302 | name = "crossbeam-utils" 303 | version = "0.8.21" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 306 | 307 | [[package]] 308 | name = "crypto-common" 309 | version = "0.1.6" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 312 | dependencies = [ 313 | "generic-array", 314 | "typenum", 315 | ] 316 | 317 | [[package]] 318 | name = "der" 319 | version = "0.7.10" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" 322 | dependencies = [ 323 | "const-oid", 324 | "pem-rfc7468", 325 | "zeroize", 326 | ] 327 | 328 | [[package]] 329 | name = "digest" 330 | version = "0.10.7" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 333 | dependencies = [ 334 | "block-buffer", 335 | "const-oid", 336 | "crypto-common", 337 | "subtle", 338 | ] 339 | 340 | [[package]] 341 | name = "displaydoc" 342 | version = "0.2.5" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 345 | dependencies = [ 346 | "proc-macro2", 347 | "quote", 348 | "syn 2.0.104", 349 | ] 350 | 351 | [[package]] 352 | name = "dotenv" 353 | version = "0.15.0" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" 356 | 357 | [[package]] 358 | name = "dotenvy" 359 | version = "0.15.7" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" 362 | 363 | [[package]] 364 | name = "either" 365 | version = "1.15.0" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 368 | dependencies = [ 369 | "serde", 370 | ] 371 | 372 | [[package]] 373 | name = "encoding_rs" 374 | version = "0.8.35" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 377 | dependencies = [ 378 | "cfg-if", 379 | ] 380 | 381 | [[package]] 382 | name = "equivalent" 383 | version = "1.0.2" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 386 | 387 | [[package]] 388 | name = "errno" 389 | version = "0.3.13" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" 392 | dependencies = [ 393 | "libc", 394 | "windows-sys 0.60.2", 395 | ] 396 | 397 | [[package]] 398 | name = "etcetera" 399 | version = "0.8.0" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" 402 | dependencies = [ 403 | "cfg-if", 404 | "home", 405 | "windows-sys 0.48.0", 406 | ] 407 | 408 | [[package]] 409 | name = "event-listener" 410 | version = "2.5.3" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" 413 | 414 | [[package]] 415 | name = "fastrand" 416 | version = "2.3.0" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 419 | 420 | [[package]] 421 | name = "flume" 422 | version = "0.11.1" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" 425 | dependencies = [ 426 | "futures-core", 427 | "futures-sink", 428 | "spin", 429 | ] 430 | 431 | [[package]] 432 | name = "fnv" 433 | version = "1.0.7" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 436 | 437 | [[package]] 438 | name = "foreign-types" 439 | version = "0.3.2" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 442 | dependencies = [ 443 | "foreign-types-shared", 444 | ] 445 | 446 | [[package]] 447 | name = "foreign-types-shared" 448 | version = "0.1.1" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 451 | 452 | [[package]] 453 | name = "form_urlencoded" 454 | version = "1.2.1" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 457 | dependencies = [ 458 | "percent-encoding", 459 | ] 460 | 461 | [[package]] 462 | name = "futures-channel" 463 | version = "0.3.31" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 466 | dependencies = [ 467 | "futures-core", 468 | "futures-sink", 469 | ] 470 | 471 | [[package]] 472 | name = "futures-core" 473 | version = "0.3.31" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 476 | 477 | [[package]] 478 | name = "futures-executor" 479 | version = "0.3.31" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 482 | dependencies = [ 483 | "futures-core", 484 | "futures-task", 485 | "futures-util", 486 | ] 487 | 488 | [[package]] 489 | name = "futures-intrusive" 490 | version = "0.5.0" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" 493 | dependencies = [ 494 | "futures-core", 495 | "lock_api", 496 | "parking_lot", 497 | ] 498 | 499 | [[package]] 500 | name = "futures-io" 501 | version = "0.3.31" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 504 | 505 | [[package]] 506 | name = "futures-sink" 507 | version = "0.3.31" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 510 | 511 | [[package]] 512 | name = "futures-task" 513 | version = "0.3.31" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 516 | 517 | [[package]] 518 | name = "futures-util" 519 | version = "0.3.31" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 522 | dependencies = [ 523 | "futures-core", 524 | "futures-io", 525 | "futures-sink", 526 | "futures-task", 527 | "memchr", 528 | "pin-project-lite", 529 | "pin-utils", 530 | "slab", 531 | ] 532 | 533 | [[package]] 534 | name = "generic-array" 535 | version = "0.14.7" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 538 | dependencies = [ 539 | "typenum", 540 | "version_check", 541 | ] 542 | 543 | [[package]] 544 | name = "getrandom" 545 | version = "0.2.16" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 548 | dependencies = [ 549 | "cfg-if", 550 | "js-sys", 551 | "libc", 552 | "wasi 0.11.1+wasi-snapshot-preview1", 553 | "wasm-bindgen", 554 | ] 555 | 556 | [[package]] 557 | name = "getrandom" 558 | version = "0.3.3" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" 561 | dependencies = [ 562 | "cfg-if", 563 | "libc", 564 | "r-efi", 565 | "wasi 0.14.2+wasi-0.2.4", 566 | ] 567 | 568 | [[package]] 569 | name = "gimli" 570 | version = "0.31.1" 571 | source = "registry+https://github.com/rust-lang/crates.io-index" 572 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 573 | 574 | [[package]] 575 | name = "gmail-link-queue-backend" 576 | version = "0.1.0" 577 | dependencies = [ 578 | "anyhow", 579 | "axum", 580 | "base64 0.21.7", 581 | "chrono", 582 | "dotenv", 583 | "oauth2", 584 | "rand", 585 | "reqwest", 586 | "serde", 587 | "serde_json", 588 | "serde_urlencoded", 589 | "sqlx", 590 | "thiserror", 591 | "tokio", 592 | "tower 0.4.13", 593 | "tower-http", 594 | "url", 595 | "uuid", 596 | ] 597 | 598 | [[package]] 599 | name = "h2" 600 | version = "0.3.27" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" 603 | dependencies = [ 604 | "bytes", 605 | "fnv", 606 | "futures-core", 607 | "futures-sink", 608 | "futures-util", 609 | "http 0.2.12", 610 | "indexmap", 611 | "slab", 612 | "tokio", 613 | "tokio-util", 614 | "tracing", 615 | ] 616 | 617 | [[package]] 618 | name = "hashbrown" 619 | version = "0.14.5" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 622 | dependencies = [ 623 | "ahash", 624 | "allocator-api2", 625 | ] 626 | 627 | [[package]] 628 | name = "hashbrown" 629 | version = "0.15.4" 630 | source = "registry+https://github.com/rust-lang/crates.io-index" 631 | checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" 632 | 633 | [[package]] 634 | name = "hashlink" 635 | version = "0.8.4" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" 638 | dependencies = [ 639 | "hashbrown 0.14.5", 640 | ] 641 | 642 | [[package]] 643 | name = "heck" 644 | version = "0.4.1" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 647 | dependencies = [ 648 | "unicode-segmentation", 649 | ] 650 | 651 | [[package]] 652 | name = "hex" 653 | version = "0.4.3" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 656 | 657 | [[package]] 658 | name = "hkdf" 659 | version = "0.12.4" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" 662 | dependencies = [ 663 | "hmac", 664 | ] 665 | 666 | [[package]] 667 | name = "hmac" 668 | version = "0.12.1" 669 | source = "registry+https://github.com/rust-lang/crates.io-index" 670 | checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" 671 | dependencies = [ 672 | "digest", 673 | ] 674 | 675 | [[package]] 676 | name = "home" 677 | version = "0.5.11" 678 | source = "registry+https://github.com/rust-lang/crates.io-index" 679 | checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" 680 | dependencies = [ 681 | "windows-sys 0.59.0", 682 | ] 683 | 684 | [[package]] 685 | name = "http" 686 | version = "0.2.12" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" 689 | dependencies = [ 690 | "bytes", 691 | "fnv", 692 | "itoa", 693 | ] 694 | 695 | [[package]] 696 | name = "http" 697 | version = "1.3.1" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" 700 | dependencies = [ 701 | "bytes", 702 | "fnv", 703 | "itoa", 704 | ] 705 | 706 | [[package]] 707 | name = "http-body" 708 | version = "0.4.6" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" 711 | dependencies = [ 712 | "bytes", 713 | "http 0.2.12", 714 | "pin-project-lite", 715 | ] 716 | 717 | [[package]] 718 | name = "http-body" 719 | version = "1.0.1" 720 | source = "registry+https://github.com/rust-lang/crates.io-index" 721 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 722 | dependencies = [ 723 | "bytes", 724 | "http 1.3.1", 725 | ] 726 | 727 | [[package]] 728 | name = "http-body-util" 729 | version = "0.1.3" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" 732 | dependencies = [ 733 | "bytes", 734 | "futures-core", 735 | "http 1.3.1", 736 | "http-body 1.0.1", 737 | "pin-project-lite", 738 | ] 739 | 740 | [[package]] 741 | name = "httparse" 742 | version = "1.10.1" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 745 | 746 | [[package]] 747 | name = "httpdate" 748 | version = "1.0.3" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 751 | 752 | [[package]] 753 | name = "hyper" 754 | version = "0.14.32" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" 757 | dependencies = [ 758 | "bytes", 759 | "futures-channel", 760 | "futures-core", 761 | "futures-util", 762 | "h2", 763 | "http 0.2.12", 764 | "http-body 0.4.6", 765 | "httparse", 766 | "httpdate", 767 | "itoa", 768 | "pin-project-lite", 769 | "socket2", 770 | "tokio", 771 | "tower-service", 772 | "tracing", 773 | "want", 774 | ] 775 | 776 | [[package]] 777 | name = "hyper" 778 | version = "1.6.0" 779 | source = "registry+https://github.com/rust-lang/crates.io-index" 780 | checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" 781 | dependencies = [ 782 | "bytes", 783 | "futures-channel", 784 | "futures-util", 785 | "http 1.3.1", 786 | "http-body 1.0.1", 787 | "httparse", 788 | "httpdate", 789 | "itoa", 790 | "pin-project-lite", 791 | "smallvec", 792 | "tokio", 793 | ] 794 | 795 | [[package]] 796 | name = "hyper-rustls" 797 | version = "0.24.2" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" 800 | dependencies = [ 801 | "futures-util", 802 | "http 0.2.12", 803 | "hyper 0.14.32", 804 | "rustls", 805 | "tokio", 806 | "tokio-rustls", 807 | ] 808 | 809 | [[package]] 810 | name = "hyper-tls" 811 | version = "0.5.0" 812 | source = "registry+https://github.com/rust-lang/crates.io-index" 813 | checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" 814 | dependencies = [ 815 | "bytes", 816 | "hyper 0.14.32", 817 | "native-tls", 818 | "tokio", 819 | "tokio-native-tls", 820 | ] 821 | 822 | [[package]] 823 | name = "hyper-util" 824 | version = "0.1.15" 825 | source = "registry+https://github.com/rust-lang/crates.io-index" 826 | checksum = "7f66d5bd4c6f02bf0542fad85d626775bab9258cf795a4256dcaf3161114d1df" 827 | dependencies = [ 828 | "bytes", 829 | "futures-core", 830 | "http 1.3.1", 831 | "http-body 1.0.1", 832 | "hyper 1.6.0", 833 | "pin-project-lite", 834 | "tokio", 835 | "tower-service", 836 | ] 837 | 838 | [[package]] 839 | name = "iana-time-zone" 840 | version = "0.1.63" 841 | source = "registry+https://github.com/rust-lang/crates.io-index" 842 | checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" 843 | dependencies = [ 844 | "android_system_properties", 845 | "core-foundation-sys", 846 | "iana-time-zone-haiku", 847 | "js-sys", 848 | "log", 849 | "wasm-bindgen", 850 | "windows-core", 851 | ] 852 | 853 | [[package]] 854 | name = "iana-time-zone-haiku" 855 | version = "0.1.2" 856 | source = "registry+https://github.com/rust-lang/crates.io-index" 857 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 858 | dependencies = [ 859 | "cc", 860 | ] 861 | 862 | [[package]] 863 | name = "icu_collections" 864 | version = "2.0.0" 865 | source = "registry+https://github.com/rust-lang/crates.io-index" 866 | checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" 867 | dependencies = [ 868 | "displaydoc", 869 | "potential_utf", 870 | "yoke", 871 | "zerofrom", 872 | "zerovec", 873 | ] 874 | 875 | [[package]] 876 | name = "icu_locale_core" 877 | version = "2.0.0" 878 | source = "registry+https://github.com/rust-lang/crates.io-index" 879 | checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" 880 | dependencies = [ 881 | "displaydoc", 882 | "litemap", 883 | "tinystr", 884 | "writeable", 885 | "zerovec", 886 | ] 887 | 888 | [[package]] 889 | name = "icu_normalizer" 890 | version = "2.0.0" 891 | source = "registry+https://github.com/rust-lang/crates.io-index" 892 | checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" 893 | dependencies = [ 894 | "displaydoc", 895 | "icu_collections", 896 | "icu_normalizer_data", 897 | "icu_properties", 898 | "icu_provider", 899 | "smallvec", 900 | "zerovec", 901 | ] 902 | 903 | [[package]] 904 | name = "icu_normalizer_data" 905 | version = "2.0.0" 906 | source = "registry+https://github.com/rust-lang/crates.io-index" 907 | checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" 908 | 909 | [[package]] 910 | name = "icu_properties" 911 | version = "2.0.1" 912 | source = "registry+https://github.com/rust-lang/crates.io-index" 913 | checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" 914 | dependencies = [ 915 | "displaydoc", 916 | "icu_collections", 917 | "icu_locale_core", 918 | "icu_properties_data", 919 | "icu_provider", 920 | "potential_utf", 921 | "zerotrie", 922 | "zerovec", 923 | ] 924 | 925 | [[package]] 926 | name = "icu_properties_data" 927 | version = "2.0.1" 928 | source = "registry+https://github.com/rust-lang/crates.io-index" 929 | checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" 930 | 931 | [[package]] 932 | name = "icu_provider" 933 | version = "2.0.0" 934 | source = "registry+https://github.com/rust-lang/crates.io-index" 935 | checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" 936 | dependencies = [ 937 | "displaydoc", 938 | "icu_locale_core", 939 | "stable_deref_trait", 940 | "tinystr", 941 | "writeable", 942 | "yoke", 943 | "zerofrom", 944 | "zerotrie", 945 | "zerovec", 946 | ] 947 | 948 | [[package]] 949 | name = "idna" 950 | version = "1.0.3" 951 | source = "registry+https://github.com/rust-lang/crates.io-index" 952 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 953 | dependencies = [ 954 | "idna_adapter", 955 | "smallvec", 956 | "utf8_iter", 957 | ] 958 | 959 | [[package]] 960 | name = "idna_adapter" 961 | version = "1.2.1" 962 | source = "registry+https://github.com/rust-lang/crates.io-index" 963 | checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 964 | dependencies = [ 965 | "icu_normalizer", 966 | "icu_properties", 967 | ] 968 | 969 | [[package]] 970 | name = "indexmap" 971 | version = "2.10.0" 972 | source = "registry+https://github.com/rust-lang/crates.io-index" 973 | checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" 974 | dependencies = [ 975 | "equivalent", 976 | "hashbrown 0.15.4", 977 | ] 978 | 979 | [[package]] 980 | name = "io-uring" 981 | version = "0.7.8" 982 | source = "registry+https://github.com/rust-lang/crates.io-index" 983 | checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" 984 | dependencies = [ 985 | "bitflags 2.9.1", 986 | "cfg-if", 987 | "libc", 988 | ] 989 | 990 | [[package]] 991 | name = "ipnet" 992 | version = "2.11.0" 993 | source = "registry+https://github.com/rust-lang/crates.io-index" 994 | checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 995 | 996 | [[package]] 997 | name = "itoa" 998 | version = "1.0.15" 999 | source = "registry+https://github.com/rust-lang/crates.io-index" 1000 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 1001 | 1002 | [[package]] 1003 | name = "js-sys" 1004 | version = "0.3.77" 1005 | source = "registry+https://github.com/rust-lang/crates.io-index" 1006 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 1007 | dependencies = [ 1008 | "once_cell", 1009 | "wasm-bindgen", 1010 | ] 1011 | 1012 | [[package]] 1013 | name = "lazy_static" 1014 | version = "1.5.0" 1015 | source = "registry+https://github.com/rust-lang/crates.io-index" 1016 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 1017 | dependencies = [ 1018 | "spin", 1019 | ] 1020 | 1021 | [[package]] 1022 | name = "libc" 1023 | version = "0.2.174" 1024 | source = "registry+https://github.com/rust-lang/crates.io-index" 1025 | checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" 1026 | 1027 | [[package]] 1028 | name = "libm" 1029 | version = "0.2.15" 1030 | source = "registry+https://github.com/rust-lang/crates.io-index" 1031 | checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" 1032 | 1033 | [[package]] 1034 | name = "libsqlite3-sys" 1035 | version = "0.27.0" 1036 | source = "registry+https://github.com/rust-lang/crates.io-index" 1037 | checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" 1038 | dependencies = [ 1039 | "cc", 1040 | "pkg-config", 1041 | "vcpkg", 1042 | ] 1043 | 1044 | [[package]] 1045 | name = "linux-raw-sys" 1046 | version = "0.9.4" 1047 | source = "registry+https://github.com/rust-lang/crates.io-index" 1048 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 1049 | 1050 | [[package]] 1051 | name = "litemap" 1052 | version = "0.8.0" 1053 | source = "registry+https://github.com/rust-lang/crates.io-index" 1054 | checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" 1055 | 1056 | [[package]] 1057 | name = "lock_api" 1058 | version = "0.4.13" 1059 | source = "registry+https://github.com/rust-lang/crates.io-index" 1060 | checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" 1061 | dependencies = [ 1062 | "autocfg", 1063 | "scopeguard", 1064 | ] 1065 | 1066 | [[package]] 1067 | name = "log" 1068 | version = "0.4.27" 1069 | source = "registry+https://github.com/rust-lang/crates.io-index" 1070 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 1071 | 1072 | [[package]] 1073 | name = "matchit" 1074 | version = "0.7.3" 1075 | source = "registry+https://github.com/rust-lang/crates.io-index" 1076 | checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" 1077 | 1078 | [[package]] 1079 | name = "md-5" 1080 | version = "0.10.6" 1081 | source = "registry+https://github.com/rust-lang/crates.io-index" 1082 | checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" 1083 | dependencies = [ 1084 | "cfg-if", 1085 | "digest", 1086 | ] 1087 | 1088 | [[package]] 1089 | name = "memchr" 1090 | version = "2.7.5" 1091 | source = "registry+https://github.com/rust-lang/crates.io-index" 1092 | checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" 1093 | 1094 | [[package]] 1095 | name = "mime" 1096 | version = "0.3.17" 1097 | source = "registry+https://github.com/rust-lang/crates.io-index" 1098 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 1099 | 1100 | [[package]] 1101 | name = "minimal-lexical" 1102 | version = "0.2.1" 1103 | source = "registry+https://github.com/rust-lang/crates.io-index" 1104 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 1105 | 1106 | [[package]] 1107 | name = "miniz_oxide" 1108 | version = "0.8.9" 1109 | source = "registry+https://github.com/rust-lang/crates.io-index" 1110 | checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" 1111 | dependencies = [ 1112 | "adler2", 1113 | ] 1114 | 1115 | [[package]] 1116 | name = "mio" 1117 | version = "1.0.4" 1118 | source = "registry+https://github.com/rust-lang/crates.io-index" 1119 | checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" 1120 | dependencies = [ 1121 | "libc", 1122 | "wasi 0.11.1+wasi-snapshot-preview1", 1123 | "windows-sys 0.59.0", 1124 | ] 1125 | 1126 | [[package]] 1127 | name = "native-tls" 1128 | version = "0.2.14" 1129 | source = "registry+https://github.com/rust-lang/crates.io-index" 1130 | checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" 1131 | dependencies = [ 1132 | "libc", 1133 | "log", 1134 | "openssl", 1135 | "openssl-probe", 1136 | "openssl-sys", 1137 | "schannel", 1138 | "security-framework", 1139 | "security-framework-sys", 1140 | "tempfile", 1141 | ] 1142 | 1143 | [[package]] 1144 | name = "nom" 1145 | version = "7.1.3" 1146 | source = "registry+https://github.com/rust-lang/crates.io-index" 1147 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 1148 | dependencies = [ 1149 | "memchr", 1150 | "minimal-lexical", 1151 | ] 1152 | 1153 | [[package]] 1154 | name = "num-bigint-dig" 1155 | version = "0.8.4" 1156 | source = "registry+https://github.com/rust-lang/crates.io-index" 1157 | checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" 1158 | dependencies = [ 1159 | "byteorder", 1160 | "lazy_static", 1161 | "libm", 1162 | "num-integer", 1163 | "num-iter", 1164 | "num-traits", 1165 | "rand", 1166 | "smallvec", 1167 | "zeroize", 1168 | ] 1169 | 1170 | [[package]] 1171 | name = "num-integer" 1172 | version = "0.1.46" 1173 | source = "registry+https://github.com/rust-lang/crates.io-index" 1174 | checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 1175 | dependencies = [ 1176 | "num-traits", 1177 | ] 1178 | 1179 | [[package]] 1180 | name = "num-iter" 1181 | version = "0.1.45" 1182 | source = "registry+https://github.com/rust-lang/crates.io-index" 1183 | checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" 1184 | dependencies = [ 1185 | "autocfg", 1186 | "num-integer", 1187 | "num-traits", 1188 | ] 1189 | 1190 | [[package]] 1191 | name = "num-traits" 1192 | version = "0.2.19" 1193 | source = "registry+https://github.com/rust-lang/crates.io-index" 1194 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 1195 | dependencies = [ 1196 | "autocfg", 1197 | "libm", 1198 | ] 1199 | 1200 | [[package]] 1201 | name = "oauth2" 1202 | version = "4.4.2" 1203 | source = "registry+https://github.com/rust-lang/crates.io-index" 1204 | checksum = "c38841cdd844847e3e7c8d29cef9dcfed8877f8f56f9071f77843ecf3baf937f" 1205 | dependencies = [ 1206 | "base64 0.13.1", 1207 | "chrono", 1208 | "getrandom 0.2.16", 1209 | "http 0.2.12", 1210 | "rand", 1211 | "reqwest", 1212 | "serde", 1213 | "serde_json", 1214 | "serde_path_to_error", 1215 | "sha2", 1216 | "thiserror", 1217 | "url", 1218 | ] 1219 | 1220 | [[package]] 1221 | name = "object" 1222 | version = "0.36.7" 1223 | source = "registry+https://github.com/rust-lang/crates.io-index" 1224 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 1225 | dependencies = [ 1226 | "memchr", 1227 | ] 1228 | 1229 | [[package]] 1230 | name = "once_cell" 1231 | version = "1.21.3" 1232 | source = "registry+https://github.com/rust-lang/crates.io-index" 1233 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 1234 | 1235 | [[package]] 1236 | name = "openssl" 1237 | version = "0.10.73" 1238 | source = "registry+https://github.com/rust-lang/crates.io-index" 1239 | checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" 1240 | dependencies = [ 1241 | "bitflags 2.9.1", 1242 | "cfg-if", 1243 | "foreign-types", 1244 | "libc", 1245 | "once_cell", 1246 | "openssl-macros", 1247 | "openssl-sys", 1248 | ] 1249 | 1250 | [[package]] 1251 | name = "openssl-macros" 1252 | version = "0.1.1" 1253 | source = "registry+https://github.com/rust-lang/crates.io-index" 1254 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 1255 | dependencies = [ 1256 | "proc-macro2", 1257 | "quote", 1258 | "syn 2.0.104", 1259 | ] 1260 | 1261 | [[package]] 1262 | name = "openssl-probe" 1263 | version = "0.1.6" 1264 | source = "registry+https://github.com/rust-lang/crates.io-index" 1265 | checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" 1266 | 1267 | [[package]] 1268 | name = "openssl-sys" 1269 | version = "0.9.109" 1270 | source = "registry+https://github.com/rust-lang/crates.io-index" 1271 | checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" 1272 | dependencies = [ 1273 | "cc", 1274 | "libc", 1275 | "pkg-config", 1276 | "vcpkg", 1277 | ] 1278 | 1279 | [[package]] 1280 | name = "parking_lot" 1281 | version = "0.12.4" 1282 | source = "registry+https://github.com/rust-lang/crates.io-index" 1283 | checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" 1284 | dependencies = [ 1285 | "lock_api", 1286 | "parking_lot_core", 1287 | ] 1288 | 1289 | [[package]] 1290 | name = "parking_lot_core" 1291 | version = "0.9.11" 1292 | source = "registry+https://github.com/rust-lang/crates.io-index" 1293 | checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" 1294 | dependencies = [ 1295 | "cfg-if", 1296 | "libc", 1297 | "redox_syscall", 1298 | "smallvec", 1299 | "windows-targets 0.52.6", 1300 | ] 1301 | 1302 | [[package]] 1303 | name = "paste" 1304 | version = "1.0.15" 1305 | source = "registry+https://github.com/rust-lang/crates.io-index" 1306 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 1307 | 1308 | [[package]] 1309 | name = "pem-rfc7468" 1310 | version = "0.7.0" 1311 | source = "registry+https://github.com/rust-lang/crates.io-index" 1312 | checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" 1313 | dependencies = [ 1314 | "base64ct", 1315 | ] 1316 | 1317 | [[package]] 1318 | name = "percent-encoding" 1319 | version = "2.3.1" 1320 | source = "registry+https://github.com/rust-lang/crates.io-index" 1321 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 1322 | 1323 | [[package]] 1324 | name = "pin-project-lite" 1325 | version = "0.2.16" 1326 | source = "registry+https://github.com/rust-lang/crates.io-index" 1327 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 1328 | 1329 | [[package]] 1330 | name = "pin-utils" 1331 | version = "0.1.0" 1332 | source = "registry+https://github.com/rust-lang/crates.io-index" 1333 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1334 | 1335 | [[package]] 1336 | name = "pkcs1" 1337 | version = "0.7.5" 1338 | source = "registry+https://github.com/rust-lang/crates.io-index" 1339 | checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" 1340 | dependencies = [ 1341 | "der", 1342 | "pkcs8", 1343 | "spki", 1344 | ] 1345 | 1346 | [[package]] 1347 | name = "pkcs8" 1348 | version = "0.10.2" 1349 | source = "registry+https://github.com/rust-lang/crates.io-index" 1350 | checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" 1351 | dependencies = [ 1352 | "der", 1353 | "spki", 1354 | ] 1355 | 1356 | [[package]] 1357 | name = "pkg-config" 1358 | version = "0.3.32" 1359 | source = "registry+https://github.com/rust-lang/crates.io-index" 1360 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 1361 | 1362 | [[package]] 1363 | name = "potential_utf" 1364 | version = "0.1.2" 1365 | source = "registry+https://github.com/rust-lang/crates.io-index" 1366 | checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" 1367 | dependencies = [ 1368 | "zerovec", 1369 | ] 1370 | 1371 | [[package]] 1372 | name = "ppv-lite86" 1373 | version = "0.2.21" 1374 | source = "registry+https://github.com/rust-lang/crates.io-index" 1375 | checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 1376 | dependencies = [ 1377 | "zerocopy", 1378 | ] 1379 | 1380 | [[package]] 1381 | name = "proc-macro2" 1382 | version = "1.0.95" 1383 | source = "registry+https://github.com/rust-lang/crates.io-index" 1384 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 1385 | dependencies = [ 1386 | "unicode-ident", 1387 | ] 1388 | 1389 | [[package]] 1390 | name = "quote" 1391 | version = "1.0.40" 1392 | source = "registry+https://github.com/rust-lang/crates.io-index" 1393 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 1394 | dependencies = [ 1395 | "proc-macro2", 1396 | ] 1397 | 1398 | [[package]] 1399 | name = "r-efi" 1400 | version = "5.3.0" 1401 | source = "registry+https://github.com/rust-lang/crates.io-index" 1402 | checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 1403 | 1404 | [[package]] 1405 | name = "rand" 1406 | version = "0.8.5" 1407 | source = "registry+https://github.com/rust-lang/crates.io-index" 1408 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1409 | dependencies = [ 1410 | "libc", 1411 | "rand_chacha", 1412 | "rand_core", 1413 | ] 1414 | 1415 | [[package]] 1416 | name = "rand_chacha" 1417 | version = "0.3.1" 1418 | source = "registry+https://github.com/rust-lang/crates.io-index" 1419 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1420 | dependencies = [ 1421 | "ppv-lite86", 1422 | "rand_core", 1423 | ] 1424 | 1425 | [[package]] 1426 | name = "rand_core" 1427 | version = "0.6.4" 1428 | source = "registry+https://github.com/rust-lang/crates.io-index" 1429 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1430 | dependencies = [ 1431 | "getrandom 0.2.16", 1432 | ] 1433 | 1434 | [[package]] 1435 | name = "redox_syscall" 1436 | version = "0.5.13" 1437 | source = "registry+https://github.com/rust-lang/crates.io-index" 1438 | checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" 1439 | dependencies = [ 1440 | "bitflags 2.9.1", 1441 | ] 1442 | 1443 | [[package]] 1444 | name = "reqwest" 1445 | version = "0.11.27" 1446 | source = "registry+https://github.com/rust-lang/crates.io-index" 1447 | checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" 1448 | dependencies = [ 1449 | "base64 0.21.7", 1450 | "bytes", 1451 | "encoding_rs", 1452 | "futures-core", 1453 | "futures-util", 1454 | "h2", 1455 | "http 0.2.12", 1456 | "http-body 0.4.6", 1457 | "hyper 0.14.32", 1458 | "hyper-rustls", 1459 | "hyper-tls", 1460 | "ipnet", 1461 | "js-sys", 1462 | "log", 1463 | "mime", 1464 | "native-tls", 1465 | "once_cell", 1466 | "percent-encoding", 1467 | "pin-project-lite", 1468 | "rustls", 1469 | "rustls-pemfile", 1470 | "serde", 1471 | "serde_json", 1472 | "serde_urlencoded", 1473 | "sync_wrapper 0.1.2", 1474 | "system-configuration", 1475 | "tokio", 1476 | "tokio-native-tls", 1477 | "tokio-rustls", 1478 | "tower-service", 1479 | "url", 1480 | "wasm-bindgen", 1481 | "wasm-bindgen-futures", 1482 | "web-sys", 1483 | "webpki-roots", 1484 | "winreg", 1485 | ] 1486 | 1487 | [[package]] 1488 | name = "ring" 1489 | version = "0.17.14" 1490 | source = "registry+https://github.com/rust-lang/crates.io-index" 1491 | checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" 1492 | dependencies = [ 1493 | "cc", 1494 | "cfg-if", 1495 | "getrandom 0.2.16", 1496 | "libc", 1497 | "untrusted", 1498 | "windows-sys 0.52.0", 1499 | ] 1500 | 1501 | [[package]] 1502 | name = "rsa" 1503 | version = "0.9.8" 1504 | source = "registry+https://github.com/rust-lang/crates.io-index" 1505 | checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" 1506 | dependencies = [ 1507 | "const-oid", 1508 | "digest", 1509 | "num-bigint-dig", 1510 | "num-integer", 1511 | "num-traits", 1512 | "pkcs1", 1513 | "pkcs8", 1514 | "rand_core", 1515 | "signature", 1516 | "spki", 1517 | "subtle", 1518 | "zeroize", 1519 | ] 1520 | 1521 | [[package]] 1522 | name = "rustc-demangle" 1523 | version = "0.1.25" 1524 | source = "registry+https://github.com/rust-lang/crates.io-index" 1525 | checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" 1526 | 1527 | [[package]] 1528 | name = "rustix" 1529 | version = "1.0.7" 1530 | source = "registry+https://github.com/rust-lang/crates.io-index" 1531 | checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" 1532 | dependencies = [ 1533 | "bitflags 2.9.1", 1534 | "errno", 1535 | "libc", 1536 | "linux-raw-sys", 1537 | "windows-sys 0.59.0", 1538 | ] 1539 | 1540 | [[package]] 1541 | name = "rustls" 1542 | version = "0.21.12" 1543 | source = "registry+https://github.com/rust-lang/crates.io-index" 1544 | checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" 1545 | dependencies = [ 1546 | "log", 1547 | "ring", 1548 | "rustls-webpki", 1549 | "sct", 1550 | ] 1551 | 1552 | [[package]] 1553 | name = "rustls-pemfile" 1554 | version = "1.0.4" 1555 | source = "registry+https://github.com/rust-lang/crates.io-index" 1556 | checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" 1557 | dependencies = [ 1558 | "base64 0.21.7", 1559 | ] 1560 | 1561 | [[package]] 1562 | name = "rustls-webpki" 1563 | version = "0.101.7" 1564 | source = "registry+https://github.com/rust-lang/crates.io-index" 1565 | checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" 1566 | dependencies = [ 1567 | "ring", 1568 | "untrusted", 1569 | ] 1570 | 1571 | [[package]] 1572 | name = "rustversion" 1573 | version = "1.0.21" 1574 | source = "registry+https://github.com/rust-lang/crates.io-index" 1575 | checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" 1576 | 1577 | [[package]] 1578 | name = "ryu" 1579 | version = "1.0.20" 1580 | source = "registry+https://github.com/rust-lang/crates.io-index" 1581 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 1582 | 1583 | [[package]] 1584 | name = "schannel" 1585 | version = "0.1.27" 1586 | source = "registry+https://github.com/rust-lang/crates.io-index" 1587 | checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" 1588 | dependencies = [ 1589 | "windows-sys 0.59.0", 1590 | ] 1591 | 1592 | [[package]] 1593 | name = "scopeguard" 1594 | version = "1.2.0" 1595 | source = "registry+https://github.com/rust-lang/crates.io-index" 1596 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1597 | 1598 | [[package]] 1599 | name = "sct" 1600 | version = "0.7.1" 1601 | source = "registry+https://github.com/rust-lang/crates.io-index" 1602 | checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" 1603 | dependencies = [ 1604 | "ring", 1605 | "untrusted", 1606 | ] 1607 | 1608 | [[package]] 1609 | name = "security-framework" 1610 | version = "2.11.1" 1611 | source = "registry+https://github.com/rust-lang/crates.io-index" 1612 | checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 1613 | dependencies = [ 1614 | "bitflags 2.9.1", 1615 | "core-foundation", 1616 | "core-foundation-sys", 1617 | "libc", 1618 | "security-framework-sys", 1619 | ] 1620 | 1621 | [[package]] 1622 | name = "security-framework-sys" 1623 | version = "2.14.0" 1624 | source = "registry+https://github.com/rust-lang/crates.io-index" 1625 | checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" 1626 | dependencies = [ 1627 | "core-foundation-sys", 1628 | "libc", 1629 | ] 1630 | 1631 | [[package]] 1632 | name = "serde" 1633 | version = "1.0.219" 1634 | source = "registry+https://github.com/rust-lang/crates.io-index" 1635 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 1636 | dependencies = [ 1637 | "serde_derive", 1638 | ] 1639 | 1640 | [[package]] 1641 | name = "serde_derive" 1642 | version = "1.0.219" 1643 | source = "registry+https://github.com/rust-lang/crates.io-index" 1644 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 1645 | dependencies = [ 1646 | "proc-macro2", 1647 | "quote", 1648 | "syn 2.0.104", 1649 | ] 1650 | 1651 | [[package]] 1652 | name = "serde_json" 1653 | version = "1.0.140" 1654 | source = "registry+https://github.com/rust-lang/crates.io-index" 1655 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 1656 | dependencies = [ 1657 | "itoa", 1658 | "memchr", 1659 | "ryu", 1660 | "serde", 1661 | ] 1662 | 1663 | [[package]] 1664 | name = "serde_path_to_error" 1665 | version = "0.1.17" 1666 | source = "registry+https://github.com/rust-lang/crates.io-index" 1667 | checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" 1668 | dependencies = [ 1669 | "itoa", 1670 | "serde", 1671 | ] 1672 | 1673 | [[package]] 1674 | name = "serde_urlencoded" 1675 | version = "0.7.1" 1676 | source = "registry+https://github.com/rust-lang/crates.io-index" 1677 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1678 | dependencies = [ 1679 | "form_urlencoded", 1680 | "itoa", 1681 | "ryu", 1682 | "serde", 1683 | ] 1684 | 1685 | [[package]] 1686 | name = "sha1" 1687 | version = "0.10.6" 1688 | source = "registry+https://github.com/rust-lang/crates.io-index" 1689 | checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 1690 | dependencies = [ 1691 | "cfg-if", 1692 | "cpufeatures", 1693 | "digest", 1694 | ] 1695 | 1696 | [[package]] 1697 | name = "sha2" 1698 | version = "0.10.9" 1699 | source = "registry+https://github.com/rust-lang/crates.io-index" 1700 | checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" 1701 | dependencies = [ 1702 | "cfg-if", 1703 | "cpufeatures", 1704 | "digest", 1705 | ] 1706 | 1707 | [[package]] 1708 | name = "shlex" 1709 | version = "1.3.0" 1710 | source = "registry+https://github.com/rust-lang/crates.io-index" 1711 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1712 | 1713 | [[package]] 1714 | name = "signal-hook-registry" 1715 | version = "1.4.5" 1716 | source = "registry+https://github.com/rust-lang/crates.io-index" 1717 | checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" 1718 | dependencies = [ 1719 | "libc", 1720 | ] 1721 | 1722 | [[package]] 1723 | name = "signature" 1724 | version = "2.2.0" 1725 | source = "registry+https://github.com/rust-lang/crates.io-index" 1726 | checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" 1727 | dependencies = [ 1728 | "digest", 1729 | "rand_core", 1730 | ] 1731 | 1732 | [[package]] 1733 | name = "slab" 1734 | version = "0.4.10" 1735 | source = "registry+https://github.com/rust-lang/crates.io-index" 1736 | checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" 1737 | 1738 | [[package]] 1739 | name = "smallvec" 1740 | version = "1.15.1" 1741 | source = "registry+https://github.com/rust-lang/crates.io-index" 1742 | checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 1743 | 1744 | [[package]] 1745 | name = "socket2" 1746 | version = "0.5.10" 1747 | source = "registry+https://github.com/rust-lang/crates.io-index" 1748 | checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" 1749 | dependencies = [ 1750 | "libc", 1751 | "windows-sys 0.52.0", 1752 | ] 1753 | 1754 | [[package]] 1755 | name = "spin" 1756 | version = "0.9.8" 1757 | source = "registry+https://github.com/rust-lang/crates.io-index" 1758 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 1759 | dependencies = [ 1760 | "lock_api", 1761 | ] 1762 | 1763 | [[package]] 1764 | name = "spki" 1765 | version = "0.7.3" 1766 | source = "registry+https://github.com/rust-lang/crates.io-index" 1767 | checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" 1768 | dependencies = [ 1769 | "base64ct", 1770 | "der", 1771 | ] 1772 | 1773 | [[package]] 1774 | name = "sqlformat" 1775 | version = "0.2.6" 1776 | source = "registry+https://github.com/rust-lang/crates.io-index" 1777 | checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" 1778 | dependencies = [ 1779 | "nom", 1780 | "unicode_categories", 1781 | ] 1782 | 1783 | [[package]] 1784 | name = "sqlx" 1785 | version = "0.7.4" 1786 | source = "registry+https://github.com/rust-lang/crates.io-index" 1787 | checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" 1788 | dependencies = [ 1789 | "sqlx-core", 1790 | "sqlx-macros", 1791 | "sqlx-mysql", 1792 | "sqlx-postgres", 1793 | "sqlx-sqlite", 1794 | ] 1795 | 1796 | [[package]] 1797 | name = "sqlx-core" 1798 | version = "0.7.4" 1799 | source = "registry+https://github.com/rust-lang/crates.io-index" 1800 | checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" 1801 | dependencies = [ 1802 | "ahash", 1803 | "atoi", 1804 | "byteorder", 1805 | "bytes", 1806 | "chrono", 1807 | "crc", 1808 | "crossbeam-queue", 1809 | "either", 1810 | "event-listener", 1811 | "futures-channel", 1812 | "futures-core", 1813 | "futures-intrusive", 1814 | "futures-io", 1815 | "futures-util", 1816 | "hashlink", 1817 | "hex", 1818 | "indexmap", 1819 | "log", 1820 | "memchr", 1821 | "once_cell", 1822 | "paste", 1823 | "percent-encoding", 1824 | "rustls", 1825 | "rustls-pemfile", 1826 | "serde", 1827 | "serde_json", 1828 | "sha2", 1829 | "smallvec", 1830 | "sqlformat", 1831 | "thiserror", 1832 | "tokio", 1833 | "tokio-stream", 1834 | "tracing", 1835 | "url", 1836 | "uuid", 1837 | "webpki-roots", 1838 | ] 1839 | 1840 | [[package]] 1841 | name = "sqlx-macros" 1842 | version = "0.7.4" 1843 | source = "registry+https://github.com/rust-lang/crates.io-index" 1844 | checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" 1845 | dependencies = [ 1846 | "proc-macro2", 1847 | "quote", 1848 | "sqlx-core", 1849 | "sqlx-macros-core", 1850 | "syn 1.0.109", 1851 | ] 1852 | 1853 | [[package]] 1854 | name = "sqlx-macros-core" 1855 | version = "0.7.4" 1856 | source = "registry+https://github.com/rust-lang/crates.io-index" 1857 | checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" 1858 | dependencies = [ 1859 | "dotenvy", 1860 | "either", 1861 | "heck", 1862 | "hex", 1863 | "once_cell", 1864 | "proc-macro2", 1865 | "quote", 1866 | "serde", 1867 | "serde_json", 1868 | "sha2", 1869 | "sqlx-core", 1870 | "sqlx-mysql", 1871 | "sqlx-postgres", 1872 | "sqlx-sqlite", 1873 | "syn 1.0.109", 1874 | "tempfile", 1875 | "tokio", 1876 | "url", 1877 | ] 1878 | 1879 | [[package]] 1880 | name = "sqlx-mysql" 1881 | version = "0.7.4" 1882 | source = "registry+https://github.com/rust-lang/crates.io-index" 1883 | checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" 1884 | dependencies = [ 1885 | "atoi", 1886 | "base64 0.21.7", 1887 | "bitflags 2.9.1", 1888 | "byteorder", 1889 | "bytes", 1890 | "chrono", 1891 | "crc", 1892 | "digest", 1893 | "dotenvy", 1894 | "either", 1895 | "futures-channel", 1896 | "futures-core", 1897 | "futures-io", 1898 | "futures-util", 1899 | "generic-array", 1900 | "hex", 1901 | "hkdf", 1902 | "hmac", 1903 | "itoa", 1904 | "log", 1905 | "md-5", 1906 | "memchr", 1907 | "once_cell", 1908 | "percent-encoding", 1909 | "rand", 1910 | "rsa", 1911 | "serde", 1912 | "sha1", 1913 | "sha2", 1914 | "smallvec", 1915 | "sqlx-core", 1916 | "stringprep", 1917 | "thiserror", 1918 | "tracing", 1919 | "uuid", 1920 | "whoami", 1921 | ] 1922 | 1923 | [[package]] 1924 | name = "sqlx-postgres" 1925 | version = "0.7.4" 1926 | source = "registry+https://github.com/rust-lang/crates.io-index" 1927 | checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" 1928 | dependencies = [ 1929 | "atoi", 1930 | "base64 0.21.7", 1931 | "bitflags 2.9.1", 1932 | "byteorder", 1933 | "chrono", 1934 | "crc", 1935 | "dotenvy", 1936 | "etcetera", 1937 | "futures-channel", 1938 | "futures-core", 1939 | "futures-io", 1940 | "futures-util", 1941 | "hex", 1942 | "hkdf", 1943 | "hmac", 1944 | "home", 1945 | "itoa", 1946 | "log", 1947 | "md-5", 1948 | "memchr", 1949 | "once_cell", 1950 | "rand", 1951 | "serde", 1952 | "serde_json", 1953 | "sha2", 1954 | "smallvec", 1955 | "sqlx-core", 1956 | "stringprep", 1957 | "thiserror", 1958 | "tracing", 1959 | "uuid", 1960 | "whoami", 1961 | ] 1962 | 1963 | [[package]] 1964 | name = "sqlx-sqlite" 1965 | version = "0.7.4" 1966 | source = "registry+https://github.com/rust-lang/crates.io-index" 1967 | checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" 1968 | dependencies = [ 1969 | "atoi", 1970 | "chrono", 1971 | "flume", 1972 | "futures-channel", 1973 | "futures-core", 1974 | "futures-executor", 1975 | "futures-intrusive", 1976 | "futures-util", 1977 | "libsqlite3-sys", 1978 | "log", 1979 | "percent-encoding", 1980 | "serde", 1981 | "sqlx-core", 1982 | "tracing", 1983 | "url", 1984 | "urlencoding", 1985 | "uuid", 1986 | ] 1987 | 1988 | [[package]] 1989 | name = "stable_deref_trait" 1990 | version = "1.2.0" 1991 | source = "registry+https://github.com/rust-lang/crates.io-index" 1992 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1993 | 1994 | [[package]] 1995 | name = "stringprep" 1996 | version = "0.1.5" 1997 | source = "registry+https://github.com/rust-lang/crates.io-index" 1998 | checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" 1999 | dependencies = [ 2000 | "unicode-bidi", 2001 | "unicode-normalization", 2002 | "unicode-properties", 2003 | ] 2004 | 2005 | [[package]] 2006 | name = "subtle" 2007 | version = "2.6.1" 2008 | source = "registry+https://github.com/rust-lang/crates.io-index" 2009 | checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 2010 | 2011 | [[package]] 2012 | name = "syn" 2013 | version = "1.0.109" 2014 | source = "registry+https://github.com/rust-lang/crates.io-index" 2015 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 2016 | dependencies = [ 2017 | "proc-macro2", 2018 | "quote", 2019 | "unicode-ident", 2020 | ] 2021 | 2022 | [[package]] 2023 | name = "syn" 2024 | version = "2.0.104" 2025 | source = "registry+https://github.com/rust-lang/crates.io-index" 2026 | checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" 2027 | dependencies = [ 2028 | "proc-macro2", 2029 | "quote", 2030 | "unicode-ident", 2031 | ] 2032 | 2033 | [[package]] 2034 | name = "sync_wrapper" 2035 | version = "0.1.2" 2036 | source = "registry+https://github.com/rust-lang/crates.io-index" 2037 | checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" 2038 | 2039 | [[package]] 2040 | name = "sync_wrapper" 2041 | version = "1.0.2" 2042 | source = "registry+https://github.com/rust-lang/crates.io-index" 2043 | checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 2044 | 2045 | [[package]] 2046 | name = "synstructure" 2047 | version = "0.13.2" 2048 | source = "registry+https://github.com/rust-lang/crates.io-index" 2049 | checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 2050 | dependencies = [ 2051 | "proc-macro2", 2052 | "quote", 2053 | "syn 2.0.104", 2054 | ] 2055 | 2056 | [[package]] 2057 | name = "system-configuration" 2058 | version = "0.5.1" 2059 | source = "registry+https://github.com/rust-lang/crates.io-index" 2060 | checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" 2061 | dependencies = [ 2062 | "bitflags 1.3.2", 2063 | "core-foundation", 2064 | "system-configuration-sys", 2065 | ] 2066 | 2067 | [[package]] 2068 | name = "system-configuration-sys" 2069 | version = "0.5.0" 2070 | source = "registry+https://github.com/rust-lang/crates.io-index" 2071 | checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" 2072 | dependencies = [ 2073 | "core-foundation-sys", 2074 | "libc", 2075 | ] 2076 | 2077 | [[package]] 2078 | name = "tempfile" 2079 | version = "3.20.0" 2080 | source = "registry+https://github.com/rust-lang/crates.io-index" 2081 | checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" 2082 | dependencies = [ 2083 | "fastrand", 2084 | "getrandom 0.3.3", 2085 | "once_cell", 2086 | "rustix", 2087 | "windows-sys 0.59.0", 2088 | ] 2089 | 2090 | [[package]] 2091 | name = "thiserror" 2092 | version = "1.0.69" 2093 | source = "registry+https://github.com/rust-lang/crates.io-index" 2094 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 2095 | dependencies = [ 2096 | "thiserror-impl", 2097 | ] 2098 | 2099 | [[package]] 2100 | name = "thiserror-impl" 2101 | version = "1.0.69" 2102 | source = "registry+https://github.com/rust-lang/crates.io-index" 2103 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 2104 | dependencies = [ 2105 | "proc-macro2", 2106 | "quote", 2107 | "syn 2.0.104", 2108 | ] 2109 | 2110 | [[package]] 2111 | name = "tinystr" 2112 | version = "0.8.1" 2113 | source = "registry+https://github.com/rust-lang/crates.io-index" 2114 | checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" 2115 | dependencies = [ 2116 | "displaydoc", 2117 | "zerovec", 2118 | ] 2119 | 2120 | [[package]] 2121 | name = "tinyvec" 2122 | version = "1.9.0" 2123 | source = "registry+https://github.com/rust-lang/crates.io-index" 2124 | checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" 2125 | dependencies = [ 2126 | "tinyvec_macros", 2127 | ] 2128 | 2129 | [[package]] 2130 | name = "tinyvec_macros" 2131 | version = "0.1.1" 2132 | source = "registry+https://github.com/rust-lang/crates.io-index" 2133 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 2134 | 2135 | [[package]] 2136 | name = "tokio" 2137 | version = "1.46.1" 2138 | source = "registry+https://github.com/rust-lang/crates.io-index" 2139 | checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" 2140 | dependencies = [ 2141 | "backtrace", 2142 | "bytes", 2143 | "io-uring", 2144 | "libc", 2145 | "mio", 2146 | "parking_lot", 2147 | "pin-project-lite", 2148 | "signal-hook-registry", 2149 | "slab", 2150 | "socket2", 2151 | "tokio-macros", 2152 | "windows-sys 0.52.0", 2153 | ] 2154 | 2155 | [[package]] 2156 | name = "tokio-macros" 2157 | version = "2.5.0" 2158 | source = "registry+https://github.com/rust-lang/crates.io-index" 2159 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 2160 | dependencies = [ 2161 | "proc-macro2", 2162 | "quote", 2163 | "syn 2.0.104", 2164 | ] 2165 | 2166 | [[package]] 2167 | name = "tokio-native-tls" 2168 | version = "0.3.1" 2169 | source = "registry+https://github.com/rust-lang/crates.io-index" 2170 | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 2171 | dependencies = [ 2172 | "native-tls", 2173 | "tokio", 2174 | ] 2175 | 2176 | [[package]] 2177 | name = "tokio-rustls" 2178 | version = "0.24.1" 2179 | source = "registry+https://github.com/rust-lang/crates.io-index" 2180 | checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" 2181 | dependencies = [ 2182 | "rustls", 2183 | "tokio", 2184 | ] 2185 | 2186 | [[package]] 2187 | name = "tokio-stream" 2188 | version = "0.1.17" 2189 | source = "registry+https://github.com/rust-lang/crates.io-index" 2190 | checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" 2191 | dependencies = [ 2192 | "futures-core", 2193 | "pin-project-lite", 2194 | "tokio", 2195 | ] 2196 | 2197 | [[package]] 2198 | name = "tokio-util" 2199 | version = "0.7.15" 2200 | source = "registry+https://github.com/rust-lang/crates.io-index" 2201 | checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" 2202 | dependencies = [ 2203 | "bytes", 2204 | "futures-core", 2205 | "futures-sink", 2206 | "pin-project-lite", 2207 | "tokio", 2208 | ] 2209 | 2210 | [[package]] 2211 | name = "tower" 2212 | version = "0.4.13" 2213 | source = "registry+https://github.com/rust-lang/crates.io-index" 2214 | checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" 2215 | dependencies = [ 2216 | "tower-layer", 2217 | "tower-service", 2218 | "tracing", 2219 | ] 2220 | 2221 | [[package]] 2222 | name = "tower" 2223 | version = "0.5.2" 2224 | source = "registry+https://github.com/rust-lang/crates.io-index" 2225 | checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" 2226 | dependencies = [ 2227 | "futures-core", 2228 | "futures-util", 2229 | "pin-project-lite", 2230 | "sync_wrapper 1.0.2", 2231 | "tokio", 2232 | "tower-layer", 2233 | "tower-service", 2234 | "tracing", 2235 | ] 2236 | 2237 | [[package]] 2238 | name = "tower-http" 2239 | version = "0.5.2" 2240 | source = "registry+https://github.com/rust-lang/crates.io-index" 2241 | checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" 2242 | dependencies = [ 2243 | "bitflags 2.9.1", 2244 | "bytes", 2245 | "http 1.3.1", 2246 | "http-body 1.0.1", 2247 | "http-body-util", 2248 | "pin-project-lite", 2249 | "tower-layer", 2250 | "tower-service", 2251 | ] 2252 | 2253 | [[package]] 2254 | name = "tower-layer" 2255 | version = "0.3.3" 2256 | source = "registry+https://github.com/rust-lang/crates.io-index" 2257 | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 2258 | 2259 | [[package]] 2260 | name = "tower-service" 2261 | version = "0.3.3" 2262 | source = "registry+https://github.com/rust-lang/crates.io-index" 2263 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 2264 | 2265 | [[package]] 2266 | name = "tracing" 2267 | version = "0.1.41" 2268 | source = "registry+https://github.com/rust-lang/crates.io-index" 2269 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 2270 | dependencies = [ 2271 | "log", 2272 | "pin-project-lite", 2273 | "tracing-attributes", 2274 | "tracing-core", 2275 | ] 2276 | 2277 | [[package]] 2278 | name = "tracing-attributes" 2279 | version = "0.1.30" 2280 | source = "registry+https://github.com/rust-lang/crates.io-index" 2281 | checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" 2282 | dependencies = [ 2283 | "proc-macro2", 2284 | "quote", 2285 | "syn 2.0.104", 2286 | ] 2287 | 2288 | [[package]] 2289 | name = "tracing-core" 2290 | version = "0.1.34" 2291 | source = "registry+https://github.com/rust-lang/crates.io-index" 2292 | checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" 2293 | dependencies = [ 2294 | "once_cell", 2295 | ] 2296 | 2297 | [[package]] 2298 | name = "try-lock" 2299 | version = "0.2.5" 2300 | source = "registry+https://github.com/rust-lang/crates.io-index" 2301 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 2302 | 2303 | [[package]] 2304 | name = "typenum" 2305 | version = "1.18.0" 2306 | source = "registry+https://github.com/rust-lang/crates.io-index" 2307 | checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" 2308 | 2309 | [[package]] 2310 | name = "unicode-bidi" 2311 | version = "0.3.18" 2312 | source = "registry+https://github.com/rust-lang/crates.io-index" 2313 | checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" 2314 | 2315 | [[package]] 2316 | name = "unicode-ident" 2317 | version = "1.0.18" 2318 | source = "registry+https://github.com/rust-lang/crates.io-index" 2319 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 2320 | 2321 | [[package]] 2322 | name = "unicode-normalization" 2323 | version = "0.1.24" 2324 | source = "registry+https://github.com/rust-lang/crates.io-index" 2325 | checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" 2326 | dependencies = [ 2327 | "tinyvec", 2328 | ] 2329 | 2330 | [[package]] 2331 | name = "unicode-properties" 2332 | version = "0.1.3" 2333 | source = "registry+https://github.com/rust-lang/crates.io-index" 2334 | checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" 2335 | 2336 | [[package]] 2337 | name = "unicode-segmentation" 2338 | version = "1.12.0" 2339 | source = "registry+https://github.com/rust-lang/crates.io-index" 2340 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 2341 | 2342 | [[package]] 2343 | name = "unicode_categories" 2344 | version = "0.1.1" 2345 | source = "registry+https://github.com/rust-lang/crates.io-index" 2346 | checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" 2347 | 2348 | [[package]] 2349 | name = "untrusted" 2350 | version = "0.9.0" 2351 | source = "registry+https://github.com/rust-lang/crates.io-index" 2352 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 2353 | 2354 | [[package]] 2355 | name = "url" 2356 | version = "2.5.4" 2357 | source = "registry+https://github.com/rust-lang/crates.io-index" 2358 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 2359 | dependencies = [ 2360 | "form_urlencoded", 2361 | "idna", 2362 | "percent-encoding", 2363 | "serde", 2364 | ] 2365 | 2366 | [[package]] 2367 | name = "urlencoding" 2368 | version = "2.1.3" 2369 | source = "registry+https://github.com/rust-lang/crates.io-index" 2370 | checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" 2371 | 2372 | [[package]] 2373 | name = "utf8_iter" 2374 | version = "1.0.4" 2375 | source = "registry+https://github.com/rust-lang/crates.io-index" 2376 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 2377 | 2378 | [[package]] 2379 | name = "uuid" 2380 | version = "1.17.0" 2381 | source = "registry+https://github.com/rust-lang/crates.io-index" 2382 | checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" 2383 | dependencies = [ 2384 | "getrandom 0.3.3", 2385 | "js-sys", 2386 | "serde", 2387 | "wasm-bindgen", 2388 | ] 2389 | 2390 | [[package]] 2391 | name = "vcpkg" 2392 | version = "0.2.15" 2393 | source = "registry+https://github.com/rust-lang/crates.io-index" 2394 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 2395 | 2396 | [[package]] 2397 | name = "version_check" 2398 | version = "0.9.5" 2399 | source = "registry+https://github.com/rust-lang/crates.io-index" 2400 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 2401 | 2402 | [[package]] 2403 | name = "want" 2404 | version = "0.3.1" 2405 | source = "registry+https://github.com/rust-lang/crates.io-index" 2406 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 2407 | dependencies = [ 2408 | "try-lock", 2409 | ] 2410 | 2411 | [[package]] 2412 | name = "wasi" 2413 | version = "0.11.1+wasi-snapshot-preview1" 2414 | source = "registry+https://github.com/rust-lang/crates.io-index" 2415 | checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 2416 | 2417 | [[package]] 2418 | name = "wasi" 2419 | version = "0.14.2+wasi-0.2.4" 2420 | source = "registry+https://github.com/rust-lang/crates.io-index" 2421 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 2422 | dependencies = [ 2423 | "wit-bindgen-rt", 2424 | ] 2425 | 2426 | [[package]] 2427 | name = "wasite" 2428 | version = "0.1.0" 2429 | source = "registry+https://github.com/rust-lang/crates.io-index" 2430 | checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" 2431 | 2432 | [[package]] 2433 | name = "wasm-bindgen" 2434 | version = "0.2.100" 2435 | source = "registry+https://github.com/rust-lang/crates.io-index" 2436 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 2437 | dependencies = [ 2438 | "cfg-if", 2439 | "once_cell", 2440 | "rustversion", 2441 | "wasm-bindgen-macro", 2442 | ] 2443 | 2444 | [[package]] 2445 | name = "wasm-bindgen-backend" 2446 | version = "0.2.100" 2447 | source = "registry+https://github.com/rust-lang/crates.io-index" 2448 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 2449 | dependencies = [ 2450 | "bumpalo", 2451 | "log", 2452 | "proc-macro2", 2453 | "quote", 2454 | "syn 2.0.104", 2455 | "wasm-bindgen-shared", 2456 | ] 2457 | 2458 | [[package]] 2459 | name = "wasm-bindgen-futures" 2460 | version = "0.4.50" 2461 | source = "registry+https://github.com/rust-lang/crates.io-index" 2462 | checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" 2463 | dependencies = [ 2464 | "cfg-if", 2465 | "js-sys", 2466 | "once_cell", 2467 | "wasm-bindgen", 2468 | "web-sys", 2469 | ] 2470 | 2471 | [[package]] 2472 | name = "wasm-bindgen-macro" 2473 | version = "0.2.100" 2474 | source = "registry+https://github.com/rust-lang/crates.io-index" 2475 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 2476 | dependencies = [ 2477 | "quote", 2478 | "wasm-bindgen-macro-support", 2479 | ] 2480 | 2481 | [[package]] 2482 | name = "wasm-bindgen-macro-support" 2483 | version = "0.2.100" 2484 | source = "registry+https://github.com/rust-lang/crates.io-index" 2485 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 2486 | dependencies = [ 2487 | "proc-macro2", 2488 | "quote", 2489 | "syn 2.0.104", 2490 | "wasm-bindgen-backend", 2491 | "wasm-bindgen-shared", 2492 | ] 2493 | 2494 | [[package]] 2495 | name = "wasm-bindgen-shared" 2496 | version = "0.2.100" 2497 | source = "registry+https://github.com/rust-lang/crates.io-index" 2498 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 2499 | dependencies = [ 2500 | "unicode-ident", 2501 | ] 2502 | 2503 | [[package]] 2504 | name = "web-sys" 2505 | version = "0.3.77" 2506 | source = "registry+https://github.com/rust-lang/crates.io-index" 2507 | checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" 2508 | dependencies = [ 2509 | "js-sys", 2510 | "wasm-bindgen", 2511 | ] 2512 | 2513 | [[package]] 2514 | name = "webpki-roots" 2515 | version = "0.25.4" 2516 | source = "registry+https://github.com/rust-lang/crates.io-index" 2517 | checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" 2518 | 2519 | [[package]] 2520 | name = "whoami" 2521 | version = "1.6.0" 2522 | source = "registry+https://github.com/rust-lang/crates.io-index" 2523 | checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" 2524 | dependencies = [ 2525 | "redox_syscall", 2526 | "wasite", 2527 | ] 2528 | 2529 | [[package]] 2530 | name = "windows-core" 2531 | version = "0.61.2" 2532 | source = "registry+https://github.com/rust-lang/crates.io-index" 2533 | checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" 2534 | dependencies = [ 2535 | "windows-implement", 2536 | "windows-interface", 2537 | "windows-link", 2538 | "windows-result", 2539 | "windows-strings", 2540 | ] 2541 | 2542 | [[package]] 2543 | name = "windows-implement" 2544 | version = "0.60.0" 2545 | source = "registry+https://github.com/rust-lang/crates.io-index" 2546 | checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" 2547 | dependencies = [ 2548 | "proc-macro2", 2549 | "quote", 2550 | "syn 2.0.104", 2551 | ] 2552 | 2553 | [[package]] 2554 | name = "windows-interface" 2555 | version = "0.59.1" 2556 | source = "registry+https://github.com/rust-lang/crates.io-index" 2557 | checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" 2558 | dependencies = [ 2559 | "proc-macro2", 2560 | "quote", 2561 | "syn 2.0.104", 2562 | ] 2563 | 2564 | [[package]] 2565 | name = "windows-link" 2566 | version = "0.1.3" 2567 | source = "registry+https://github.com/rust-lang/crates.io-index" 2568 | checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" 2569 | 2570 | [[package]] 2571 | name = "windows-result" 2572 | version = "0.3.4" 2573 | source = "registry+https://github.com/rust-lang/crates.io-index" 2574 | checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" 2575 | dependencies = [ 2576 | "windows-link", 2577 | ] 2578 | 2579 | [[package]] 2580 | name = "windows-strings" 2581 | version = "0.4.2" 2582 | source = "registry+https://github.com/rust-lang/crates.io-index" 2583 | checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" 2584 | dependencies = [ 2585 | "windows-link", 2586 | ] 2587 | 2588 | [[package]] 2589 | name = "windows-sys" 2590 | version = "0.48.0" 2591 | source = "registry+https://github.com/rust-lang/crates.io-index" 2592 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 2593 | dependencies = [ 2594 | "windows-targets 0.48.5", 2595 | ] 2596 | 2597 | [[package]] 2598 | name = "windows-sys" 2599 | version = "0.52.0" 2600 | source = "registry+https://github.com/rust-lang/crates.io-index" 2601 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 2602 | dependencies = [ 2603 | "windows-targets 0.52.6", 2604 | ] 2605 | 2606 | [[package]] 2607 | name = "windows-sys" 2608 | version = "0.59.0" 2609 | source = "registry+https://github.com/rust-lang/crates.io-index" 2610 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 2611 | dependencies = [ 2612 | "windows-targets 0.52.6", 2613 | ] 2614 | 2615 | [[package]] 2616 | name = "windows-sys" 2617 | version = "0.60.2" 2618 | source = "registry+https://github.com/rust-lang/crates.io-index" 2619 | checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 2620 | dependencies = [ 2621 | "windows-targets 0.53.2", 2622 | ] 2623 | 2624 | [[package]] 2625 | name = "windows-targets" 2626 | version = "0.48.5" 2627 | source = "registry+https://github.com/rust-lang/crates.io-index" 2628 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 2629 | dependencies = [ 2630 | "windows_aarch64_gnullvm 0.48.5", 2631 | "windows_aarch64_msvc 0.48.5", 2632 | "windows_i686_gnu 0.48.5", 2633 | "windows_i686_msvc 0.48.5", 2634 | "windows_x86_64_gnu 0.48.5", 2635 | "windows_x86_64_gnullvm 0.48.5", 2636 | "windows_x86_64_msvc 0.48.5", 2637 | ] 2638 | 2639 | [[package]] 2640 | name = "windows-targets" 2641 | version = "0.52.6" 2642 | source = "registry+https://github.com/rust-lang/crates.io-index" 2643 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 2644 | dependencies = [ 2645 | "windows_aarch64_gnullvm 0.52.6", 2646 | "windows_aarch64_msvc 0.52.6", 2647 | "windows_i686_gnu 0.52.6", 2648 | "windows_i686_gnullvm 0.52.6", 2649 | "windows_i686_msvc 0.52.6", 2650 | "windows_x86_64_gnu 0.52.6", 2651 | "windows_x86_64_gnullvm 0.52.6", 2652 | "windows_x86_64_msvc 0.52.6", 2653 | ] 2654 | 2655 | [[package]] 2656 | name = "windows-targets" 2657 | version = "0.53.2" 2658 | source = "registry+https://github.com/rust-lang/crates.io-index" 2659 | checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" 2660 | dependencies = [ 2661 | "windows_aarch64_gnullvm 0.53.0", 2662 | "windows_aarch64_msvc 0.53.0", 2663 | "windows_i686_gnu 0.53.0", 2664 | "windows_i686_gnullvm 0.53.0", 2665 | "windows_i686_msvc 0.53.0", 2666 | "windows_x86_64_gnu 0.53.0", 2667 | "windows_x86_64_gnullvm 0.53.0", 2668 | "windows_x86_64_msvc 0.53.0", 2669 | ] 2670 | 2671 | [[package]] 2672 | name = "windows_aarch64_gnullvm" 2673 | version = "0.48.5" 2674 | source = "registry+https://github.com/rust-lang/crates.io-index" 2675 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 2676 | 2677 | [[package]] 2678 | name = "windows_aarch64_gnullvm" 2679 | version = "0.52.6" 2680 | source = "registry+https://github.com/rust-lang/crates.io-index" 2681 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 2682 | 2683 | [[package]] 2684 | name = "windows_aarch64_gnullvm" 2685 | version = "0.53.0" 2686 | source = "registry+https://github.com/rust-lang/crates.io-index" 2687 | checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 2688 | 2689 | [[package]] 2690 | name = "windows_aarch64_msvc" 2691 | version = "0.48.5" 2692 | source = "registry+https://github.com/rust-lang/crates.io-index" 2693 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 2694 | 2695 | [[package]] 2696 | name = "windows_aarch64_msvc" 2697 | version = "0.52.6" 2698 | source = "registry+https://github.com/rust-lang/crates.io-index" 2699 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 2700 | 2701 | [[package]] 2702 | name = "windows_aarch64_msvc" 2703 | version = "0.53.0" 2704 | source = "registry+https://github.com/rust-lang/crates.io-index" 2705 | checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 2706 | 2707 | [[package]] 2708 | name = "windows_i686_gnu" 2709 | version = "0.48.5" 2710 | source = "registry+https://github.com/rust-lang/crates.io-index" 2711 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 2712 | 2713 | [[package]] 2714 | name = "windows_i686_gnu" 2715 | version = "0.52.6" 2716 | source = "registry+https://github.com/rust-lang/crates.io-index" 2717 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 2718 | 2719 | [[package]] 2720 | name = "windows_i686_gnu" 2721 | version = "0.53.0" 2722 | source = "registry+https://github.com/rust-lang/crates.io-index" 2723 | checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" 2724 | 2725 | [[package]] 2726 | name = "windows_i686_gnullvm" 2727 | version = "0.52.6" 2728 | source = "registry+https://github.com/rust-lang/crates.io-index" 2729 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 2730 | 2731 | [[package]] 2732 | name = "windows_i686_gnullvm" 2733 | version = "0.53.0" 2734 | source = "registry+https://github.com/rust-lang/crates.io-index" 2735 | checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 2736 | 2737 | [[package]] 2738 | name = "windows_i686_msvc" 2739 | version = "0.48.5" 2740 | source = "registry+https://github.com/rust-lang/crates.io-index" 2741 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 2742 | 2743 | [[package]] 2744 | name = "windows_i686_msvc" 2745 | version = "0.52.6" 2746 | source = "registry+https://github.com/rust-lang/crates.io-index" 2747 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 2748 | 2749 | [[package]] 2750 | name = "windows_i686_msvc" 2751 | version = "0.53.0" 2752 | source = "registry+https://github.com/rust-lang/crates.io-index" 2753 | checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 2754 | 2755 | [[package]] 2756 | name = "windows_x86_64_gnu" 2757 | version = "0.48.5" 2758 | source = "registry+https://github.com/rust-lang/crates.io-index" 2759 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 2760 | 2761 | [[package]] 2762 | name = "windows_x86_64_gnu" 2763 | version = "0.52.6" 2764 | source = "registry+https://github.com/rust-lang/crates.io-index" 2765 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 2766 | 2767 | [[package]] 2768 | name = "windows_x86_64_gnu" 2769 | version = "0.53.0" 2770 | source = "registry+https://github.com/rust-lang/crates.io-index" 2771 | checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 2772 | 2773 | [[package]] 2774 | name = "windows_x86_64_gnullvm" 2775 | version = "0.48.5" 2776 | source = "registry+https://github.com/rust-lang/crates.io-index" 2777 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 2778 | 2779 | [[package]] 2780 | name = "windows_x86_64_gnullvm" 2781 | version = "0.52.6" 2782 | source = "registry+https://github.com/rust-lang/crates.io-index" 2783 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 2784 | 2785 | [[package]] 2786 | name = "windows_x86_64_gnullvm" 2787 | version = "0.53.0" 2788 | source = "registry+https://github.com/rust-lang/crates.io-index" 2789 | checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 2790 | 2791 | [[package]] 2792 | name = "windows_x86_64_msvc" 2793 | version = "0.48.5" 2794 | source = "registry+https://github.com/rust-lang/crates.io-index" 2795 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 2796 | 2797 | [[package]] 2798 | name = "windows_x86_64_msvc" 2799 | version = "0.52.6" 2800 | source = "registry+https://github.com/rust-lang/crates.io-index" 2801 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 2802 | 2803 | [[package]] 2804 | name = "windows_x86_64_msvc" 2805 | version = "0.53.0" 2806 | source = "registry+https://github.com/rust-lang/crates.io-index" 2807 | checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 2808 | 2809 | [[package]] 2810 | name = "winreg" 2811 | version = "0.50.0" 2812 | source = "registry+https://github.com/rust-lang/crates.io-index" 2813 | checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" 2814 | dependencies = [ 2815 | "cfg-if", 2816 | "windows-sys 0.48.0", 2817 | ] 2818 | 2819 | [[package]] 2820 | name = "wit-bindgen-rt" 2821 | version = "0.39.0" 2822 | source = "registry+https://github.com/rust-lang/crates.io-index" 2823 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 2824 | dependencies = [ 2825 | "bitflags 2.9.1", 2826 | ] 2827 | 2828 | [[package]] 2829 | name = "writeable" 2830 | version = "0.6.1" 2831 | source = "registry+https://github.com/rust-lang/crates.io-index" 2832 | checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" 2833 | 2834 | [[package]] 2835 | name = "yoke" 2836 | version = "0.8.0" 2837 | source = "registry+https://github.com/rust-lang/crates.io-index" 2838 | checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" 2839 | dependencies = [ 2840 | "serde", 2841 | "stable_deref_trait", 2842 | "yoke-derive", 2843 | "zerofrom", 2844 | ] 2845 | 2846 | [[package]] 2847 | name = "yoke-derive" 2848 | version = "0.8.0" 2849 | source = "registry+https://github.com/rust-lang/crates.io-index" 2850 | checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" 2851 | dependencies = [ 2852 | "proc-macro2", 2853 | "quote", 2854 | "syn 2.0.104", 2855 | "synstructure", 2856 | ] 2857 | 2858 | [[package]] 2859 | name = "zerocopy" 2860 | version = "0.8.26" 2861 | source = "registry+https://github.com/rust-lang/crates.io-index" 2862 | checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" 2863 | dependencies = [ 2864 | "zerocopy-derive", 2865 | ] 2866 | 2867 | [[package]] 2868 | name = "zerocopy-derive" 2869 | version = "0.8.26" 2870 | source = "registry+https://github.com/rust-lang/crates.io-index" 2871 | checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" 2872 | dependencies = [ 2873 | "proc-macro2", 2874 | "quote", 2875 | "syn 2.0.104", 2876 | ] 2877 | 2878 | [[package]] 2879 | name = "zerofrom" 2880 | version = "0.1.6" 2881 | source = "registry+https://github.com/rust-lang/crates.io-index" 2882 | checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 2883 | dependencies = [ 2884 | "zerofrom-derive", 2885 | ] 2886 | 2887 | [[package]] 2888 | name = "zerofrom-derive" 2889 | version = "0.1.6" 2890 | source = "registry+https://github.com/rust-lang/crates.io-index" 2891 | checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 2892 | dependencies = [ 2893 | "proc-macro2", 2894 | "quote", 2895 | "syn 2.0.104", 2896 | "synstructure", 2897 | ] 2898 | 2899 | [[package]] 2900 | name = "zeroize" 2901 | version = "1.8.1" 2902 | source = "registry+https://github.com/rust-lang/crates.io-index" 2903 | checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 2904 | 2905 | [[package]] 2906 | name = "zerotrie" 2907 | version = "0.2.2" 2908 | source = "registry+https://github.com/rust-lang/crates.io-index" 2909 | checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" 2910 | dependencies = [ 2911 | "displaydoc", 2912 | "yoke", 2913 | "zerofrom", 2914 | ] 2915 | 2916 | [[package]] 2917 | name = "zerovec" 2918 | version = "0.11.2" 2919 | source = "registry+https://github.com/rust-lang/crates.io-index" 2920 | checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" 2921 | dependencies = [ 2922 | "yoke", 2923 | "zerofrom", 2924 | "zerovec-derive", 2925 | ] 2926 | 2927 | [[package]] 2928 | name = "zerovec-derive" 2929 | version = "0.11.1" 2930 | source = "registry+https://github.com/rust-lang/crates.io-index" 2931 | checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" 2932 | dependencies = [ 2933 | "proc-macro2", 2934 | "quote", 2935 | "syn 2.0.104", 2936 | ] 2937 | --------------------------------------------------------------------------------