first commit

This commit is contained in:
Timur Gordon
2025-09-24 05:11:15 +02:00
commit be061409af
19 changed files with 6052 additions and 0 deletions

25
server/Cargo.toml Normal file
View File

@@ -0,0 +1,25 @@
[package]
name = "self-server"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "server"
path = "src/main.rs"
[dependencies]
tokio = { version = "1.0", features = ["full"] }
axum = "0.7"
tower = "0.4"
tower-http = { version = "0.5", features = ["cors", "fs"] }
serde = { workspace = true }
serde_json = { workspace = true }
uuid = { version = "1.0", features = ["v4"] }
lettre = "0.11"
tokio-stream = { version = "0.1", features = ["sync"] }
futures-util = "0.3"
async-stream = "0.3"
tracing = "0.1"
tracing-subscriber = "0.3"
anyhow = "1.0"
clap = { version = "4.0", features = ["derive"] }

325
server/src/main.rs Normal file
View File

@@ -0,0 +1,325 @@
use axum::{
extract::{Path, Request, State},
http::{header, StatusCode},
middleware::Next,
response::{IntoResponse, Response, Sse},
routing::{get, post},
Json, Router,
};
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
sync::{Arc, Mutex},
time::Duration,
};
use tokio::sync::broadcast;
use tokio_stream::{wrappers::BroadcastStream, StreamExt};
use tower_http::cors::CorsLayer;
use tracing::{info, warn};
use uuid::Uuid;
use clap::Parser;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
/// Port to run the server on
#[arg(short, long, default_value_t = 8080)]
port: u16,
/// Base URL for verification links
#[arg(long, default_value = "http://localhost:8080")]
base_url: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct EmailVerificationRequest {
email: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct RegistrationRequest {
email: String,
public_key: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct RegistrationResponse {
success: bool,
message: String,
user_id: Option<String>,
}
#[derive(Debug, Clone)]
struct VerificationStatus {
email: String,
verified: bool,
verification_token: String,
}
type VerificationStore = Arc<Mutex<HashMap<String, VerificationStatus>>>;
type NotificationSender = broadcast::Sender<String>;
#[derive(Clone)]
struct AppState {
verifications: VerificationStore,
notification_tx: NotificationSender,
base_url: String,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let args = Args::parse();
tracing_subscriber::fmt::init();
let verifications: VerificationStore = Arc::new(Mutex::new(HashMap::new()));
let (notification_tx, _) = broadcast::channel(100);
let state = AppState {
verifications,
notification_tx,
base_url: args.base_url.clone(),
};
let app = Router::new()
.route("/api/send-verification", post(send_verification))
.route("/api/verification-status/:email", get(verification_status_sse))
.route("/api/verify/:token", get(verify_email))
.route("/api/register", post(register_user))
.route("/health", get(health_check))
.layer(axum::middleware::from_fn(log_requests))
.layer(
CorsLayer::new()
.allow_origin(tower_http::cors::Any)
.allow_methods([axum::http::Method::GET, axum::http::Method::POST])
.allow_headers([header::CONTENT_TYPE, header::AUTHORIZATION])
.allow_credentials(false),
)
.with_state(state);
let bind_address = format!("127.0.0.1:{}", args.port);
let listener = tokio::net::TcpListener::bind(&bind_address).await?;
info!("Server running on http://{}", bind_address);
axum::serve(listener, app).await?;
Ok(())
}
async fn log_requests(req: Request, next: Next) -> Response {
let method = req.method().clone();
let uri = req.uri().clone();
info!("🌐 {} {}", method, uri);
next.run(req).await
}
async fn health_check() -> impl IntoResponse {
Json(serde_json::json!({
"status": "healthy",
"service": "self-server"
}))
}
async fn send_verification(
State(state): State<AppState>,
Json(request): Json<EmailVerificationRequest>,
) -> impl IntoResponse {
info!("📧 Received email verification request for: {}", request.email);
let verification_token = Uuid::new_v4().to_string();
let verification_url = format!("{}/api/verify/{}", state.base_url, verification_token);
// Store verification status
{
let mut verifications = state.verifications.lock().unwrap();
verifications.insert(
request.email.clone(),
VerificationStatus {
email: request.email.clone(),
verified: false,
verification_token: verification_token.clone(),
},
);
}
// For development: output verification link to console (no SMTP needed)
info!(
"Email verification requested for: {} - Verification URL: {}",
request.email, verification_url
);
// Display verification link in console for development
let verification_url_clone = verification_url.clone();
let email_clone = request.email.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(1)).await;
println!("\n🔗 EMAIL VERIFICATION LINK for {}:", email_clone);
println!(" {}", verification_url_clone);
println!(" Click this link to verify your email\n");
});
Json(serde_json::json!({
"success": true,
"message": "Verification email sent",
"verification_url": verification_url // Remove in production
}))
}
async fn verification_status_sse(
Path(email): Path<String>,
State(state): State<AppState>,
) -> impl IntoResponse {
let mut rx = state.notification_tx.subscribe();
let stream = async_stream::stream! {
loop {
match rx.recv().await {
Ok(notification) => {
if notification.starts_with(&email) {
let status = notification.split(':').nth(1).unwrap_or("unknown");
yield Ok::<_, axum::Error>(axum::response::sse::Event::default().data(status));
}
}
Err(_) => break,
}
}
};
Sse::new(stream).keep_alive(
axum::response::sse::KeepAlive::new()
.interval(Duration::from_secs(30))
.text("keep-alive"),
)
}
async fn verify_email(
Path(token): Path<String>,
State(state): State<AppState>,
) -> impl IntoResponse {
let mut email_to_notify = None;
// Find and update verification status
{
let mut verifications = state.verifications.lock().unwrap();
for (email, status) in verifications.iter_mut() {
if status.verification_token == token {
status.verified = true;
email_to_notify = Some(email.clone());
break;
}
}
}
match email_to_notify {
Some(email) => {
// Notify via SSE
let notification = format!("{}:verified", email);
let _ = state.notification_tx.send(notification);
info!("Email verified successfully: {}", email);
// Return HTML response for user
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/html")
.body(format!(
r#"
<!DOCTYPE html>
<html>
<head>
<title>Email Verified</title>
<style>
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
text-align: center; padding: 2rem; background: #f8f9fa; }}
.container {{ max-width: 500px; margin: 0 auto; background: white;
padding: 2rem; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); }}
.success {{ color: #198754; font-size: 3rem; margin-bottom: 1rem; }}
h1 {{ color: #333; margin-bottom: 1rem; }}
p {{ color: #666; line-height: 1.6; }}
</style>
</head>
<body>
<div class="container">
<div class="success">✅</div>
<h1>Email Verified Successfully!</h1>
<p>Your email address <strong>{}</strong> has been verified.</p>
<p>You can now close this tab and continue with your registration.</p>
</div>
</body>
</html>
"#,
email
))
.unwrap()
}
None => {
warn!("Invalid verification token: {}", token);
Response::builder()
.status(StatusCode::BAD_REQUEST)
.header("Content-Type", "text/html")
.body(
r#"
<!DOCTYPE html>
<html>
<head>
<title>Verification Failed</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
text-align: center; padding: 2rem; background: #f8f9fa; }
.container { max-width: 500px; margin: 0 auto; background: white;
padding: 2rem; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
.error { color: #dc3545; font-size: 3rem; margin-bottom: 1rem; }
h1 { color: #333; margin-bottom: 1rem; }
p { color: #666; line-height: 1.6; }
</style>
</head>
<body>
<div class="container">
<div class="error">❌</div>
<h1>Verification Failed</h1>
<p>The verification link is invalid or has expired.</p>
<p>Please request a new verification email.</p>
</div>
</body>
</html>
"#.to_string()
)
.unwrap()
}
}
}
async fn register_user(
State(state): State<AppState>,
Json(request): Json<RegistrationRequest>,
) -> impl IntoResponse {
// Check if email is verified
let is_verified = {
let verifications = state.verifications.lock().unwrap();
verifications
.get(&request.email)
.map(|status| status.verified)
.unwrap_or(false)
};
if !is_verified {
return Json(RegistrationResponse {
success: false,
message: "Email not verified".to_string(),
user_id: None,
});
}
// Generate user ID
let user_id = Uuid::new_v4().to_string();
// In a real implementation, store user data in database
info!(
"User registered successfully - Email: {}, Public Key: {}, User ID: {}",
request.email, request.public_key, user_id
);
Json(RegistrationResponse {
success: true,
message: "Registration completed successfully".to_string(),
user_id: Some(user_id),
})
}