diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f604404 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +# Rust build output +target/ + +# Git metadata +.git +.gitignore + +# Local dev configs +.env +.vscode/ +.idea/ + +# OS junk +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/.gitignore b/.gitignore index ea8c4bf..ec47942 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /target + +.env \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 90a3175..5d21979 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -743,6 +743,7 @@ version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" dependencies = [ + "cc", "pkg-config", "vcpkg", ] diff --git a/Cargo.toml b/Cargo.toml index 14b8a3d..3b05a5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ bcrypt = "0.17" r2d2 = "0.8" r2d2_sqlite = "0.31.0" dashmap = "6.1" -rusqlite = "0.37.0" +rusqlite = { version = "0.37", features = ["bundled"] } jsonwebtoken = "9.3.1" dotenvy = "0.15.7" chrono = "0.4.42" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b8f64fe --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM rust:latest AS builder +WORKDIR /app +COPY . . +RUN cargo build --release + +FROM debian:bookworm-slim +WORKDIR /app +COPY --from=builder /app/target/release/hotel-api-rs /usr/local/bin/hotel-api-rs + +# Create the directory where DB will be stored +RUN mkdir -p /db + +# Expose API port +EXPOSE 8080 + +CMD ["/usr/local/bin/hotel-api-rs"] \ No newline at end of file diff --git a/db/1.sqlite b/db/1.sqlite index 00c4e40..d0ca1d3 100644 Binary files a/db/1.sqlite and b/db/1.sqlite differ diff --git a/db/auth.sqlite b/db/auth.sqlite index 5999a37..5c01723 100644 Binary files a/db/auth.sqlite and b/db/auth.sqlite differ diff --git a/db/auth.sqlite-shm b/db/auth.sqlite-shm index 51155c7..fe9ac28 100644 Binary files a/db/auth.sqlite-shm and b/db/auth.sqlite-shm differ diff --git a/db/auth.sqlite-wal b/db/auth.sqlite-wal index 7f2cd26..e69de29 100644 Binary files a/db/auth.sqlite-wal and b/db/auth.sqlite-wal differ diff --git a/src/chat/handlers.rs b/src/chat/handlers.rs index 4d6924e..54703bd 100644 --- a/src/chat/handlers.rs +++ b/src/chat/handlers.rs @@ -278,4 +278,5 @@ pub async fn get_hotel_users( Ok(json) => (StatusCode::OK, json), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Serialization failed: {}", e)), } -} \ No newline at end of file +} + diff --git a/src/main.rs b/src/main.rs index 24e29d9..498b769 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,14 +19,15 @@ use crate::utils::db_pool::{HotelPool,AppState}; use routes::create_router; use crate::utils::auth::JwtKeys; - +use std::env; +use dotenvy::dotenv; #[tokio::main(flavor = "multi_thread", worker_threads = 8)] async fn main() -> std::io::Result<()> { - + dotenv().ok(); let hotel_pools = HotelPool::new(); let logs_manager = SqliteConnectionManager::file("db/auth.sqlite"); @@ -42,7 +43,12 @@ async fn main() -> std::io::Result<()> { //jwt_secret: "your_jwt_secret_key s".to_string(), // better: load from env var }; - let jwt_secret = "your_jwt_secret_key".to_string(); + //let jwt_secret = "your_jwt_secret_key".to_string(); + + let jwt_secret = env::var("JWT_SECRET") + .expect("JWT_SECRET must be set") + .to_string(); + let jwt_keys = JwtKeys { encoding: EncodingKey::from_secret(jwt_secret.as_ref()), decoding: DecodingKey::from_secret(jwt_secret.as_ref()), @@ -53,7 +59,7 @@ async fn main() -> std::io::Result<()> { let app = create_router(state) .layer(Extension(jwt_keys)); - let listener = TcpListener::bind("0.0.0.0:3000").await?; + let listener = TcpListener::bind("0.0.0.0:8080").await?; serve(listener, app).into_future().await?; Ok(()) } diff --git a/src/rooms/model.rs b/src/rooms/model.rs deleted file mode 100644 index de69eb0..0000000 --- a/src/rooms/model.rs +++ /dev/null @@ -1,10 +0,0 @@ -use serde::{Deserialize, Serialize}; -use uuid::Uuid; -use chrono::{DateTime, Utc}; - -#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)] -pub struct Room{ - pub id: i32, - pub room_number: i32, - pub status: String -} diff --git a/src/utils/auth.rs b/src/utils/auth.rs index d8dd782..7e9dea3 100644 --- a/src/utils/auth.rs +++ b/src/utils/auth.rs @@ -235,7 +235,7 @@ pub struct UpdatePasswordValues{ username: String, current_password: String, newpassword: String, - hotel_id: i32, + //hotel_id: i32, } @@ -296,7 +296,7 @@ pub async fn UpdatePassword( pub struct LoginValues { username : String, password : String, - hotel_id: i32, + //hotel_id: i32, } pub struct LoginPayload(pub LoginValues); @@ -320,7 +320,7 @@ struct Claims{ id: i32, hotel_id: i32, //display_name - username: String, + //username: String, exp: usize, } @@ -376,7 +376,7 @@ pub async fn clean_auth_loging( let claims = serde_json::json!({ "id": user_id, "hotel_id": hotel_id, - "username": payload.username, + //"username": payload.username, "exp": expiration }); @@ -400,6 +400,7 @@ pub struct CreateRefreshTokenValue { } +//TODO: refactor this to impl IntoResponse ans not Result #[axum::debug_handler] pub async fn create_refresh_token( State(state): State, @@ -417,7 +418,6 @@ pub async fn create_refresh_token( let salt = SaltString::generate(&mut OsRng); let mut bytes = [0u8; 64]; OsRng.fill_bytes(&mut bytes); - let raw_token = Uuid::new_v4().to_string(); let hashed_token = argon2 @@ -504,19 +504,16 @@ pub async fn login_refresh_token ( Ok(opt) => opt, Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "DB query error").into_response(), }; - let (user_id, token_hash, hotel_id) = match device_row { Some(tuple) => tuple, None => return (StatusCode::UNAUTHORIZED, "No matching device").into_response(), }; - if !verify_password(&payload.refresh_token, &token_hash) { return (StatusCode::UNAUTHORIZED, "Invalid or mismatched token").into_response(); } - let expiration = match chrono::Utc::now().checked_add_signed(chrono::Duration::hours(15)) { Some(time) => time.timestamp() as usize, None => { @@ -543,6 +540,98 @@ pub async fn login_refresh_token ( Json(LoginResponse { token }).into_response() } +pub async fn logout_from_single_device ( + State(state): State, + Extension(keys): Extension, + user_agent: Option>, + Json(payload): Json +) -> impl IntoResponse { + + let user_agent_str = user_agent + .map(|ua| ua.to_string()) + .unwrap_or_else(|| "Unknown".to_string()); + + let device_id_str = payload.device_id.to_string(); + + let conn = match state.logs_pool.get() { + Ok(c) => c, + Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "DB connection error").into_response(), + }; + + let device_row = match conn.query_row( + "SELECT user_id, token_hash, hotel_id, id FROM refresh_token WHERE device_id = ?1 AND user_agent = ?2 AND revoke = 0 ", + params![&device_id_str, &user_agent_str], + |row| { + let user_id: i32 = row.get(0)?; + let token_hash: String = row.get(1)?; + let hotel_id: i32 = row.get(2)?; + let id:i32 = row.get(3)?; + //let displayname: String = row.get(3)?; + Ok((user_id, token_hash, hotel_id,id)) + }, + ).optional() { + Ok(opt) => opt, + Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "DB query error").into_response(), + }; + + let (user_id, token_hash, hotel_id, token_id) = match device_row { + Some(tuple) => tuple, + None => return (StatusCode::UNAUTHORIZED, "No matching device").into_response(), + }; + + if !verify_password(&payload.refresh_token, &token_hash) { + return (StatusCode::UNAUTHORIZED, "Invalid or mismatched token").into_response(); + } + + let revoked: Result = conn.query_row( + "UPDATE refresh_token SET revoked = 1 WHERE id = ?1 RETURNING device_id", + params![&token_id], + |row| row.get(0), + ); + + return (StatusCode::OK, format!("Token deleted for device id {}", &device_id_str)).into_response() + +} + +pub async fn logout_from_all_devices ( + State(state): State, + Extension(keys): Extension, + AuthClaims { user_id, hotel_id }: AuthClaims, + Json(payload): Json +) -> impl IntoResponse { + + + + let device_id_str = payload.device_id.to_string(); + + let conn = match state.logs_pool.get() { + Ok(c) => c, + Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "DB connection error").into_response(), + }; + + let result = conn.execute( + "UPDATE refresh_token SET revoked = 1 WHERE user_id = ?1 AND revoked = 0", + params![&user_id], + ); + + match result { + Ok(count) if count > 0 => { + (StatusCode::OK, format!("Revoked {} active tokens", count)).into_response() + } + Ok(_) => (StatusCode::NOT_FOUND, "No active tokens to revoke").into_response(), + Err(_) => ( + StatusCode::INTERNAL_SERVER_ERROR, + "Database update error".to_string(), + ) + .into_response(), + } + + + +} + + + fn internal_error(err: E) -> (StatusCode, String) { (StatusCode::INTERNAL_SERVER_ERROR, format!("Internal error: {}", err)) } \ No newline at end of file diff --git a/src/utils/routes.rs b/src/utils/routes.rs index 1f744aa..99ad04a 100644 --- a/src/utils/routes.rs +++ b/src/utils/routes.rs @@ -23,5 +23,11 @@ pub fn utils_routes() -> Router { .route("/create_refresh", post(create_refresh_token)) .route("/login_refresh_token", post(login_refresh_token)) + + .route("/logout_single_device", post(logout_from_single_device)) + .route("/logout_all_devices", post(logout_from_all_devices)) + + + //.with_state(state) } \ No newline at end of file diff --git a/test.rs b/test.rs deleted file mode 100644 index 49d378f..0000000 --- a/test.rs +++ /dev/null @@ -1,9 +0,0 @@ -fn get_pool(hotel_id: String) -> connection { - - if connection { - return connection; - } - - create connection - -} \ No newline at end of file diff --git a/utils command.txt b/utils command.txt index 223d11e..f256f6f 100644 --- a/utils command.txt +++ b/utils command.txt @@ -1 +1,6 @@ -cross build --release --target aarch64-unknown-linux-gnu \ No newline at end of file +cross build --release --target aarch64-unknown-linux-gnu + +docker run -p 8080:8080 \ + -v ${PWD}/db:/db \ + -e JWT_SECRET="my-dev-secret" \ + rust-api:1.0.0 \ No newline at end of file