Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,5 @@ bcrypt = "0.15"
dotenvy = "0.15"
tower-http = { version = "0.5", features = ["cors", "trace"] }
async-trait = "0.1"
urlencoding = "2.1"

2 changes: 0 additions & 2 deletions apps/api/src/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,12 @@ use serde_json::{json, Value};
pub fn create_router() -> Router {
Router::new()
.route("/health", get(health_check))

// --- V1 Routes ---
.route("/api/v1/auth/login", post(login_v1))
.route("/api/v1/repositories", get(list_repositories_v1))
.route("/api/v1/resumes/generate", post(generate_resume_v1))
.route("/api/v1/ats/score", post(ats_score_v1))
.route("/api/v1/analytics/overview", get(analytics_v1))

// --- V2 Enterprise Routes ---
.route("/api/v2/search/hybrid", post(hybrid_search_v2))
.route("/api/v2/career/insights", get(career_insights_v2))
Expand Down
4 changes: 3 additions & 1 deletion crates/ai/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ pub mod provider;
pub mod rag;
pub mod tokenizer;

pub use provider::{AIProvider, ChatMessage, ClaudeProvider, GeminiProvider, OllamaProvider, OpenAIProvider};
pub use provider::{
AIProvider, ChatMessage, ClaudeProvider, GeminiProvider, OllamaProvider, OpenAIProvider,
};
5 changes: 4 additions & 1 deletion crates/ai/src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@ pub struct OllamaProvider {
#[async_trait]
impl AIProvider for OllamaProvider {
async fn generate(&self, prompt: &str) -> Result<String> {
Ok(format!("[Ollama Local Response] Processed prompt: {}", prompt))
Ok(format!(
"[Ollama Local Response] Processed prompt: {}",
prompt
))
}
async fn embeddings(&self, _text: &str) -> Result<Vec<f32>> {
Ok(vec![0.05; 1536])
Expand Down
6 changes: 5 additions & 1 deletion crates/ai/src/rag.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
pub fn build_rag_context(retrieved_chunks: &[String], user_query: &str) -> String {
format!("Context:\n{}\n\nUser Question: {}", retrieved_chunks.join("\n"), user_query)
format!(
"Context:\n{}\n\nUser Question: {}",
retrieved_chunks.join("\n"),
user_query
)
}
6 changes: 5 additions & 1 deletion crates/ats/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ pub fn analyze_resume_ats(resume_text: &str) -> AtsScoreResult {

AtsScoreResult {
overall_score: score.min(100),
keyword_matches: vec!["Rust".to_string(), "PostgreSQL".to_string(), "API".to_string()],
keyword_matches: vec![
"Rust".to_string(),
"PostgreSQL".to_string(),
"API".to_string(),
],
formatting_score: 95,
suggestions,
}
Expand Down
3 changes: 3 additions & 0 deletions crates/auth/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ uuid.workspace = true
chrono.workspace = true
thiserror.workspace = true
axum.workspace = true
reqwest.workspace = true
urlencoding.workspace = true

100 changes: 95 additions & 5 deletions crates/auth/src/jwt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,46 @@ use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}
use serde::{Deserialize, Serialize};
use uuid::Uuid;

#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Claims {
pub sub: String,
pub email: String,
pub exp: usize,
pub iat: usize,
}

pub fn create_jwt(user_id: Uuid, email: &str, secret: &str) -> Result<String, jsonwebtoken::errors::Error> {
let expiration = Utc::now()
.checked_add_signed(Duration::days(7))
pub fn create_jwt(
user_id: Uuid,
email: &str,
secret: &str,
) -> Result<String, jsonwebtoken::errors::Error> {
create_jwt_with_ttl(user_id, email, secret, Duration::days(7))
}

pub fn create_jwt_with_ttl(
user_id: Uuid,
email: &str,
secret: &str,
ttl: Duration,
) -> Result<String, jsonwebtoken::errors::Error> {
let now = Utc::now();
let expiration = now
.checked_add_signed(ttl)
.expect("valid timestamp")
.timestamp() as usize;
Comment on lines +29 to 32

let claims = Claims {
sub: user_id.to_string(),
email: email.to_string(),
exp: expiration,
iat: now.timestamp() as usize,
};

encode(&Header::default(), &claims, &EncodingKey::from_secret(secret.as_bytes()))
encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(secret.as_bytes()),
)
}

pub fn verify_jwt(token: &str, secret: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
Expand All @@ -34,3 +54,73 @@ pub fn verify_jwt(token: &str, secret: &str) -> Result<Claims, jsonwebtoken::err

Ok(token_data.claims)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_create_and_verify_jwt() {
let user_id = Uuid::new_v4();
let email = "dev@devresume.ai";
let secret = "super_secret_jwt_key_for_unit_tests";

let token = create_jwt(user_id, email, secret).expect("Token generation should succeed");
assert!(!token.is_empty());

let claims = verify_jwt(&token, secret).expect("Token verification should succeed");
assert_eq!(claims.sub, user_id.to_string());
assert_eq!(claims.email, email);
}

#[test]
fn test_invalid_secret_fails_verification() {
let user_id = Uuid::new_v4();
let email = "dev@devresume.ai";
let secret = "correct_secret";
let wrong_secret = "wrong_secret";

let token = create_jwt(user_id, email, secret).expect("Token generation should succeed");
let result = verify_jwt(&token, wrong_secret);

assert!(result.is_err());
}

#[test]
fn test_malformed_token_fails_verification() {
let secret = "secret";
assert!(verify_jwt("not_a_valid_jwt_token", secret).is_err());
assert!(verify_jwt("header.payload", secret).is_err());
assert!(verify_jwt("", secret).is_err());
}

#[test]
fn test_expired_token_fails_verification() {
let user_id = Uuid::new_v4();
let email = "dev@devresume.ai";
let secret = "secret";

// jsonwebtoken has a default 60-second leeway, so set -120 seconds to guarantee expiration
let token = create_jwt_with_ttl(user_id, email, secret, Duration::seconds(-120))
.expect("Token generation should succeed");

let result = verify_jwt(&token, secret);
assert!(result.is_err());
}

#[test]
fn test_tampered_token_fails_verification() {
let user_id = Uuid::new_v4();
let email = "dev@devresume.ai";
let secret = "super_secret_key";

let token = create_jwt(user_id, email, secret).expect("Token generation should succeed");
let parts: Vec<&str> = token.split('.').collect();
assert_eq!(parts.len(), 3);

// Tamper with payload
let tampered_payload = format!("{}.eyJzdWIiOiJoYWNrZXIifQ.{}", parts[0], parts[2]);
let result = verify_jwt(&tampered_payload, secret);
assert!(result.is_err());
}
}
23 changes: 12 additions & 11 deletions crates/auth/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
pub mod api_keys;
pub mod jwt;
pub mod middleware;
pub mod oauth;
pub mod passwords;
pub mod permissions;
pub mod rbac;
pub mod refresh_tokens;

pub use api_keys::generate_api_key;
pub use jwt::{create_jwt, verify_jwt, Claims};
pub use oauth::validate_oauth_provider;
pub use permissions::check_permission;
pub use jwt::{create_jwt, create_jwt_with_ttl, verify_jwt, Claims};
pub use middleware::{AuthError, AuthUser};
pub use oauth::{
validate_oauth_provider, GitHubOAuthClient, GitHubOAuthError, GitHubUserProfile,
GoogleOAuthClient, GoogleUserProfile, OAuthCallbackQuery, OAuthSessionResult, OAuthState,
};
pub use passwords::{hash_password, verify_password};
pub use permissions::{
check_permission, get_permissions_for_role, role_has_permission, Permission,
};
pub use rbac::{has_role, Role};
pub use refresh_tokens::generate_refresh_token;

pub fn hash_password(password: &str) -> Result<String, bcrypt::BcryptError> {
bcrypt::hash(password, bcrypt::DEFAULT_COST)
}

pub fn verify_password(password: &str, hash: &str) -> Result<bool, bcrypt::BcryptError> {
bcrypt::verify(password, hash)
}
70 changes: 70 additions & 0 deletions crates/auth/src/middleware.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use crate::jwt::{verify_jwt, Claims};
use axum::{
async_trait,
extract::FromRequestParts,
http::{header, request::Parts, StatusCode},
response::{IntoResponse, Response},
Json,
};
use serde_json::json;

#[derive(Debug, Clone)]
pub struct AuthUser {
pub id: String,
pub email: String,
}

pub struct AuthError(pub StatusCode, pub String);

impl IntoResponse for AuthError {
fn into_response(self) -> Response {
let body = Json(json!({
"error": self.1,
"status": self.0.as_u16(),
}));
(self.0, body).into_response()
}
}

#[async_trait]
impl<S> FromRequestParts<S> for AuthUser
where
S: Send + Sync,
{
type Rejection = AuthError;

async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let auth_header = parts
.headers
.get(header::AUTHORIZATION)
.and_then(|val| val.to_str().ok())
.ok_or_else(|| {
AuthError(
StatusCode::UNAUTHORIZED,
"Missing Authorization header".to_string(),
)
})?;

let token = auth_header.strip_prefix("Bearer ").ok_or_else(|| {
AuthError(
StatusCode::UNAUTHORIZED,
"Invalid Authorization scheme".to_string(),
)
})?;

let jwt_secret = std::env::var("JWT_SECRET")
.unwrap_or_else(|_| "devresume_jwt_secret_key_32_chars_min".to_string());

Comment on lines +55 to +57
let claims: Claims = verify_jwt(token, &jwt_secret).map_err(|_| {
AuthError(
StatusCode::UNAUTHORIZED,
"Invalid or expired JWT token".to_string(),
)
})?;

Ok(AuthUser {
id: claims.sub,
email: claims.email,
})
}
}
3 changes: 0 additions & 3 deletions crates/auth/src/oauth.rs

This file was deleted.

17 changes: 17 additions & 0 deletions crates/auth/src/oauth/callback.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize)]
pub struct OAuthCallbackQuery {
pub code: String,
pub state: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct OAuthSessionResult {
pub user_id: String,
pub email: String,
pub username: String,
pub avatar_url: String,
pub access_token: String,
pub refresh_token: String,
}
Loading