first
This commit is contained in:
2257
Cargo.lock
generated
Normal file
2257
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
Cargo.toml
Executable file
16
Cargo.toml
Executable 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
13
Dockerfile
Normal 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
15
Dockerfile.dev
Normal 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
5
schema.sql
Executable 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
100
src/auth/auth.rs
Executable 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
2
src/auth/mod.rs
Executable file
@@ -0,0 +1,2 @@
|
||||
pub mod auth;
|
||||
pub mod structs;
|
||||
40
src/auth/structs.rs
Executable file
40
src/auth/structs.rs
Executable 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
0
src/company/company.rs
Normal file
2
src/company/mod.rs
Normal file
2
src/company/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod company;
|
||||
pub mod structs;
|
||||
21
src/company/structs.rs
Normal file
21
src/company/structs.rs
Normal 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
21
src/data/data.rs
Normal 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
1
src/data/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod data;
|
||||
10
src/data/struct.rs
Normal file
10
src/data/struct.rs
Normal 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
59
src/main.rs
Executable 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
78
src/state/data.rs
Normal 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
2
src/state/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod data;
|
||||
pub mod structs;
|
||||
14
src/state/structs.rs
Normal file
14
src/state/structs.rs
Normal 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,
|
||||
}
|
||||
Reference in New Issue
Block a user