first commit
This commit is contained in:
25
server/Cargo.toml
Normal file
25
server/Cargo.toml
Normal 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
325
server/src/main.rs
Normal 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),
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user