jj workspaces over the network
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(server): add authentication - login, tokens, middleware

+159
+159
crates/tandem-server/src/auth.rs
··· 1 + use crate::{AppState, db::UserRow}; 2 + use axum::{ 3 + Json, 4 + extract::{Request, State}, 5 + http::{StatusCode, header}, 6 + middleware::Next, 7 + response::Response, 8 + }; 9 + use chrono::{Duration, Utc}; 10 + use serde::{Deserialize, Serialize}; 11 + 12 + #[derive(Debug, Serialize, Deserialize)] 13 + pub struct LoginRequest { 14 + pub email: String, 15 + pub password: String, 16 + } 17 + 18 + #[derive(Debug, Serialize)] 19 + pub struct LoginResponse { 20 + pub token: String, 21 + pub expires_at: String, 22 + } 23 + 24 + #[derive(Debug, Serialize)] 25 + pub struct UserResponse { 26 + pub id: String, 27 + pub email: String, 28 + pub name: Option<String>, 29 + } 30 + 31 + #[derive(Debug, Clone)] 32 + pub struct AuthenticatedUser { 33 + pub id: String, 34 + pub email: String, 35 + pub name: Option<String>, 36 + } 37 + 38 + impl From<UserRow> for AuthenticatedUser { 39 + fn from(row: UserRow) -> Self { 40 + Self { 41 + id: row.id, 42 + email: row.email, 43 + name: row.name, 44 + } 45 + } 46 + } 47 + 48 + /// Login handler 49 + pub async fn login( 50 + State(state): State<AppState>, 51 + Json(req): Json<LoginRequest>, 52 + ) -> Result<Json<LoginResponse>, StatusCode> { 53 + // Find user by email 54 + let user = state 55 + .db 56 + .get_user_by_email(&req.email) 57 + .await 58 + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? 59 + .ok_or(StatusCode::UNAUTHORIZED)?; 60 + 61 + // Verify password (simple comparison for now - use bcrypt in production) 62 + if !verify_password(&req.password, &user.password_hash) { 63 + return Err(StatusCode::UNAUTHORIZED); 64 + } 65 + 66 + // Generate token 67 + let token = generate_token(); 68 + let expires_at = Utc::now() + Duration::days(7); 69 + 70 + // Store token 71 + state 72 + .db 73 + .create_token(&token, &user.id, expires_at) 74 + .await 75 + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; 76 + 77 + Ok(Json(LoginResponse { 78 + token, 79 + expires_at: expires_at.to_rfc3339(), 80 + })) 81 + } 82 + 83 + /// Get current user handler 84 + pub async fn get_me(user: AuthenticatedUser) -> Json<UserResponse> { 85 + Json(UserResponse { 86 + id: user.id, 87 + email: user.email, 88 + name: user.name, 89 + }) 90 + } 91 + 92 + /// Authentication middleware 93 + pub async fn auth_middleware( 94 + State(state): State<AppState>, 95 + mut request: Request, 96 + next: Next, 97 + ) -> Result<Response, StatusCode> { 98 + // Extract bearer token 99 + let token = request 100 + .headers() 101 + .get(header::AUTHORIZATION) 102 + .and_then(|h| h.to_str().ok()) 103 + .and_then(|h| h.strip_prefix("Bearer ")) 104 + .ok_or(StatusCode::UNAUTHORIZED)?; 105 + 106 + // Verify token and get user 107 + let user = state 108 + .db 109 + .verify_token(token) 110 + .await 111 + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? 112 + .ok_or(StatusCode::UNAUTHORIZED)?; 113 + 114 + // Add user to request extensions 115 + request 116 + .extensions_mut() 117 + .insert(AuthenticatedUser::from(user)); 118 + 119 + Ok(next.run(request).await) 120 + } 121 + 122 + /// Extract authenticated user from request 123 + #[async_trait::async_trait] 124 + impl<S> axum::extract::FromRequestParts<S> for AuthenticatedUser 125 + where 126 + S: Send + Sync, 127 + { 128 + type Rejection = StatusCode; 129 + 130 + async fn from_request_parts( 131 + parts: &mut axum::http::request::Parts, 132 + _state: &S, 133 + ) -> Result<Self, Self::Rejection> { 134 + parts 135 + .extensions 136 + .get::<AuthenticatedUser>() 137 + .cloned() 138 + .ok_or(StatusCode::UNAUTHORIZED) 139 + } 140 + } 141 + 142 + fn generate_token() -> String { 143 + use rand::Rng; 144 + let mut rng = rand::thread_rng(); 145 + let bytes: [u8; 32] = rng.r#gen(); 146 + hex::encode(bytes) 147 + } 148 + 149 + fn verify_password(password: &str, hash: &str) -> bool { 150 + // Simple comparison for prototype - use bcrypt in production 151 + // Hash is just the password for now 152 + password == hash 153 + } 154 + 155 + /// Hash a password (for user creation) 156 + pub fn hash_password(password: &str) -> String { 157 + // Simple passthrough for prototype - use bcrypt in production 158 + password.to_string() 159 + }