This commit is contained in:
2025-06-08 13:27:50 -04:00
commit 41c4eca4f5
18 changed files with 2656 additions and 0 deletions

2257
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

16
Cargo.toml Executable file
View File

@@ -0,0 +1,16 @@
[package]
name = "api_rust"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = "0.6"
tokio = { version = "1.0", features = ["full"] }
sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "postgres"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
argon2 = "0.4"
jsonwebtoken = "8.1"
chrono = "0.4"
dotenv = "0.15"
tower-http = { version = "0.4", features = ["cors"] }

13
Dockerfile Normal file
View File

@@ -0,0 +1,13 @@
# Build stage
FROM rust:latest AS builder
WORKDIR /usr/src/app
COPY . .
RUN cargo install cargo-watch
RUN cargo build --release
# Runtime stage
FROM debian:bookworm-slim
WORKDIR /usr/local/bin
COPY --from=builder /usr/src/app/target/release/api_rust .
EXPOSE 9552
CMD ["./api_rust"]

15
Dockerfile.dev Normal file
View File

@@ -0,0 +1,15 @@
# Development stage for Rust API
FROM rust:latest AS builder
WORKDIR /usr/src/app
COPY . .
RUN cargo install cargo-watch
RUN cargo build --release
# Runtime stage for development
FROM rust:latest
WORKDIR /usr/src/app
COPY --from=builder /usr/src/app/target/release/api_rust /usr/local/bin/api_rust
COPY --from=builder /usr/local/cargo/bin/cargo-watch /usr/local/bin/cargo-watch
COPY . .
EXPOSE 9552
CMD ["cargo-watch", "-x", "run"]

5
schema.sql Executable file
View File

@@ -0,0 +1,5 @@
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(255) UNIQUE NOT NULL,
password_hash TEXT NOT NULL
);

100
src/auth/auth.rs Executable file
View File

@@ -0,0 +1,100 @@
use axum::{
extract::{State, Json},
http::StatusCode,
response::IntoResponse,
routing::post,
Router,
};
use crate::auth::structs::{AppState, User, RegisterRequest, LoginRequest, Claims};
use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
Argon2,
};
use jsonwebtoken::{encode, Header, EncodingKey};
// Create auth router
pub fn router() -> Router<AppState> {
Router::new()
.route("/auth/register", post(register))
.route("/auth/login", post(login))
}
// Register endpoint
async fn register(
State(state): State<AppState>,
Json(payload): Json<RegisterRequest>,
) -> impl IntoResponse {
// Check if username exists
let exists = sqlx::query("SELECT 1 FROM users WHERE username = $1")
.bind(&payload.username)
.fetch_optional(&state.db)
.await
.unwrap();
if exists.is_some() {
return (StatusCode::BAD_REQUEST, "Username already exists").into_response();
}
// Hash password
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let password_hash = argon2
.hash_password(payload.password.as_bytes(), &salt)
.unwrap()
.to_string();
// Insert user
let result = sqlx::query_as::<_, User>(
"INSERT INTO users (username, password) VALUES ($1, $2) RETURNING id, username, password",
)
.bind(&payload.username)
.bind(&password_hash)
.fetch_one(&state.db)
.await;
match result {
Ok(user) => (StatusCode::CREATED, Json(user)).into_response(),
Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Failed to register").into_response(),
}
}
// Login endpoint
async fn login(
State(state): State<AppState>,
Json(payload): Json<LoginRequest>,
) -> impl IntoResponse {
// Fetch user
let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE username = $1")
.bind(&payload.username)
.fetch_optional(&state.db)
.await
.unwrap();
let user = match user {
Some(u) => u,
None => return (StatusCode::UNAUTHORIZED, "Invalid credentials").into_response(),
};
// Verify password
let parsed_hash = PasswordHash::new(&user.password).unwrap();
if Argon2::default()
.verify_password(payload.password.as_bytes(), &parsed_hash)
.is_err()
{
return (StatusCode::UNAUTHORIZED, "Invalid credentials").into_response();
}
// Generate JWT
let claims = Claims {
sub: user.username,
exp: (chrono::Utc::now() + chrono::Duration::hours(24)).timestamp() as usize,
};
let token = encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(state.jwt_secret.as_bytes()),
)
.unwrap();
(StatusCode::OK, Json(serde_json::json!({ "token": token }))).into_response()
}

2
src/auth/mod.rs Executable file
View File

@@ -0,0 +1,2 @@
pub mod auth;
pub mod structs;

40
src/auth/structs.rs Executable file
View File

@@ -0,0 +1,40 @@
// src/auth/structs.rs
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use sqlx::PgPool;
#[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct User {
pub id: i32,
pub username: String,
pub password: String,
pub created: String,
pub email: Option<String>,
pub last_login: Option<String>,
pub owner: Option<i32>,
}
#[derive(Debug, Clone)]
pub struct AppState {
pub db: PgPool,
pub jwt_secret: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RegisterRequest {
pub username: String,
pub password: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LoginRequest {
pub username: String,
pub password: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
pub sub: String,
pub exp: usize,
}

0
src/company/company.rs Normal file
View File

2
src/company/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod company;
pub mod structs;

21
src/company/structs.rs Normal file
View File

@@ -0,0 +1,21 @@
// src/company/struct.rs
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use chrono::NaiveDate;
#[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct Company {
pub id: i32,
pub active: bool,
pub created: NaiveDate,
pub name: String,
pub address: Option<String>,
pub town: Option<String>,
pub state: Option<String>,
pub phone: Option<i64>,
pub owner_name: Option<String>,
pub owner_phone_number: Option<i64>,
pub email: Option<String>,
pub user_id: Option<i32>,
}

21
src/data/data.rs Normal file
View File

@@ -0,0 +1,21 @@
use axum::{
extract::State,
http::StatusCode,
response::IntoResponse,
routing::get,
Router,
};
use crate::auth::structs::AppState;
// Define the handler for the /user/ endpoint
async fn get_user(State(state): State<AppState>) -> impl IntoResponse {
// Placeholder for user data retrieval logic
// In a real application, you would query the database using state.db
(StatusCode::OK, "User data retrieved successfully")
}
// Define the router for the data module
pub fn router() -> Router<AppState> {
Router::new()
.route("/user/", get(get_user))
}

1
src/data/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub mod data;

10
src/data/struct.rs Normal file
View File

@@ -0,0 +1,10 @@
// src/data/struct.rs
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
#[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct County {
pub id: i32,
pub name: String,
pub state: i32,
}

59
src/main.rs Executable file
View File

@@ -0,0 +1,59 @@
use axum::{
http::{header, Method},
Router,
};
use std::env;
use tower_http::cors::{CorsLayer, Any};
use crate::auth::structs::AppState;
use crate::auth::auth::router as auth_router;
use crate::data::data::router as data_router;
use crate::state::data::{get_counties_by_state, get_county_by_id};
mod auth;
mod data;
mod state;
#[tokio::main]
async fn main() {
// Load environment variables
dotenv::dotenv().ok();
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
let jwt_secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set");
let frontend_origin = env::var("FRONTEND_ORIGIN").unwrap_or_else(|_| "http://localhost:9551".to_string());
// Connect to PostgreSQL
let db = sqlx::PgPool::connect(&database_url)
.await
.expect("Failed to connect to database");
// Create app state
let state = AppState { db, jwt_secret };
// Configure CORS
let cors = CorsLayer::new()
.allow_origin(frontend_origin.parse::<header::HeaderValue>().unwrap())
.allow_methods([Method::GET, Method::POST])
.allow_headers(Any);
// Build router
let app = Router::new()
.route("/", axum::routing::get(|| async { "API is running" }))
.merge(auth_router())
.merge(data_router())
.route("/state/:state_abbr", axum::routing::get(get_counties_by_state))
.route("/state/:state_abbr/:county_id", axum::routing::get(get_county_by_id))
.layer(cors)
.with_state(state);
// Print server status
// use std::io::Write;
println!("Server is running on http://0.0.0.0:9552");
// std::io::stdout().flush().unwrap();
// Run server
axum::Server::bind(&"0.0.0.0:9552".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}

78
src/state/data.rs Normal file
View File

@@ -0,0 +1,78 @@
use axum::{
extract::{Path, State},
response::Json,
http::StatusCode,
};
use serde::Serialize;
use sqlx::PgPool;
use crate::auth::structs::AppState; // Import AppState
use crate::state::structs::{County, ErrorResponse};
pub async fn get_counties_by_state(
State(app_state): State<AppState>, // Changed from State<PgPool> to State<AppState>
Path(state_abbr): Path<String>,
) -> Result<Json<Vec<County>>, (StatusCode, Json<ErrorResponse>)> {
let state_abbr_upper = state_abbr.to_uppercase(); // Ensure consistent casing for DB query
println!("Querying counties for state: {}", state_abbr_upper);
match sqlx::query_as::<_, County>("SELECT id, name, state FROM county WHERE UPPER(state) = $1 ORDER BY name ASC")
.bind(&state_abbr_upper)
.fetch_all(&app_state.db) // Use app_state.db to access the pool
.await
{
Ok(counties) => {
if counties.is_empty() {
Err((
StatusCode::NOT_FOUND,
Json(ErrorResponse {
error: format!("No counties found for state abbreviation: {}", state_abbr_upper),
}),
))
} else {
Ok(Json(counties))
}
}
Err(e) => {
eprintln!("Database error fetching counties for state {}: {}", state_abbr_upper, e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "Failed to retrieve counties. Please try again later.".to_string(),
}),
))
}
}
}
pub async fn get_county_by_id(
State(app_state): State<AppState>,
Path((state_abbr, county_id)): Path<(String, i32)>,
) -> Result<Json<County>, (StatusCode, Json<ErrorResponse>)> {
let state_abbr_upper = state_abbr.to_uppercase();
println!("Querying county with ID: {} for state: {}", county_id, state_abbr_upper);
match sqlx::query_as::<_, County>("SELECT id, name, state FROM county WHERE UPPER(state) = $1 AND id = $2")
.bind(&state_abbr_upper)
.bind(county_id)
.fetch_one(&app_state.db)
.await
{
Ok(county) => Ok(Json(county)),
Err(sqlx::Error::RowNotFound) => Err((
StatusCode::NOT_FOUND,
Json(ErrorResponse {
error: format!("County with ID {} not found in state {}", county_id, state_abbr_upper),
}),
)),
Err(e) => {
eprintln!("Database error fetching county with ID {} for state {}: {}", county_id, state_abbr_upper, e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "Failed to retrieve county. Please try again later.".to_string(),
}),
))
}
}
}

2
src/state/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod data;
pub mod structs;

14
src/state/structs.rs Normal file
View File

@@ -0,0 +1,14 @@
use serde::Serialize;
use sqlx::FromRow;
#[derive(Serialize, FromRow)]
pub struct County {
id: i32, // Assuming county table has an integer primary key id
name: String,
state: String, // This will be the state abbreviation
}
#[derive(Serialize)]
pub struct ErrorResponse {
pub error: String,
}