From 03995bc607db6302b0615841faf5b48bd838da95 Mon Sep 17 00:00:00 2001 From: ofcljaved Date: Sat, 25 Jan 2025 16:26:25 +0530 Subject: [PATCH 01/11] feat: add admin routes --- latent-backend/api/.env.example | 3 +- latent-backend/api/src/error.rs | 4 + latent-backend/api/src/main.rs | 35 ++++++--- latent-backend/api/src/routes/admin.rs | 101 +++++++++++++++++++++++++ latent-backend/api/src/routes/mod.rs | 6 +- latent-backend/api/src/routes/test.rs | 79 +++++++++++++++++++ latent-backend/api/src/routes/user.rs | 8 +- latent-backend/api/test.sh | 13 ++-- latent-backend/db/src/admin.rs | 59 +++++++++++++++ latent-backend/db/src/lib.rs | 5 ++ latent-backend/db/src/test.rs | 52 +++++++++++++ 11 files changed, 346 insertions(+), 19 deletions(-) create mode 100644 latent-backend/api/src/routes/admin.rs create mode 100644 latent-backend/api/src/routes/test.rs create mode 100644 latent-backend/db/src/admin.rs create mode 100644 latent-backend/db/src/test.rs diff --git a/latent-backend/api/.env.example b/latent-backend/api/.env.example index c7c665a..08199ef 100644 --- a/latent-backend/api/.env.example +++ b/latent-backend/api/.env.example @@ -1,4 +1,5 @@ DB_URL=postgres://postgres:mysecretpassword@localhost:5432 TWILIO_AUTH_TOKEN="<>" TWILIO_ACCOUNT_SID="<>" -TWILIO_PHONE_NUMBER="<>" \ No newline at end of file +TWILIO_PHONE_NUMBER="<>" +PORT=8080 diff --git a/latent-backend/api/src/error.rs b/latent-backend/api/src/error.rs index f2b9cc9..18ea61d 100644 --- a/latent-backend/api/src/error.rs +++ b/latent-backend/api/src/error.rs @@ -30,6 +30,10 @@ pub enum AppError { /// Bad request (400) #[oai(status = 400)] BadRequest(Json), + + /// Admin Not Found (411) + #[oai(status = 411)] + AdminNotFound(Json), } impl From for AppError { diff --git a/latent-backend/api/src/main.rs b/latent-backend/api/src/main.rs index a9418f8..94359d8 100644 --- a/latent-backend/api/src/main.rs +++ b/latent-backend/api/src/main.rs @@ -12,14 +12,13 @@ mod utils; use db::Db; use dotenv::dotenv; +use std::env; #[derive(Clone)] pub struct AppState { db: Arc, } -pub struct Api; - #[tokio::main] async fn main() -> Result<(), std::io::Error> { // Load environment variables @@ -28,30 +27,48 @@ async fn main() -> Result<(), std::io::Error> { // Initialize logger env_logger::init_from_env(env_logger::Env::default().default_filter_or("info")); + // Read port from environment variable + let port = env::var("PORT").unwrap_or_else(|_| "8080".to_string()); + let server_url = format!("http://localhost:{}/api/v1", port); + // Create and initialize database let db = Db::new().await; db.init().await.expect("Failed to initialize database"); let db = Arc::new(db); // Create API service - let api_service = OpenApiService::new(Api, "Latent Booking", "1.0") - .server("http://localhost:3000/api/v1"); + let api_service = OpenApiService::new(routes::user::UserApi, "Latent Booking", "1.0") + .server(&server_url); + let admin_api_service = OpenApiService::new(routes::admin::AdminApi, "Admin Latent Booking", "1.0") + .server(format!("{}/admin", server_url)); + // Create Swagger UI let ui = api_service.swagger_ui(); // Create route with CORS - let app = Route::new() + let mut app = Route::new() .nest("/api/v1", api_service) - .nest("/docs", ui) + .nest("/api/v1/admin", admin_api_service) + .nest("/docs", ui); + + if cfg!(debug_assertions) { + let test_api_service = OpenApiService::new(routes::test::TestApi, "Test Latent Booking", "1.0") + .server(format!("{}/test", server_url)); + + app = app.nest("/api/v1/test", test_api_service); + println!("Test routes enabled (development mode)"); + } + + let app = app .with(Cors::new()) .data(AppState { db }); - println!("Server running at http://localhost:3000"); - println!("API docs at http://localhost:3000/docs"); + println!("Server running at {}", server_url); + println!("API docs at {}/docs", server_url); // Start server - Server::new(TcpListener::bind("0.0.0.0:3000")) + Server::new(TcpListener::bind(format!("0.0.0.0:{}", port))) .run(app) .await } diff --git a/latent-backend/api/src/routes/admin.rs b/latent-backend/api/src/routes/admin.rs new file mode 100644 index 0000000..163829c --- /dev/null +++ b/latent-backend/api/src/routes/admin.rs @@ -0,0 +1,101 @@ +use crate::{ + error::AppError, + utils::{totp, twilio}, + AppState, +}; +use poem::web::{Data, Json}; +use poem_openapi::{payload, Object, OpenApi}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize, Object)] +struct VerifyAdminResponse { + token: String, +} + +#[derive(Debug, Serialize, Deserialize, Object)] +struct SignInRequest { + number: String, +} + +#[derive(Debug, Serialize, Deserialize, Object)] +struct SignInResponse { + message: String, +} + +#[derive(Debug, Serialize, Deserialize, Object)] +struct SignInVerify { + number: String, + totp: String, +} + +pub struct AdminApi; + +#[OpenApi] +impl AdminApi { + /// Sign in existing admin + #[oai(path = "/signin", method = "post")] + async fn sign_in( + &self, + body: Json, + state: Data<&AppState>, + ) -> poem::Result, AppError> { + let number = body.0.number; + + println!("Signing in admin with number: {}", number); + + let admin_result = state.db.get_admin_by_number(&number).await; + + if let Err(sqlx::Error::RowNotFound) = admin_result { + return Err(AppError::AdminNotFound(payload::Json( + crate::error::ErrorBody { + message: "Admin not found".to_string(), + }, + ))); + } + + let _admin = admin_result?; + // Generate and send OTP + let otp = totp::get_token(&number, "ADMIN_AUTH"); + if cfg!(not(debug_assertions)) { + twilio::send_message( + &format!("Your admin OTP for signing in to Latent is {}", otp), + &number, + ) + .await + .map_err(|_| { + AppError::InternalServerError(payload::Json(crate::error::ErrorBody { + message: "Failed to send OTP".to_string(), + })) + })?; + } else { + println!("Development mode: OTP is {}", otp); + } + + Ok(payload::Json(SignInResponse { + message: "OTP sent successfully".to_string(), + })) + } + + /// Verify sign in with OTP + #[oai(path = "/signin/verify", method = "post")] + async fn sign_in_verify( + &self, + body: Json, + state: Data<&AppState>, + ) -> poem::Result, AppError> { + let SignInVerify { number, totp: otp } = body.0; + + // Verify OTP + if cfg!(not(debug_assertions)) && !totp::verify_token(&number, "ADMIN_AUTH", &otp) { + return Err(AppError::InvalidCredentials(payload::Json( + crate::error::ErrorBody { + message: "Invalid OTP".to_string(), + }, + ))); + } + + let token = state.db.verify_admin_signin(number).await?; + + Ok(payload::Json(VerifyAdminResponse { token })) + } +} diff --git a/latent-backend/api/src/routes/mod.rs b/latent-backend/api/src/routes/mod.rs index 018ff2e..f28d0b2 100644 --- a/latent-backend/api/src/routes/mod.rs +++ b/latent-backend/api/src/routes/mod.rs @@ -1 +1,5 @@ -pub mod user; \ No newline at end of file +pub mod user; +pub mod admin; + +#[cfg(debug_assertions)] +pub mod test; diff --git a/latent-backend/api/src/routes/test.rs b/latent-backend/api/src/routes/test.rs new file mode 100644 index 0000000..8e099ea --- /dev/null +++ b/latent-backend/api/src/routes/test.rs @@ -0,0 +1,79 @@ +use crate::{error::AppError, AppState}; +use db::AdminType; +use poem::web::{Data, Json}; +use poem_openapi::{payload, Object, OpenApi}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Object)] +struct CreateTestUser { + number: String, + name: String, +} + +#[derive(Debug, Deserialize, Serialize, Object)] +struct CreateTestUserResponse { + message: String, + id: String, +} + +pub struct TestApi; + +#[OpenApi] +impl TestApi { + /// Create a test admin + #[oai(path = "/create-admin", method = "post")] + async fn create_admin( + &self, + body: Json, + state: Data<&AppState>, + ) -> poem::Result, AppError> { + let number = body.0.number; + let name = body.0.name; + let user = state + .db + .create_test_admin(number.clone(), name, AdminType::Creator) + .await?; + + Ok(payload::Json(CreateTestUserResponse { + message: "Test Admin created successfully".to_string(), + id: user.id.to_string(), + })) + } + + /// Create a test super-admin + #[oai(path = "/create-super-admin", method = "post")] + async fn create_super_admin( + &self, + body: Json, + state: Data<&AppState>, + ) -> poem::Result, AppError> { + let number = body.0.number; + let name = body.0.name; + let user = state + .db + .create_test_admin(number.clone(), name, AdminType::SuperAdmin) + .await?; + + Ok(payload::Json(CreateTestUserResponse { + message: "Test Super Admin created successfully".to_string(), + id: user.id.to_string(), + })) + } + + /// Create a test user + #[oai(path = "/create-user", method = "post")] + async fn create_test_user( + &self, + body: Json, + state: Data<&AppState>, + ) -> poem::Result, AppError> { + let number = body.0.number; + let name = body.0.name; + let user = state.db.create_test_user(number.clone(), name).await?; + + Ok(payload::Json(CreateTestUserResponse { + message: "Test User created successfully".to_string(), + id: user.id.to_string(), + })) + } +} diff --git a/latent-backend/api/src/routes/user.rs b/latent-backend/api/src/routes/user.rs index b14f4d4..bfdf026 100644 --- a/latent-backend/api/src/routes/user.rs +++ b/latent-backend/api/src/routes/user.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use poem::web::{Data, Json}; use poem_openapi::{OpenApi, Object, payload}; -use crate::{error::AppError, Api, AppState, utils::{totp, twilio}}; +use crate::{error::AppError, AppState, utils::{totp, twilio}}; #[derive(Debug, Serialize, Deserialize, Object)] struct CreateUser { @@ -42,8 +42,10 @@ struct SignInVerify { totp: String, } +pub struct UserApi; + #[OpenApi] -impl Api { +impl UserApi { /// Create a new user #[oai(path = "/signup", method = "post")] async fn create_user(&self, body: Json, state: Data<&AppState>) -> poem::Result, AppError> { @@ -141,4 +143,4 @@ impl Api { Ok(payload::Json(VerifyUserResponse { token })) } -} \ No newline at end of file +} diff --git a/latent-backend/api/test.sh b/latent-backend/api/test.sh index 5ac1010..3ee476c 100755 --- a/latent-backend/api/test.sh +++ b/latent-backend/api/test.sh @@ -1,7 +1,10 @@ #!/bin/bash +PORT=${PORT:-8080} +BASE_URL="http://localhost:${PORT}" + echo "1. Creating new user..." -curl -X POST http://localhost:3000/api/v1/signup \ +curl -X POST "$BASE_URL/api/v1/signup" \ -H "Content-Type: application/json" \ -d '{"number": "9729302411"}' | jq @@ -9,7 +12,7 @@ echo -e "\nPress Enter to continue with signup verification..." read echo "2. Verifying signup..." -curl -X POST http://localhost:3000/api/v1/signup/verify \ +curl -X POST "$BASE_URL/api/v1/signup/verify" \ -H "Content-Type: application/json" \ -d '{ "number": "9729302411", @@ -21,7 +24,7 @@ echo -e "\nPress Enter to continue with signin..." read echo "3. Signing in..." -curl -X POST http://localhost:3000/api/v1/signin \ +curl -X POST "$BASE_URL/api/v1/signin" \ -H "Content-Type: application/json" \ -d '{"number": "9729302411"}' | jq @@ -29,9 +32,9 @@ echo -e "\nPress Enter to continue with signin verification..." read echo "4. Verifying signin..." -curl -X POST http://localhost:3000/api/v1/signin/verify \ +curl -X POST "$BASE_URL/api/v1/signin/verify" \ -H "Content-Type: application/json" \ -d '{ "number": "9729302411", "totp": "123456" - }' | jq \ No newline at end of file + }' | jq diff --git a/latent-backend/db/src/admin.rs b/latent-backend/db/src/admin.rs new file mode 100644 index 0000000..405fee3 --- /dev/null +++ b/latent-backend/db/src/admin.rs @@ -0,0 +1,59 @@ +use crate::Db; +use log::info; +use serde::{Deserialize, Serialize}; +use sqlx::Error; +use sqlx::FromRow; +use uuid::Uuid; + +#[derive(sqlx::Type, Debug, Serialize, Deserialize)] +#[sqlx(type_name = "admin_type")] +pub enum AdminType { + SuperAdmin, + Creator, +} + +#[derive(FromRow, Serialize, Deserialize)] +pub struct Admin { + pub id: Uuid, + pub number: String, + pub name: String, + pub verified: bool, + #[sqlx(rename = "type")] + pub admin_type: AdminType, +} + +impl Db { + pub async fn get_admin_by_number(&self, phone_number: &str) -> Result { + info!("Fetching admin with number: {}", phone_number); + + let result = sqlx::query_as::<_, Admin>("SELECT * FROM admins WHERE number = $1") + .bind(phone_number) + .fetch_one(&self.client) + .await; + + match result { + Ok(admin) => { + info!("Admin found with id: {}", admin.id); + Ok(admin) + } + Err(e) => { + eprintln!("Database error occurred: {}", e); + Err(e) + } + } + //info!("Admin found with id: {}", admin.id); + //Ok(admin) + } + + pub async fn verify_admin_signin(&self, phone_number: String) -> Result { + info!("Verifying signin for admin with number: {}", phone_number); + + let admin = sqlx::query_as::<_, Admin>("SELECT * FROM admins WHERE number = $1") + .bind(phone_number) + .fetch_one(&self.client) + .await?; + + info!("Signin verified for admin with id: {}", admin.id); + Ok(admin.id.to_string()) + } +} diff --git a/latent-backend/db/src/lib.rs b/latent-backend/db/src/lib.rs index 870aee5..997a02a 100644 --- a/latent-backend/db/src/lib.rs +++ b/latent-backend/db/src/lib.rs @@ -3,8 +3,13 @@ use log::{info, error}; mod config; mod user; +mod admin; + +#[cfg(debug_assertions)] +pub mod test; pub use user::User; +pub use admin::AdminType; pub struct Db { client: PgPool, diff --git a/latent-backend/db/src/test.rs b/latent-backend/db/src/test.rs new file mode 100644 index 0000000..025ac3f --- /dev/null +++ b/latent-backend/db/src/test.rs @@ -0,0 +1,52 @@ +use crate::{admin::{Admin, AdminType}, Db, User}; +use sqlx::Error; +use uuid::Uuid; +use log::info; + +impl Db { + pub async fn create_test_admin(&self, phone_number: String, name: String, admin_type: AdminType) -> Result { + info!("Creating test admin with number: {} and name: {}", phone_number, name); + + let admin = sqlx::query_as::<_, Admin>( + r#" + INSERT INTO admins (id, number, name, verified, type) + VALUES ($1, $2, $3, false, $4) + ON CONFLICT (number) DO UPDATE + SET number = EXCLUDED.number + RETURNING * + "# + ) + .bind(Uuid::new_v4()) + .bind(phone_number) + .bind(name) + .bind(admin_type) + .fetch_one(&self.client) + .await?; + + info!("Test admin created/updated successfully with id: {}", admin.id); + Ok(admin) + } + + pub async fn create_test_user(&self, phone_number: String, name: String) -> Result { + info!("Creating test user with number: {} and name: {}", phone_number, name); + + let user = sqlx::query_as::<_, User>( + r#" + INSERT INTO users (id, number, name, verified) + VALUES ($1, $2, $3, false) + ON CONFLICT (number) DO UPDATE + SET number = EXCLUDED.number + RETURNING * + "# + ) + .bind(Uuid::new_v4()) + .bind(phone_number) + .bind(name) + .fetch_one(&self.client) + .await?; + + info!("Test User created/updated successfully with id: {}", user.id); + Ok(user) + } +} + From b28375862f4b2d942fc255d891308d775b2e87ef Mon Sep 17 00:00:00 2001 From: ofcljaved Date: Sat, 25 Jan 2025 18:26:37 +0530 Subject: [PATCH 02/11] chore: add location endpoint --- latent-backend/api/src/main.rs | 36 ++++++++++------ latent-backend/api/src/middleware/admin.rs | 50 ++++++++++++++++++++++ latent-backend/api/src/middleware/mod.rs | 1 + latent-backend/api/src/routes/admin.rs | 16 +++++++ 4 files changed, 89 insertions(+), 14 deletions(-) create mode 100644 latent-backend/api/src/middleware/admin.rs create mode 100644 latent-backend/api/src/middleware/mod.rs diff --git a/latent-backend/api/src/main.rs b/latent-backend/api/src/main.rs index 94359d8..41ac38b 100644 --- a/latent-backend/api/src/main.rs +++ b/latent-backend/api/src/main.rs @@ -1,12 +1,9 @@ -use poem::{ - listener::TcpListener, - middleware::Cors, - EndpointExt, Route, Server, -}; +use poem::{get, listener::TcpListener, middleware::Cors, EndpointExt, Route, Server}; use poem_openapi::OpenApiService; use std::sync::Arc; mod error; +mod middleware; mod routes; mod utils; @@ -23,7 +20,7 @@ pub struct AppState { async fn main() -> Result<(), std::io::Error> { // Load environment variables dotenv().ok(); - + // Initialize logger env_logger::init_from_env(env_logger::Env::default().default_filter_or("info")); @@ -37,11 +34,12 @@ async fn main() -> Result<(), std::io::Error> { let db = Arc::new(db); // Create API service - let api_service = OpenApiService::new(routes::user::UserApi, "Latent Booking", "1.0") - .server(&server_url); - - let admin_api_service = OpenApiService::new(routes::admin::AdminApi, "Admin Latent Booking", "1.0") - .server(format!("{}/admin", server_url)); + let api_service = + OpenApiService::new(routes::user::UserApi, "Latent Booking", "1.0").server(&server_url); + + let admin_api_service = + OpenApiService::new(routes::admin::AdminApi, "Admin Latent Booking", "1.0") + .server(format!("{}/admin", server_url)); // Create Swagger UI let ui = api_service.swagger_ui(); @@ -53,20 +51,30 @@ async fn main() -> Result<(), std::io::Error> { .nest("/docs", ui); if cfg!(debug_assertions) { - let test_api_service = OpenApiService::new(routes::test::TestApi, "Test Latent Booking", "1.0") - .server(format!("{}/test", server_url)); + let test_api_service = + OpenApiService::new(routes::test::TestApi, "Test Latent Booking", "1.0") + .server(format!("{}/test", server_url)); app = app.nest("/api/v1/test", test_api_service); println!("Test routes enabled (development mode)"); } let app = app + .at("/api/v1/admin/location", |route| { + route + .get() + .with(middleware::admin::AdminMiddleware) // AdminMiddleware for GET + .to(routes::admin::AdminApi::get_location) + .post() + .with(middleware::admin::SuperAdminMiddleware) // SuperAdminMiddleware for POST + .to(routes::admin::AdminApi::create_location) + }) .with(Cors::new()) .data(AppState { db }); println!("Server running at {}", server_url); println!("API docs at {}/docs", server_url); - + // Start server Server::new(TcpListener::bind(format!("0.0.0.0:{}", port))) .run(app) diff --git a/latent-backend/api/src/middleware/admin.rs b/latent-backend/api/src/middleware/admin.rs new file mode 100644 index 0000000..b8212d1 --- /dev/null +++ b/latent-backend/api/src/middleware/admin.rs @@ -0,0 +1,50 @@ +use poem::{ + handler, + middleware::{Middleware, Next}, + web::Data, + Endpoint, Request, Result, +}; + +pub struct AdminMiddleware; + +impl Middleware for AdminMiddleware { + type Output = AdminMiddlewareImpl; + + fn transform(&self, ep: E) -> Self::Output { + AdminMiddlewareImpl { ep } + } +} + +pub struct AdminMiddlewareImpl { + ep: E, +} + +#[handler] +impl AdminMiddlewareImpl { + async fn call(&self, req: Request, next: Next) -> Result { + println!("AdminMiddleware hit!"); + next.run(req).await + } +} + +pub struct SuperAdminMiddleware; + +impl Middleware for SuperAdminMiddleware { + type Output = SuperAdminMiddlewareImpl; + + fn transform(&self, ep: E) -> Self::Output { + SuperAdminMiddlewareImpl { ep } + } +} + +pub struct SuperAdminMiddlewareImpl { + ep: E, +} + +#[handler] +impl SuperAdminMiddlewareImpl { + async fn call(&self, req: Request, next: Next) -> Result { + println!("SuperAdminMiddleware hit!"); + next.run(req).await + } +} diff --git a/latent-backend/api/src/middleware/mod.rs b/latent-backend/api/src/middleware/mod.rs new file mode 100644 index 0000000..92918b0 --- /dev/null +++ b/latent-backend/api/src/middleware/mod.rs @@ -0,0 +1 @@ +pub mod admin; diff --git a/latent-backend/api/src/routes/admin.rs b/latent-backend/api/src/routes/admin.rs index 163829c..cffe193 100644 --- a/latent-backend/api/src/routes/admin.rs +++ b/latent-backend/api/src/routes/admin.rs @@ -98,4 +98,20 @@ impl AdminApi { Ok(payload::Json(VerifyAdminResponse { token })) } + + #[oai(path = "/location", method = "get")] + pub async fn get_location(&self) -> poem::Result> { + println!("GET /api/v1/admin/location handler hit!"); + Ok(payload::Json( + serde_json::json!({ "message": "GET /admin/location" }), + )) + } + + #[oai(path = "/location", method = "post")] + pub async fn create_location(&self) -> poem::Result> { + println!("POST /api/v1/admin/location handler hit!"); + Ok(payload::Json( + serde_json::json!({ "message": "POST /admin/location" }), + )) + } } From a4d8cc2a62cec2352c256c17725d9f04b702242f Mon Sep 17 00:00:00 2001 From: ofcljaved Date: Sun, 26 Jan 2025 01:19:41 +0530 Subject: [PATCH 03/11] feat: add location endpoint with middleware boilerplate --- latent-backend/api/src/main.rs | 18 ++---- latent-backend/api/src/middleware/admin.rs | 36 ++++++----- latent-backend/api/src/routes/admin.rs | 72 ++++++++++++++++++---- latent-backend/db/src/lib.rs | 2 + latent-backend/db/src/location.rs | 49 +++++++++++++++ 5 files changed, 137 insertions(+), 40 deletions(-) create mode 100644 latent-backend/db/src/location.rs diff --git a/latent-backend/api/src/main.rs b/latent-backend/api/src/main.rs index 41ac38b..05bee6d 100644 --- a/latent-backend/api/src/main.rs +++ b/latent-backend/api/src/main.rs @@ -1,4 +1,7 @@ -use poem::{get, listener::TcpListener, middleware::Cors, EndpointExt, Route, Server}; +use poem::{ + listener::TcpListener, middleware::Cors, EndpointExt, Result, + Route, Server, +}; use poem_openapi::OpenApiService; use std::sync::Arc; @@ -59,18 +62,7 @@ async fn main() -> Result<(), std::io::Error> { println!("Test routes enabled (development mode)"); } - let app = app - .at("/api/v1/admin/location", |route| { - route - .get() - .with(middleware::admin::AdminMiddleware) // AdminMiddleware for GET - .to(routes::admin::AdminApi::get_location) - .post() - .with(middleware::admin::SuperAdminMiddleware) // SuperAdminMiddleware for POST - .to(routes::admin::AdminApi::create_location) - }) - .with(Cors::new()) - .data(AppState { db }); + let app = app.with(Cors::new()).data(AppState { db }); println!("Server running at {}", server_url); println!("API docs at {}/docs", server_url); diff --git a/latent-backend/api/src/middleware/admin.rs b/latent-backend/api/src/middleware/admin.rs index b8212d1..56af316 100644 --- a/latent-backend/api/src/middleware/admin.rs +++ b/latent-backend/api/src/middleware/admin.rs @@ -1,9 +1,4 @@ -use poem::{ - handler, - middleware::{Middleware, Next}, - web::Data, - Endpoint, Request, Result, -}; +use poem::{Endpoint, EndpointExt, Middleware, Request, Result}; pub struct AdminMiddleware; @@ -19,17 +14,19 @@ pub struct AdminMiddlewareImpl { ep: E, } -#[handler] -impl AdminMiddlewareImpl { - async fn call(&self, req: Request, next: Next) -> Result { +impl poem::Endpoint for AdminMiddlewareImpl { + type Output = E::Output; + + async fn call(&self, req: Request) -> Result { println!("AdminMiddleware hit!"); - next.run(req).await + self.ep.call(req).await } } +// Define a simple middleware for POST pub struct SuperAdminMiddleware; -impl Middleware for SuperAdminMiddleware { +impl Middleware for SuperAdminMiddleware { type Output = SuperAdminMiddlewareImpl; fn transform(&self, ep: E) -> Self::Output { @@ -41,10 +38,19 @@ pub struct SuperAdminMiddlewareImpl { ep: E, } -#[handler] -impl SuperAdminMiddlewareImpl { - async fn call(&self, req: Request, next: Next) -> Result { +impl poem::Endpoint for SuperAdminMiddlewareImpl { + type Output = E::Output; + + async fn call(&self, req: Request) -> Result { println!("SuperAdminMiddleware hit!"); - next.run(req).await + self.ep.call(req).await } } + +pub fn admin_middleware(ep: impl poem::Endpoint) -> impl poem::Endpoint { + ep.with(AdminMiddleware) +} + +pub fn superadmin_middleware(ep: impl poem::Endpoint) -> impl poem::Endpoint { + ep.with(SuperAdminMiddleware) +} diff --git a/latent-backend/api/src/routes/admin.rs b/latent-backend/api/src/routes/admin.rs index cffe193..d5d491d 100644 --- a/latent-backend/api/src/routes/admin.rs +++ b/latent-backend/api/src/routes/admin.rs @@ -1,8 +1,10 @@ use crate::{ error::AppError, utils::{totp, twilio}, + middleware::admin::{admin_middleware, superadmin_middleware}, AppState, }; + use poem::web::{Data, Json}; use poem_openapi::{payload, Object, OpenApi}; use serde::{Deserialize, Serialize}; @@ -28,6 +30,34 @@ struct SignInVerify { totp: String, } + +#[derive(Debug, Serialize, Deserialize, Object)] +struct Location { + id: String, + name: String, + description: String, + image_url: String, +} + +#[derive(Debug, Serialize, Deserialize, Object)] +pub struct LocationResponse { + locations: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Object)] +pub struct CreateLocation { + name: String, + description: String, + image_url: String, +} + +#[derive(Debug, Serialize, Deserialize, Object)] +pub struct CreateLocationResponse { + message: String, + id: String, +} + + pub struct AdminApi; #[OpenApi] @@ -99,19 +129,37 @@ impl AdminApi { Ok(payload::Json(VerifyAdminResponse { token })) } - #[oai(path = "/location", method = "get")] - pub async fn get_location(&self) -> poem::Result> { - println!("GET /api/v1/admin/location handler hit!"); - Ok(payload::Json( - serde_json::json!({ "message": "GET /admin/location" }), - )) + #[oai(path = "/location", method = "get", transform = "admin_middleware")] + pub async fn get_location( + &self, + state: Data<&AppState>, + ) -> poem::Result, AppError> { + + let db_locations = state.db.get_location().await?; + + let locations = db_locations.iter().map(|l| Location { + id: l.id.to_string(), + name: l.name.clone(), + description: l.description.clone(), + image_url: l.image_url.clone(), + }).collect(); + + Ok(payload::Json(LocationResponse { locations })) } - #[oai(path = "/location", method = "post")] - pub async fn create_location(&self) -> poem::Result> { - println!("POST /api/v1/admin/location handler hit!"); - Ok(payload::Json( - serde_json::json!({ "message": "POST /admin/location" }), - )) + #[oai(path = "/location", method = "post", transform = "superadmin_middleware")] + pub async fn create_location( + &self, + body: Json, + state: Data<&AppState>, + ) -> poem::Result, AppError> { + let CreateLocation { name, description, image_url } = body.0; + + let location = state.db.create_location(name, description, image_url).await?; + + Ok(payload::Json(CreateLocationResponse { + message: "Location created successfully".to_string(), + id: location.id.to_string(), + })) } } diff --git a/latent-backend/db/src/lib.rs b/latent-backend/db/src/lib.rs index 997a02a..348ba1b 100644 --- a/latent-backend/db/src/lib.rs +++ b/latent-backend/db/src/lib.rs @@ -4,12 +4,14 @@ use log::{info, error}; mod config; mod user; mod admin; +mod location; #[cfg(debug_assertions)] pub mod test; pub use user::User; pub use admin::AdminType; +pub use location::Location; pub struct Db { client: PgPool, diff --git a/latent-backend/db/src/location.rs b/latent-backend/db/src/location.rs new file mode 100644 index 0000000..9e4d0de --- /dev/null +++ b/latent-backend/db/src/location.rs @@ -0,0 +1,49 @@ +use crate::Db; +use sqlx::Error; +use sqlx::FromRow; +use uuid::Uuid; +use serde::{Deserialize, Serialize}; +use log::info; + +#[derive(FromRow, Serialize, Deserialize)] +pub struct Location { + pub id: Uuid, + pub name: String, + pub description: String, + pub image_url: String, +} + +impl Db { + + pub async fn get_location(&self) -> Result, Error> { + info!("Fetching location"); + + let locations = sqlx::query_as::<_, Location>("SELECT * FROM locations") + .fetch_all(&self.client) + .await?; + + info!("Location found"); + Ok(locations) + } + + pub async fn create_location(&self, name: String, description: String, image_url: String) -> Result { + info!("Creating new location"); + + let location = sqlx::query_as::<_, Location>( + r#" + INSERT INTO locations (id, name, description, image_url) + VALUES ($1, $2, $3, $4) + RETURNING * + "# + ) + .bind(Uuid::new_v4()) + .bind(name) + .bind(description) + .bind(image_url) + .fetch_one(&self.client) + .await?; + + info!("Location created/updated successfully"); + Ok(location) + } +} From b5f21d0787dce9a8cc19037f0b3c4c6cbc16effa Mon Sep 17 00:00:00 2001 From: ofcljaved Date: Sun, 26 Jan 2025 03:19:50 +0530 Subject: [PATCH 04/11] feat: add middleware --- latent-backend/Cargo.lock | 71 +++++++++++++++++ latent-backend/api/Cargo.toml | 3 +- latent-backend/api/src/error.rs | 18 +++++ latent-backend/api/src/middleware/admin.rs | 91 ++++++++++++++-------- latent-backend/api/src/routes/admin.rs | 75 ++++++++++++++---- latent-backend/api/src/utils/config.rs | 13 ++++ latent-backend/api/src/utils/mod.rs | 3 +- 7 files changed, 225 insertions(+), 49 deletions(-) create mode 100644 latent-backend/api/src/utils/config.rs diff --git a/latent-backend/Cargo.lock b/latent-backend/Cargo.lock index 8b8ae08..7b6309b 100644 --- a/latent-backend/Cargo.lock +++ b/latent-backend/Cargo.lock @@ -89,6 +89,7 @@ dependencies = [ "db", "dotenv", "env_logger", + "jsonwebtoken", "log", "poem", "poem-openapi", @@ -704,8 +705,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -1217,6 +1220,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" +dependencies = [ + "base64 0.21.7", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1368,6 +1386,16 @@ dependencies = [ "libc", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -1515,6 +1543,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64 0.22.1", + "serde", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1846,6 +1884,21 @@ dependencies = [ "uncased", ] +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rsa" version = "0.9.7" @@ -2051,6 +2104,18 @@ dependencies = [ "rand_core", ] +[[package]] +name = "simple_asn1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.11", + "time", +] + [[package]] name = "slab" version = "0.4.9" @@ -2772,6 +2837,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.4" diff --git a/latent-backend/api/Cargo.toml b/latent-backend/api/Cargo.toml index 3aa9f20..d3de34e 100644 --- a/latent-backend/api/Cargo.toml +++ b/latent-backend/api/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] db = { path = "../db" } dotenv = "0.15.0" +jsonwebtoken = "9.3.0" poem = "3.1.3" poem-openapi = { version = "5.1.2", features = ["swagger-ui"] } serde = "1.0.217" @@ -16,4 +17,4 @@ sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "tls-native-t reqwest = { version = "0.11.13", features = ["json"] } env_logger = "0.10" log = "0.4" -sha2 = "0.10" \ No newline at end of file +sha2 = "0.10" diff --git a/latent-backend/api/src/error.rs b/latent-backend/api/src/error.rs index 18ea61d..7939d52 100644 --- a/latent-backend/api/src/error.rs +++ b/latent-backend/api/src/error.rs @@ -1,4 +1,6 @@ use poem_openapi::{payload::Json, ApiResponse, Object}; +use poem::error::Error as PoemError; +use jsonwebtoken::errors::Error as JwtError; #[derive(Debug, Object)] pub struct ErrorBody { @@ -48,3 +50,19 @@ impl From for AppError { } } } + +impl From for AppError { + fn from(err: PoemError) -> Self { + AppError::InternalServerError(Json(ErrorBody { + message: err.to_string(), + })) + } +} + +impl From for AppError { + fn from(err: JwtError) -> Self { + AppError::InternalServerError(Json(ErrorBody { + message: format!("JWT error: {}", err), + })) + } +} diff --git a/latent-backend/api/src/middleware/admin.rs b/latent-backend/api/src/middleware/admin.rs index 56af316..2e20898 100644 --- a/latent-backend/api/src/middleware/admin.rs +++ b/latent-backend/api/src/middleware/admin.rs @@ -1,56 +1,85 @@ +use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; use poem::{Endpoint, EndpointExt, Middleware, Request, Result}; +use poem_openapi::payload; +use serde::{Deserialize, Serialize}; -pub struct AdminMiddleware; +use crate::{ + error::AppError, + utils::config::{admin_jwt_password, superadmin_jwt_password}, +}; -impl Middleware for AdminMiddleware { - type Output = AdminMiddlewareImpl; - - fn transform(&self, ep: E) -> Self::Output { - AdminMiddlewareImpl { ep } - } +#[derive(Debug, Serialize, Deserialize)] +struct Claims { + sub: String, // Subject (e.g., user ID) + exp: usize, // Expiration time } -pub struct AdminMiddlewareImpl { - ep: E, +pub struct JwtMiddleware { + secret: String, // Secret key for JWT verification } -impl poem::Endpoint for AdminMiddlewareImpl { - type Output = E::Output; - - async fn call(&self, req: Request) -> Result { - println!("AdminMiddleware hit!"); - self.ep.call(req).await +impl JwtMiddleware { + /// Create a new instance of `JwtMiddleware` with the given secret key + pub fn new(secret: String) -> Self { + Self { secret } } } - -// Define a simple middleware for POST -pub struct SuperAdminMiddleware; - -impl Middleware for SuperAdminMiddleware { - type Output = SuperAdminMiddlewareImpl; +impl Middleware for JwtMiddleware { + type Output = JwtMiddlewareImpl; fn transform(&self, ep: E) -> Self::Output { - SuperAdminMiddlewareImpl { ep } + JwtMiddlewareImpl { + ep, + secret: self.secret.clone(), + } } } -pub struct SuperAdminMiddlewareImpl { +pub struct JwtMiddlewareImpl { ep: E, + secret: String, } -impl poem::Endpoint for SuperAdminMiddlewareImpl { +impl poem::Endpoint for JwtMiddlewareImpl { type Output = E::Output; - async fn call(&self, req: Request) -> Result { - println!("SuperAdminMiddleware hit!"); - self.ep.call(req).await + async fn call(&self, mut req: Request) -> Result { + let token = req + .headers() + .get("Authorization") + .and_then(|value| value.to_str().ok()); + + if let Some(token) = token { + let decoding_key = DecodingKey::from_secret(self.secret.as_bytes()); + let validation = Validation::new(Algorithm::HS256); + + match decode::(token, &decoding_key, &validation) { + Ok(token_data) => { + req.extensions_mut().insert(token_data.claims.sub); + self.ep.call(req).await + } + Err(_) => Err( + AppError::Unauthorized(payload::Json(crate::error::ErrorBody { + message: "Unauthorized".to_string(), + })) + .into(), + ), + } + } else { + Err( + AppError::Unauthorized(payload::Json(crate::error::ErrorBody { + message: "Unauthorized".to_string(), + })) + .into(), + ) + } } } -pub fn admin_middleware(ep: impl poem::Endpoint) -> impl poem::Endpoint { - ep.with(AdminMiddleware) +pub fn admin_middleware(ep: impl Endpoint) -> impl Endpoint { + ep.with(JwtMiddleware::new(admin_jwt_password())) } -pub fn superadmin_middleware(ep: impl poem::Endpoint) -> impl poem::Endpoint { - ep.with(SuperAdminMiddleware) +pub fn superadmin_middleware(ep: impl Endpoint) -> impl Endpoint { + ep.with(JwtMiddleware::new(superadmin_jwt_password())) } diff --git a/latent-backend/api/src/routes/admin.rs b/latent-backend/api/src/routes/admin.rs index d5d491d..6b9613f 100644 --- a/latent-backend/api/src/routes/admin.rs +++ b/latent-backend/api/src/routes/admin.rs @@ -1,13 +1,21 @@ use crate::{ error::AppError, - utils::{totp, twilio}, middleware::admin::{admin_middleware, superadmin_middleware}, + utils::{config, totp, twilio}, AppState, }; +use jsonwebtoken::{encode, EncodingKey, Header}; use poem::web::{Data, Json}; use poem_openapi::{payload, Object, OpenApi}; use serde::{Deserialize, Serialize}; +use std::time::{SystemTime, UNIX_EPOCH}; + +#[derive(Debug, Serialize, Deserialize)] +struct Claims { + sub: String, // Subject (e.g., user ID) + exp: usize, // Expiration time +} #[derive(Debug, Deserialize, Serialize, Object)] struct VerifyAdminResponse { @@ -30,7 +38,6 @@ struct SignInVerify { totp: String, } - #[derive(Debug, Serialize, Deserialize, Object)] struct Location { id: String, @@ -57,7 +64,6 @@ pub struct CreateLocationResponse { id: String, } - pub struct AdminApi; #[OpenApi] @@ -124,9 +130,33 @@ impl AdminApi { ))); } - let token = state.db.verify_admin_signin(number).await?; + let user_id = state.db.verify_admin_signin(number).await?; - Ok(payload::Json(VerifyAdminResponse { token })) + let current_time = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|_| { + poem::Error::from_string( + "Failed to get current time".to_string(), + poem::http::StatusCode::INTERNAL_SERVER_ERROR, + ) + })? + .as_secs() as usize; + + // Set expiration time to 1 hour from now + let exp = current_time + 3600; + + let claims = Claims { + sub: user_id.clone(), + exp, + }; + + let jwt_token = encode( + &Header::default(), + &claims, + &EncodingKey::from_secret(config::admin_jwt_password().as_bytes()), + )?; + + Ok(payload::Json(VerifyAdminResponse { token: jwt_token })) } #[oai(path = "/location", method = "get", transform = "admin_middleware")] @@ -134,28 +164,41 @@ impl AdminApi { &self, state: Data<&AppState>, ) -> poem::Result, AppError> { - let db_locations = state.db.get_location().await?; - let locations = db_locations.iter().map(|l| Location { - id: l.id.to_string(), - name: l.name.clone(), - description: l.description.clone(), - image_url: l.image_url.clone(), - }).collect(); + let locations = db_locations + .iter() + .map(|l| Location { + id: l.id.to_string(), + name: l.name.clone(), + description: l.description.clone(), + image_url: l.image_url.clone(), + }) + .collect(); Ok(payload::Json(LocationResponse { locations })) } - #[oai(path = "/location", method = "post", transform = "superadmin_middleware")] + #[oai( + path = "/location", + method = "post", + transform = "superadmin_middleware" + )] pub async fn create_location( &self, body: Json, state: Data<&AppState>, ) -> poem::Result, AppError> { - let CreateLocation { name, description, image_url } = body.0; - - let location = state.db.create_location(name, description, image_url).await?; + let CreateLocation { + name, + description, + image_url, + } = body.0; + + let location = state + .db + .create_location(name, description, image_url) + .await?; Ok(payload::Json(CreateLocationResponse { message: "Location created successfully".to_string(), diff --git a/latent-backend/api/src/utils/config.rs b/latent-backend/api/src/utils/config.rs new file mode 100644 index 0000000..dce6d0a --- /dev/null +++ b/latent-backend/api/src/utils/config.rs @@ -0,0 +1,13 @@ +use std::env; + +pub fn jwt_password() -> String { + env::var("JWT_PASSWORD").unwrap_or_else(|_| "123random".to_string()) +} + +pub fn admin_jwt_password() -> String { + env::var("ADMIN_JWT_PASSWORD").unwrap_or_else(|_| "123random".to_string()) +} + +pub fn superadmin_jwt_password() -> String { + env::var("SUPERADMIN_JWT_PASSWORD").unwrap_or_else(|_| "123random".to_string()) +} diff --git a/latent-backend/api/src/utils/mod.rs b/latent-backend/api/src/utils/mod.rs index ab598b5..65050dd 100644 --- a/latent-backend/api/src/utils/mod.rs +++ b/latent-backend/api/src/utils/mod.rs @@ -1,2 +1,3 @@ pub mod totp; -pub mod twilio; \ No newline at end of file +pub mod twilio; +pub mod config; From b14cccbc2acebdd5257d4fbd5c4acab831e4218d Mon Sep 17 00:00:00 2001 From: ofcljaved Date: Sun, 26 Jan 2025 04:10:39 +0530 Subject: [PATCH 05/11] chore: user and admin pass the test --- latent-backend/api/src/main.rs | 5 +++-- latent-backend/api/src/routes/user.rs | 22 +++++++++++++++++----- latent-backend/api/src/utils/config.rs | 1 + latent-backend/api/test.sh | 8 ++++---- 4 files changed, 25 insertions(+), 11 deletions(-) diff --git a/latent-backend/api/src/main.rs b/latent-backend/api/src/main.rs index 05bee6d..de34630 100644 --- a/latent-backend/api/src/main.rs +++ b/latent-backend/api/src/main.rs @@ -38,7 +38,8 @@ async fn main() -> Result<(), std::io::Error> { // Create API service let api_service = - OpenApiService::new(routes::user::UserApi, "Latent Booking", "1.0").server(&server_url); + OpenApiService::new(routes::user::UserApi, "Latent Booking", "1.0") + .server(format!("{}/user", server_url)); let admin_api_service = OpenApiService::new(routes::admin::AdminApi, "Admin Latent Booking", "1.0") @@ -49,7 +50,7 @@ async fn main() -> Result<(), std::io::Error> { // Create route with CORS let mut app = Route::new() - .nest("/api/v1", api_service) + .nest("/api/v1/user", api_service) .nest("/api/v1/admin", admin_api_service) .nest("/docs", ui); diff --git a/latent-backend/api/src/routes/user.rs b/latent-backend/api/src/routes/user.rs index bfdf026..9c2976c 100644 --- a/latent-backend/api/src/routes/user.rs +++ b/latent-backend/api/src/routes/user.rs @@ -17,7 +17,7 @@ struct CreateUserResponse { #[derive(Debug, Serialize, Deserialize, Object)] struct CreateUserVerify { number: String, - totp: String, + otp: String, name: String, } @@ -39,7 +39,7 @@ struct SignInResponse { #[derive(Debug, Serialize, Deserialize, Object)] struct SignInVerify { number: String, - totp: String, + otp: String, } pub struct UserApi; @@ -77,17 +77,19 @@ impl UserApi { body: Json, state: Data<&AppState> ) -> poem::Result, AppError> { - let CreateUserVerify { number, totp: otp, name } = body.0; + let CreateUserVerify { number, otp, name } = body.0; // Verify OTP if cfg!(not(debug_assertions)) { if !totp::verify_token(&number, "AUTH", &otp) { + println!("Invalid OTP"); return Err(AppError::InvalidCredentials(payload::Json(crate::error::ErrorBody { message: "Invalid OTP".to_string(), }))); } } + println!("Verifying user with number: {}", number); let token = state.db.verify_user(number, name).await?; Ok(payload::Json(VerifyUserResponse { token })) @@ -102,8 +104,18 @@ impl UserApi { ) -> poem::Result, AppError> { let number = body.0.number; - let _user = state.db.get_user_by_number(&number).await?; + let user_result = state.db.get_user_by_number(&number).await; + if let Err(sqlx::Error::RowNotFound) = user_result { + return Err(AppError::AdminNotFound(payload::Json( + crate::error::ErrorBody { + message: "User not found".to_string(), + }, + ))); + } + + let _user = user_result?; + // Generate and send OTP let otp = totp::get_token(&number, "AUTH"); if cfg!(not(debug_assertions)) { @@ -128,7 +140,7 @@ impl UserApi { body: Json, state: Data<&AppState> ) -> poem::Result, AppError> { - let SignInVerify { number, totp: otp } = body.0; + let SignInVerify { number, otp } = body.0; // Verify OTP if cfg!(not(debug_assertions)) { diff --git a/latent-backend/api/src/utils/config.rs b/latent-backend/api/src/utils/config.rs index dce6d0a..254cc93 100644 --- a/latent-backend/api/src/utils/config.rs +++ b/latent-backend/api/src/utils/config.rs @@ -1,5 +1,6 @@ use std::env; +#[allow(dead_code)] pub fn jwt_password() -> String { env::var("JWT_PASSWORD").unwrap_or_else(|_| "123random".to_string()) } diff --git a/latent-backend/api/test.sh b/latent-backend/api/test.sh index 3ee476c..f447ba8 100755 --- a/latent-backend/api/test.sh +++ b/latent-backend/api/test.sh @@ -4,7 +4,7 @@ PORT=${PORT:-8080} BASE_URL="http://localhost:${PORT}" echo "1. Creating new user..." -curl -X POST "$BASE_URL/api/v1/signup" \ +curl -X POST "$BASE_URL/api/v1/user/signup" \ -H "Content-Type: application/json" \ -d '{"number": "9729302411"}' | jq @@ -12,7 +12,7 @@ echo -e "\nPress Enter to continue with signup verification..." read echo "2. Verifying signup..." -curl -X POST "$BASE_URL/api/v1/signup/verify" \ +curl -X POST "$BASE_URL/api/v1/user/signup/verify" \ -H "Content-Type: application/json" \ -d '{ "number": "9729302411", @@ -24,7 +24,7 @@ echo -e "\nPress Enter to continue with signin..." read echo "3. Signing in..." -curl -X POST "$BASE_URL/api/v1/signin" \ +curl -X POST "$BASE_URL/api/v1/user/signin" \ -H "Content-Type: application/json" \ -d '{"number": "9729302411"}' | jq @@ -32,7 +32,7 @@ echo -e "\nPress Enter to continue with signin verification..." read echo "4. Verifying signin..." -curl -X POST "$BASE_URL/api/v1/signin/verify" \ +curl -X POST "$BASE_URL/api/v1/user/signin/verify" \ -H "Content-Type: application/json" \ -d '{ "number": "9729302411", From f8b954f9fae51676a5cbcd39179a23f80a406caa Mon Sep 17 00:00:00 2001 From: ofcljaved Date: Sun, 26 Jan 2025 04:41:01 +0530 Subject: [PATCH 06/11] feat: create jwt token --- latent-backend/api/src/routes/admin.rs | 26 ++-------------- latent-backend/api/src/routes/test.rs | 20 +++++++++--- latent-backend/api/src/utils/jwt.rs | 42 ++++++++++++++++++++++++++ latent-backend/api/src/utils/mod.rs | 1 + 4 files changed, 60 insertions(+), 29 deletions(-) create mode 100644 latent-backend/api/src/utils/jwt.rs diff --git a/latent-backend/api/src/routes/admin.rs b/latent-backend/api/src/routes/admin.rs index 6b9613f..80840f9 100644 --- a/latent-backend/api/src/routes/admin.rs +++ b/latent-backend/api/src/routes/admin.rs @@ -1,7 +1,7 @@ use crate::{ error::AppError, middleware::admin::{admin_middleware, superadmin_middleware}, - utils::{config, totp, twilio}, + utils::{config, jwt::create_jwt, totp, twilio}, AppState, }; @@ -132,29 +132,7 @@ impl AdminApi { let user_id = state.db.verify_admin_signin(number).await?; - let current_time = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map_err(|_| { - poem::Error::from_string( - "Failed to get current time".to_string(), - poem::http::StatusCode::INTERNAL_SERVER_ERROR, - ) - })? - .as_secs() as usize; - - // Set expiration time to 1 hour from now - let exp = current_time + 3600; - - let claims = Claims { - sub: user_id.clone(), - exp, - }; - - let jwt_token = encode( - &Header::default(), - &claims, - &EncodingKey::from_secret(config::admin_jwt_password().as_bytes()), - )?; + let jwt_token = create_jwt(user_id, 3600, &config::admin_jwt_password())?; Ok(payload::Json(VerifyAdminResponse { token: jwt_token })) } diff --git a/latent-backend/api/src/routes/test.rs b/latent-backend/api/src/routes/test.rs index 8e099ea..6601aea 100644 --- a/latent-backend/api/src/routes/test.rs +++ b/latent-backend/api/src/routes/test.rs @@ -1,4 +1,4 @@ -use crate::{error::AppError, AppState}; +use crate::{error::AppError, utils::{config, jwt::create_jwt}, AppState}; use db::AdminType; use poem::web::{Data, Json}; use poem_openapi::{payload, Object, OpenApi}; @@ -13,7 +13,7 @@ struct CreateTestUser { #[derive(Debug, Deserialize, Serialize, Object)] struct CreateTestUserResponse { message: String, - id: String, + token: String, } pub struct TestApi; @@ -34,9 +34,13 @@ impl TestApi { .create_test_admin(number.clone(), name, AdminType::Creator) .await?; + + let user_id = user.id.to_string(); + let jwt_token = create_jwt(user_id, 3600, &config::admin_jwt_password())?; + Ok(payload::Json(CreateTestUserResponse { message: "Test Admin created successfully".to_string(), - id: user.id.to_string(), + token: jwt_token, })) } @@ -54,9 +58,12 @@ impl TestApi { .create_test_admin(number.clone(), name, AdminType::SuperAdmin) .await?; + let user_id = user.id.to_string(); + let jwt_token = create_jwt(user_id, 3600, &config::superadmin_jwt_password())?; + Ok(payload::Json(CreateTestUserResponse { message: "Test Super Admin created successfully".to_string(), - id: user.id.to_string(), + token: jwt_token, })) } @@ -71,9 +78,12 @@ impl TestApi { let name = body.0.name; let user = state.db.create_test_user(number.clone(), name).await?; + let user_id = user.id.to_string(); + let jwt_token = create_jwt(user_id, 3600, &config::jwt_password())?; + Ok(payload::Json(CreateTestUserResponse { message: "Test User created successfully".to_string(), - id: user.id.to_string(), + token: jwt_token, })) } } diff --git a/latent-backend/api/src/utils/jwt.rs b/latent-backend/api/src/utils/jwt.rs new file mode 100644 index 0000000..d46766b --- /dev/null +++ b/latent-backend/api/src/utils/jwt.rs @@ -0,0 +1,42 @@ +use jsonwebtoken::{encode, EncodingKey, Header}; +use serde::{Deserialize, Serialize}; +use std::time::{SystemTime, UNIX_EPOCH}; +use crate::error::AppError; +use poem_openapi::payload; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + pub sub: String, // Subject (e.g., user ID) + pub exp: usize, // Expiration time +} + +/// Generates a JWT token for the given user ID and expiration time. +pub fn create_jwt( + user_id: String, + expiration_seconds: usize, + secret_key: &str, +) -> Result { + let current_time = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|_| { + AppError::InternalServerError(payload::Json(crate::error::ErrorBody { + message: "Failed to get current time".to_string(), + })) + })? + .as_secs() as usize; + + let exp = current_time + expiration_seconds; + + let claims = Claims { + sub: user_id, + exp, + }; + + let jwt_token = encode( + &Header::default(), + &claims, + &EncodingKey::from_secret(secret_key.as_bytes()), + )?; + + Ok(jwt_token) +} diff --git a/latent-backend/api/src/utils/mod.rs b/latent-backend/api/src/utils/mod.rs index 65050dd..b4992da 100644 --- a/latent-backend/api/src/utils/mod.rs +++ b/latent-backend/api/src/utils/mod.rs @@ -1,3 +1,4 @@ pub mod totp; pub mod twilio; pub mod config; +pub mod jwt; From 9f720d548860972b9bee4554f0a381328c084938 Mon Sep 17 00:00:00 2001 From: ofcljaved Date: Sun, 26 Jan 2025 05:30:45 +0530 Subject: [PATCH 07/11] chore: testing event --- latent-backend/api/src/main.rs | 5 + latent-backend/api/src/middleware/admin.rs | 6 +- latent-backend/api/src/routes/event.rs | 142 +++++++++++++++++++++ latent-backend/api/src/routes/mod.rs | 1 + 4 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 latent-backend/api/src/routes/event.rs diff --git a/latent-backend/api/src/main.rs b/latent-backend/api/src/main.rs index de34630..718ed18 100644 --- a/latent-backend/api/src/main.rs +++ b/latent-backend/api/src/main.rs @@ -45,6 +45,10 @@ async fn main() -> Result<(), std::io::Error> { OpenApiService::new(routes::admin::AdminApi, "Admin Latent Booking", "1.0") .server(format!("{}/admin", server_url)); + let event_api_service = + OpenApiService::new(routes::event::EventApi, "Event Latent Booking", "1.0") + .server(format!("{}/admin/event", server_url)); + // Create Swagger UI let ui = api_service.swagger_ui(); @@ -52,6 +56,7 @@ async fn main() -> Result<(), std::io::Error> { let mut app = Route::new() .nest("/api/v1/user", api_service) .nest("/api/v1/admin", admin_api_service) + .nest("/api/v1/admin/event", event_api_service) .nest("/docs", ui); if cfg!(debug_assertions) { diff --git a/latent-backend/api/src/middleware/admin.rs b/latent-backend/api/src/middleware/admin.rs index 2e20898..fa90919 100644 --- a/latent-backend/api/src/middleware/admin.rs +++ b/latent-backend/api/src/middleware/admin.rs @@ -14,6 +14,9 @@ struct Claims { exp: usize, // Expiration time } +#[derive(Debug, Clone)] +pub struct TokenData(String); + pub struct JwtMiddleware { secret: String, // Secret key for JWT verification } @@ -55,7 +58,8 @@ impl poem::Endpoint for JwtMiddlewareImpl { match decode::(token, &decoding_key, &validation) { Ok(token_data) => { - req.extensions_mut().insert(token_data.claims.sub); + req.extensions_mut() + .insert(TokenData(token_data.claims.sub)); self.ep.call(req).await } Err(_) => Err( diff --git a/latent-backend/api/src/routes/event.rs b/latent-backend/api/src/routes/event.rs new file mode 100644 index 0000000..4e4d571 --- /dev/null +++ b/latent-backend/api/src/routes/event.rs @@ -0,0 +1,142 @@ +use crate::{ + error::AppError, + middleware::admin::{admin_middleware, superadmin_middleware, TokenData}, + utils::{config, jwt::create_jwt, totp, twilio}, + AppState, +}; + +use poem::{web::{Data, Json}, Request}; +use poem_openapi::{payload, Object, OpenApi}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Object)] +struct CreateEvent { + name: String, + description: String, + start_time: String, // Use chrono for date handling + location_id: String, + banner: String, + seats: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Object)] +struct SeatType { + name: String, + description: String, + price: f64, + capacity: i32, +} + +#[derive(Debug, Serialize, Deserialize, Object)] +struct UpdateEvent { + name: Option, + description: Option, + start_time: Option, + location_id: Option, + banner: Option, + published: Option, + ended: Option, +} + +#[derive(Debug, Serialize, Deserialize, Object)] +struct UpdateSeat { + id: Option, + name: String, + description: String, + price: f64, + capacity: i32, +} + +#[derive(Debug, Serialize, Deserialize, Object)] +struct EventResponse { + id: String, + name: String, + description: String, + start_time: String, + location_id: String, + banner: String, + seats: Vec, +} + +pub struct EventApi; + +#[OpenApi] +impl EventApi { + /// Create a new event + #[oai(path = "/", method = "get", transform = "admin_middleware")] + async fn create_event( + &self, + admin_id: Data<&TokenData>, + state: Data<&AppState>, + ) -> poem::Result, AppError>{ + println!("Admin ID from token: {:?}", admin_id.0); + + //let admin_id = state.admin_id; // Assuming admin_id is set by middleware + //let event = state.db.create_event(admin_id, body.0).await?; + Ok(payload::Json({"message"})) + } + + ///// Update event metadata + //#[oai( + // path = "/event/metadata/:event_id", + // method = "put", + // transform = "admin_middleware" + //)] + //async fn update_event_metadata( + // &self, + // event_id: Path, + // body: Json, + // state: Data<&AppState>, + //) -> poem::Result, AppError> { + // let admin_id = state.admin_id; + // let event = state + // .db + // .update_event_metadata(event_id.0, admin_id, body.0) + // .await?; + // Ok(payload::Json(event)) + //} + // + ///// List events for an admin + //#[oai(path = "/event", method = "get", transform = "admin_middleware")] + //async fn list_events( + // &self, + // state: Data<&AppState>, + //) -> poem::Result>, AppError> { + // let admin_id = state.admin_id; + // let events = state.db.get_events(admin_id).await?; + // Ok(payload::Json(events)) + //} + // + ///// Get a specific event + //#[oai( + // path = "/event/:event_id", + // method = "get", + // transform = "admin_middleware" + //)] + //async fn get_event( + // &self, + // event_id: Path, + // state: Data<&AppState>, + //) -> poem::Result, AppError> { + // let admin_id = state.admin_id; + // let event = state.db.get_event(event_id.0, admin_id).await?; + // Ok(payload::Json(event)) + //} + // + ///// Update seats for an event + //#[oai( + // path = "/event/seats/:event_id", + // method = "put", + // transform = "admin_middleware" + //)] + //async fn update_seats( + // &self, + // event_id: Path, + // body: Json>, + // state: Data<&AppState>, + //) -> poem::Result, AppError> { + // let admin_id = state.admin_id; + // state.db.update_seats(event_id.0, admin_id, body.0).await?; + // Ok(payload::Json("Seats updated successfully".to_string())) + //} +} diff --git a/latent-backend/api/src/routes/mod.rs b/latent-backend/api/src/routes/mod.rs index f28d0b2..9a0ae06 100644 --- a/latent-backend/api/src/routes/mod.rs +++ b/latent-backend/api/src/routes/mod.rs @@ -1,5 +1,6 @@ pub mod user; pub mod admin; +pub mod event; #[cfg(debug_assertions)] pub mod test; From 309526add1dec6ce323ba415f729d516a605a329 Mon Sep 17 00:00:00 2001 From: ofcljaved Date: Sun, 26 Jan 2025 18:08:44 +0530 Subject: [PATCH 08/11] feat: add event handler --- latent-backend/Cargo.lock | 11 + latent-backend/api/Cargo.toml | 2 + latent-backend/api/src/error.rs | 15 + latent-backend/api/src/middleware/admin.rs | 9 +- latent-backend/api/src/routes/event.rs | 406 ++++++++++++++++----- latent-backend/db/Cargo.toml | 4 +- latent-backend/db/src/event.rs | 360 ++++++++++++++++++ latent-backend/db/src/lib.rs | 15 +- 8 files changed, 720 insertions(+), 102 deletions(-) create mode 100644 latent-backend/db/src/event.rs diff --git a/latent-backend/Cargo.lock b/latent-backend/Cargo.lock index 7b6309b..e0a9af1 100644 --- a/latent-backend/Cargo.lock +++ b/latent-backend/Cargo.lock @@ -86,6 +86,7 @@ dependencies = [ name = "api" version = "0.1.0" dependencies = [ + "chrono", "db", "dotenv", "env_logger", @@ -99,6 +100,7 @@ dependencies = [ "sha2", "sqlx", "thiserror 2.0.11", + "time", "tokio", ] @@ -227,7 +229,10 @@ checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", + "serde", + "wasm-bindgen", "windows-targets 0.52.6", ] @@ -388,9 +393,11 @@ dependencies = [ name = "db" version = "0.1.0" dependencies = [ + "chrono", "dotenv", "log", "serde", + "serde_json", "sqlx", "tokio", "uuid", @@ -2183,6 +2190,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a007b6936676aa9ab40207cde35daab0a04b823be8ae004368c0793b96a61e0" dependencies = [ "bytes", + "chrono", "crc", "crossbeam-queue", "either", @@ -2262,6 +2270,7 @@ dependencies = [ "bitflags 2.7.0", "byteorder", "bytes", + "chrono", "crc", "digest", "dotenvy", @@ -2305,6 +2314,7 @@ dependencies = [ "base64 0.22.1", "bitflags 2.7.0", "byteorder", + "chrono", "crc", "dotenvy", "etcetera", @@ -2341,6 +2351,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f85ca71d3a5b24e64e1d08dd8fe36c6c95c339a896cc33068148906784620540" dependencies = [ "atoi", + "chrono", "flume", "futures-channel", "futures-core", diff --git a/latent-backend/api/Cargo.toml b/latent-backend/api/Cargo.toml index d3de34e..eafb145 100644 --- a/latent-backend/api/Cargo.toml +++ b/latent-backend/api/Cargo.toml @@ -18,3 +18,5 @@ reqwest = { version = "0.11.13", features = ["json"] } env_logger = "0.10" log = "0.4" sha2 = "0.10" +time = { version = "0.3", features = ["macros"] } +chrono = { version = "0.4", features = ["serde"] } diff --git a/latent-backend/api/src/error.rs b/latent-backend/api/src/error.rs index 7939d52..8a23895 100644 --- a/latent-backend/api/src/error.rs +++ b/latent-backend/api/src/error.rs @@ -1,3 +1,4 @@ +use db::DBError; use poem_openapi::{payload::Json, ApiResponse, Object}; use poem::error::Error as PoemError; use jsonwebtoken::errors::Error as JwtError; @@ -66,3 +67,17 @@ impl From for AppError { })) } } + +impl From for AppError { + fn from(err: DBError) -> Self { + match err { + DBError::NotFound(msg) => AppError::NotFound(Json(ErrorBody { + message: msg, + })), + DBError::InvalidInput(msg) => AppError::BadRequest(Json(ErrorBody { + message: msg, + })), + DBError::DatabaseError(sqlx_err) => sqlx_err.into(), // Convert sqlx::Error to AppError + } + } +} diff --git a/latent-backend/api/src/middleware/admin.rs b/latent-backend/api/src/middleware/admin.rs index fa90919..b2d82ba 100644 --- a/latent-backend/api/src/middleware/admin.rs +++ b/latent-backend/api/src/middleware/admin.rs @@ -15,7 +15,9 @@ struct Claims { } #[derive(Debug, Clone)] -pub struct TokenData(String); +pub struct TokenData { + pub id: String, +} pub struct JwtMiddleware { secret: String, // Secret key for JWT verification @@ -58,8 +60,9 @@ impl poem::Endpoint for JwtMiddlewareImpl { match decode::(token, &decoding_key, &validation) { Ok(token_data) => { - req.extensions_mut() - .insert(TokenData(token_data.claims.sub)); + req.extensions_mut().insert(TokenData { + id: token_data.claims.sub, + }); self.ep.call(req).await } Err(_) => Err( diff --git a/latent-backend/api/src/routes/event.rs b/latent-backend/api/src/routes/event.rs index 4e4d571..0b0801d 100644 --- a/latent-backend/api/src/routes/event.rs +++ b/latent-backend/api/src/routes/event.rs @@ -1,50 +1,49 @@ use crate::{ error::AppError, - middleware::admin::{admin_middleware, superadmin_middleware, TokenData}, - utils::{config, jwt::create_jwt, totp, twilio}, + middleware::admin::{admin_middleware, TokenData}, AppState, }; -use poem::{web::{Data, Json}, Request}; -use poem_openapi::{payload, Object, OpenApi}; +use db::{CreateEventInput, SeatTypeInput, SeatUpdateInput, UpdateEventInput}; +use poem::web::{Data, Json}; +use poem_openapi::{param::Path, payload, Object, OpenApi}; use serde::{Deserialize, Serialize}; +use sqlx::types::{time::PrimitiveDateTime, Uuid}; +use time::macros::format_description; #[derive(Debug, Serialize, Deserialize, Object)] struct CreateEvent { name: String, description: String, - start_time: String, // Use chrono for date handling - location_id: String, banner: String, + location_id: String, + start_time: String, seats: Vec, } +#[derive(Debug, Serialize, Deserialize, Object)] +struct CreateEventResponse { + message: String, + id: String, +} + #[derive(Debug, Serialize, Deserialize, Object)] struct SeatType { name: String, description: String, - price: f64, + price: i32, capacity: i32, } #[derive(Debug, Serialize, Deserialize, Object)] struct UpdateEvent { - name: Option, - description: Option, - start_time: Option, - location_id: Option, - banner: Option, - published: Option, - ended: Option, -} - -#[derive(Debug, Serialize, Deserialize, Object)] -struct UpdateSeat { - id: Option, name: String, description: String, - price: f64, - capacity: i32, + start_time: String, + location_id: String, + banner: String, + published: bool, + ended: bool, } #[derive(Debug, Serialize, Deserialize, Object)] @@ -55,7 +54,56 @@ struct EventResponse { start_time: String, location_id: String, banner: String, - seats: Vec, + published: bool, + ended: bool, + seats_types: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Object)] +struct SingleEventWithSeatsResponse { + message: String, + event: EventResponse, +} + +#[derive(Debug, Serialize, Deserialize, Object)] +struct EventWithSeatsResponse { + message: String, + event: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Object)] +struct UpdateSeat { + id: Option, + name: String, + description: String, + price: i32, + capacity: i32, +} + +impl UpdateSeat { + fn to_seat_update_input(&self) -> Result { + let id = match &self.id { + Some(id) => Some(Uuid::parse_str(id).map_err(|_| { + AppError::BadRequest(payload::Json(crate::error::ErrorBody { + message: format!("Invalid seat ID: {}", id), + })) + })?), + None => None, + }; + + Ok(SeatUpdateInput { + id, + name: self.name.clone(), + description: self.description.clone(), + price: self.price, + capacity: self.capacity, + }) + } +} + +#[derive(Debug, Serialize, Deserialize, Object)] +struct UpdateSeatsInput { + seats: Vec, } pub struct EventApi; @@ -63,80 +111,256 @@ pub struct EventApi; #[OpenApi] impl EventApi { /// Create a new event + #[oai(path = "/", method = "post", transform = "admin_middleware")] + async fn create_admin_event( + &self, + admin_id: Data<&TokenData>, + body: Json, + state: Data<&AppState>, + ) -> poem::Result, AppError> { + println!("Admin ID from token: {:?}", admin_id.id); + + let location_id = Uuid::parse_str(body.0.location_id.as_str()).map_err(|_| { + AppError::InvalidCredentials(payload::Json(crate::error::ErrorBody { + message: "Invalid location ID".to_string(), + })) + })?; + + let format = format_description!("[year]-[month]-[day] [hour]:[minute]:[second]"); + let start_time = + PrimitiveDateTime::parse(body.0.start_time.as_str(), &format).map_err(|_| { + AppError::InvalidCredentials(payload::Json(crate::error::ErrorBody { + message: "Invalid start time".to_string(), + })) + })?; + + let admin_id = Uuid::parse_str(admin_id.id.as_str()).map_err(|_| { + AppError::InvalidCredentials(payload::Json(crate::error::ErrorBody { + message: "Invalid admin ID".to_string(), + })) + })?; + + let event = state + .db + .create_event(CreateEventInput { + name: body.0.name, + description: body.0.description, + banner: body.0.banner, + admin_id, + location_id, + start_time, + seats: body + .0 + .seats + .iter() + .map(|st| SeatTypeInput { + name: st.name.clone(), + description: st.description.clone(), + price: st.price, + capacity: st.capacity, + }) + .collect(), + }) + .await?; + + Ok(payload::Json(CreateEventResponse { + message: "Event created successfully".to_string(), + id: event.id.to_string(), + })) + } + + /// Update event metadata + #[oai( + path = "/metadata/:event_id", + method = "put", + transform = "admin_middleware" + )] + async fn update_admin_event( + &self, + event_id: Path, + admin_id: Data<&TokenData>, + body: Json, + state: Data<&AppState>, + ) -> poem::Result, AppError> { + let location_id = Uuid::parse_str(body.0.location_id.as_str()).map_err(|_| { + AppError::InvalidCredentials(payload::Json(crate::error::ErrorBody { + message: "Invalid location ID".to_string(), + })) + })?; + + let format = format_description!("[year]-[month]-[day] [hour]:[minute]:[second]"); + let start_time = + PrimitiveDateTime::parse(body.0.start_time.as_str(), &format).map_err(|_| { + AppError::InvalidCredentials(payload::Json(crate::error::ErrorBody { + message: "Invalid start time".to_string(), + })) + })?; + + let admin_id = Uuid::parse_str(admin_id.id.as_str()).map_err(|_| { + AppError::InvalidCredentials(payload::Json(crate::error::ErrorBody { + message: "Invalid admin ID".to_string(), + })) + })?; + + let event_id = Uuid::parse_str(event_id.0.as_str()).map_err(|_| { + AppError::InvalidCredentials(payload::Json(crate::error::ErrorBody { + message: "Invalid admin ID".to_string(), + })) + })?; + let event = state + .db + .update_event_metadata(UpdateEventInput { + name: body.0.name, + description: body.0.description, + banner: body.0.banner, + admin_id, + location_id, + start_time, + published: body.0.published, + ended: body.0.ended, + event_id, + }) + .await?; + + Ok(payload::Json(CreateEventResponse { + message: "Event updated successfully".to_string(), + id: event.id.to_string(), + })) + } + + /// List events for an admin #[oai(path = "/", method = "get", transform = "admin_middleware")] - async fn create_event( + async fn list_events( &self, admin_id: Data<&TokenData>, state: Data<&AppState>, - ) -> poem::Result, AppError>{ - println!("Admin ID from token: {:?}", admin_id.0); + ) -> poem::Result, AppError> { + let admin_id = Uuid::parse_str(admin_id.id.as_str()).map_err(|_| { + AppError::InvalidCredentials(payload::Json(crate::error::ErrorBody { + message: "Invalid admin ID".to_string(), + })) + })?; - //let admin_id = state.admin_id; // Assuming admin_id is set by middleware - //let event = state.db.create_event(admin_id, body.0).await?; - Ok(payload::Json({"message"})) + let events = state.db.get_events(admin_id).await?; + let event_responses: Vec = events + .into_iter() + .map(|event| EventResponse { + id: event.id.to_string(), + name: event.name, + description: event.description, + banner: event.banner, + location_id: event.location_id.to_string(), + start_time: event.start_time.to_string(), + published: event.published, + ended: event.ended, + seats_types: event + .seat_types + .map(|seat_types_value| { + serde_json::from_value::>(seat_types_value) + .unwrap_or_else(|_| Vec::new()) // Handle deserialization errors + }) + .unwrap_or_default(), + }) + .collect(); + + Ok(payload::Json(EventWithSeatsResponse { + message: "Events fetched successfully".to_string(), + event: event_responses, + })) + } + + /// Get a specific event + #[oai(path = "/:event_id", method = "get", transform = "admin_middleware")] + async fn get_event_by_id( + &self, + event_id: Path, + admin_id: Data<&TokenData>, + state: Data<&AppState>, + ) -> poem::Result, AppError> { + let admin_id = Uuid::parse_str(admin_id.id.as_str()).map_err(|_| { + AppError::InvalidCredentials(payload::Json(crate::error::ErrorBody { + message: "Invalid admin ID".to_string(), + })) + })?; + + let event_id = Uuid::parse_str(event_id.0.as_str()).map_err(|_| { + AppError::InvalidCredentials(payload::Json(crate::error::ErrorBody { + message: "Invalid admin ID".to_string(), + })) + })?; + + let events = state.db.get_event(event_id, admin_id).await?; + let event_responses = EventResponse { + id: events.id.to_string(), + name: events.name, + description: events.description, + banner: events.banner, + location_id: events.location_id.to_string(), + start_time: events.start_time.to_string(), + published: events.published, + ended: events.ended, + seats_types: events + .seat_types + .map(|seat_types_value| { + serde_json::from_value::>(seat_types_value) + .unwrap_or_else(|_| Vec::new()) // Handle deserialization errors + }) + .unwrap_or_default(), + }; + + Ok(payload::Json(SingleEventWithSeatsResponse { + message: "Events fetched successfully".to_string(), + event: event_responses, + })) } - ///// Update event metadata - //#[oai( - // path = "/event/metadata/:event_id", - // method = "put", - // transform = "admin_middleware" - //)] - //async fn update_event_metadata( - // &self, - // event_id: Path, - // body: Json, - // state: Data<&AppState>, - //) -> poem::Result, AppError> { - // let admin_id = state.admin_id; - // let event = state - // .db - // .update_event_metadata(event_id.0, admin_id, body.0) - // .await?; - // Ok(payload::Json(event)) - //} - // - ///// List events for an admin - //#[oai(path = "/event", method = "get", transform = "admin_middleware")] - //async fn list_events( - // &self, - // state: Data<&AppState>, - //) -> poem::Result>, AppError> { - // let admin_id = state.admin_id; - // let events = state.db.get_events(admin_id).await?; - // Ok(payload::Json(events)) - //} - // - ///// Get a specific event - //#[oai( - // path = "/event/:event_id", - // method = "get", - // transform = "admin_middleware" - //)] - //async fn get_event( - // &self, - // event_id: Path, - // state: Data<&AppState>, - //) -> poem::Result, AppError> { - // let admin_id = state.admin_id; - // let event = state.db.get_event(event_id.0, admin_id).await?; - // Ok(payload::Json(event)) - //} - // - ///// Update seats for an event - //#[oai( - // path = "/event/seats/:event_id", - // method = "put", - // transform = "admin_middleware" - //)] - //async fn update_seats( - // &self, - // event_id: Path, - // body: Json>, - // state: Data<&AppState>, - //) -> poem::Result, AppError> { - // let admin_id = state.admin_id; - // state.db.update_seats(event_id.0, admin_id, body.0).await?; - // Ok(payload::Json("Seats updated successfully".to_string())) - //} + /// Update seats for an event + #[oai( + path = "/seats/:event_id", + method = "put", + transform = "admin_middleware" + )] + async fn update_seats( + &self, + event_id: Path, + admin_id: Data<&TokenData>, + body: Json, + state: Data<&AppState>, + ) -> poem::Result, AppError> { + let admin_id = Uuid::parse_str(admin_id.id.as_str()).map_err(|_| { + AppError::InvalidCredentials(payload::Json(crate::error::ErrorBody { + message: "Invalid admin ID".to_string(), + })) + })?; + + let event_id = Uuid::parse_str(event_id.0.as_str()).map_err(|_| { + AppError::InvalidCredentials(payload::Json(crate::error::ErrorBody { + message: "Invalid event ID".to_string(), + })) + })?; + + if body.0.seats.is_empty() { + return Err(AppError::BadRequest(payload::Json( + crate::error::ErrorBody { + message: "No seats provided".to_string(), + }, + ))); + } + + let seats: Result, AppError> = body + .0 + .seats + .into_iter() + .map(|seat| seat.to_seat_update_input()) + .collect(); + + let seats = seats?; + + state + .db + .update_seats(event_id, admin_id, seats) + .await?; + + Ok(payload::Json("Seats updated successfully".to_string())) + } } diff --git a/latent-backend/db/Cargo.toml b/latent-backend/db/Cargo.toml index 088da88..6323138 100644 --- a/latent-backend/db/Cargo.toml +++ b/latent-backend/db/Cargo.toml @@ -4,9 +4,11 @@ version = "0.1.0" edition = "2021" [dependencies] -sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "uuid", "time", "tls-native-tls"] } +sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "uuid", "time", "tls-native-tls", "chrono"] } tokio = { version = "1.43.0", features = ["full"] } serde = "1.0.217" uuid = { version = "1.6", features = ["v4", "serde"] } dotenv = "0.15.0" log = "0.4" +chrono = { version = "0.4", features = ["serde"] } +serde_json = "1.0" diff --git a/latent-backend/db/src/event.rs b/latent-backend/db/src/event.rs new file mode 100644 index 0000000..6d7864f --- /dev/null +++ b/latent-backend/db/src/event.rs @@ -0,0 +1,360 @@ +use crate::Db; +use log::info; +use serde::{Deserialize, Serialize}; +use sqlx::types::time::OffsetDateTime; +use sqlx::types::time::PrimitiveDateTime; +use sqlx::Error; +use sqlx::FromRow; +use uuid::Uuid; + +#[derive(FromRow, Serialize, Deserialize)] +pub struct Event { + pub id: Uuid, +} + +#[derive(Debug)] +pub struct SeatTypeInput { + pub name: String, + pub description: String, + pub price: i32, + pub capacity: i32, +} + +#[derive(Debug)] +pub struct CreateEventInput { + pub name: String, + pub description: String, + pub banner: String, + pub admin_id: Uuid, + pub location_id: Uuid, + pub start_time: PrimitiveDateTime, + pub seats: Vec, +} + +#[derive(Debug)] +pub struct UpdateEventInput { + pub name: String, + pub description: String, + pub banner: String, + pub admin_id: Uuid, + pub event_id: Uuid, + pub location_id: Uuid, + pub start_time: PrimitiveDateTime, + pub published: bool, + pub ended: bool, +} + +#[derive(FromRow, Serialize, Deserialize)] +pub struct EventWithSeats { + pub id: Uuid, + pub name: String, + pub description: String, + pub banner: String, + pub admin_id: Uuid, + pub location_id: Uuid, + pub start_time: chrono::NaiveDateTime, + pub published: bool, + pub ended: bool, + pub seat_types: Option, +} + +#[derive(Debug)] +pub struct SeatUpdateInput { + pub id: Option, + pub name: String, + pub description: String, + pub price: i32, + pub capacity: i32, +} + +#[derive(Debug)] +pub enum DBError { + NotFound(String), + InvalidInput(String), + DatabaseError(sqlx::Error), +} + +impl From for DBError { + fn from(err: sqlx::Error) -> Self { + DBError::DatabaseError(err) + } +} + +impl Db { + pub async fn create_event(&self, input: CreateEventInput) -> Result { + info!("Creating new event"); + + let seat_names: Vec = input.seats.iter().map(|st| st.name.clone()).collect(); + let seat_descriptions: Vec = input + .seats + .iter() + .map(|st| st.description.clone()) + .collect(); + let seat_prices: Vec = input.seats.iter().map(|st| st.price).collect(); + let seat_capacities: Vec = input.seats.iter().map(|st| st.capacity).collect(); + + let event = sqlx::query_as::<_, Event>( + r#" + WITH new_event AS ( + INSERT INTO events (id, name, description, banner, admin_id, location_id, start_time) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING id + ) + inserted_seats AS ( + INSERT INTO seat_types (id, name, description, event_id, price, capacity) + SELECT + uuid_generate_v4() AS id, + name, + description, + new_event.id, + price, + capacity + FROM UNNEST($8::text[], $9::text[], $10::int[], $11::int[]) AS t(name, description, price, capacity) + RETURNING * + ) + SELECT id FROM new_event + "#, + ) + .bind(Uuid::new_v4()) + .bind(input.name) + .bind(input.description) + .bind(input.banner) + .bind(input.admin_id) + .bind(input.location_id) + .bind(input.start_time) + .bind(seat_names) + .bind(seat_descriptions) + .bind(seat_prices) + .bind(seat_capacities) + .fetch_one(&self.client) + .await?; + + info!("Event and seats created/updated successfully"); + Ok(event) + } + + pub async fn update_event_metadata(&self, input: UpdateEventInput) -> Result { + info!("Updating event metadata"); + + let event = sqlx::query_as::<_, Event>( + r#" + UPDATE events + SET + name = $1, + description = $2, + start_time = $3, + location_id = $4, + banner = $5, + published = $6, + ended = $7 + WHERE id = $8 AND admin_id = $9 + RETURNING id + "#, + ) + .bind(input.name) + .bind(input.description) + .bind(input.start_time) + .bind(input.location_id) + .bind(input.banner) + .bind(input.published) + .bind(input.ended) + .bind(input.event_id) + .bind(input.admin_id) + .fetch_one(&self.client) + .await?; + + info!("Event metadata updated successfully"); + Ok(event) + } + + pub async fn get_events(&self, admin_id: Uuid) -> Result, Error> { + info!("Fetching events"); + let raw_events = sqlx::query_as::<_, EventWithSeats>( + r#" + SELECT + events.*, + json_agg( + json_build_object( + 'id', seat_types.id, + 'name', seat_types.name, + 'description', seat_types.description, + 'price', seat_types.price, + 'capacity', seat_types.capacity + ) + ) FILTER (WHERE seat_types.id IS NOT NULL) AS seat_types + FROM events + LEFT JOIN seat_types ON events.id = seat_types.event_id + WHERE events.admin_id = $1 + GROUP BY events.id + "#, + ) + .bind(admin_id) + .fetch_all(&self.client) + .await?; + let events = raw_events + .into_iter() + .map(|raw_event| { + let seat_types = raw_event + .seat_types + .map(|value| serde_json::from_value(value).unwrap_or_default()) + .unwrap_or_default(); + + EventWithSeats { + id: raw_event.id, + name: raw_event.name, + description: raw_event.description, + banner: raw_event.banner, + admin_id: raw_event.admin_id, + location_id: raw_event.location_id, + start_time: raw_event.start_time, + published: raw_event.published, + ended: raw_event.ended, + seat_types: Some(seat_types), + } + }) + .collect(); + + info!("Events fetched successfully"); + Ok(events) + } + + pub async fn get_event(&self, event_id: Uuid, admin_id: Uuid) -> Result { + info!("Fetching event"); + let event = sqlx::query_as::<_, EventWithSeats>( + r#" + SELECT + events.*, + json_agg( + json_build_object( + 'id', seat_types.id, + 'name', seat_types.name, + 'description', seat_types.description, + 'price', seat_types.price, + 'capacity', seat_types.capacity + ) + ) FILTER (WHERE seat_types.id IS NOT NULL) AS seat_types + FROM events + LEFT JOIN seat_types ON events.id = seat_types.event_id + WHERE events.id = $1 AND events.admin_id = $2 + GROUP BY events.id + "#, + ) + .bind(event_id) + .bind(admin_id) + .fetch_one(&self.client) + .await?; + + info!("Event fetched successfully"); + Ok(event) + } + + pub async fn update_seats( + &self, + event_id: Uuid, + admin_id: Uuid, + seats: Vec, + ) -> Result<(), DBError> { + info!("Updating seats for event"); + + let mut tx = self.client.begin().await?; + + let event = sqlx::query!( + r#" + SELECT id, start_time + FROM events + WHERE id = $1 AND admin_id = $2 + "#, + event_id, + admin_id + ) + .fetch_optional(&mut *tx) + .await?; + + let event = event.ok_or_else(|| Error::RowNotFound)?; + + let current_time = OffsetDateTime::now_utc(); + + // Check if the event has already started + if event.start_time > current_time { + return Err(DBError::InvalidInput( + "Event has already started".to_string(), + )); + } + + let current_seats = sqlx::query!( + r#" + SELECT id, name, description, price, capacity + FROM seat_types + WHERE event_id = $1 + "#, + event_id + ) + .fetch_all(&mut *tx) + .await?; + + let (new_seats, updated_seats): (Vec<_>, Vec<_>) = + seats.into_iter().partition(|seat| seat.id.is_none()); + + let deleted_seats: Vec = current_seats + .iter() + .filter(|current_seat| { + !updated_seats + .iter() + .any(|seat| seat.id.as_ref() == Some(¤t_seat.id)) + }) + .map(|seat| seat.id) + .collect(); + + // Perform the database operations in a transaction + if !deleted_seats.is_empty() { + sqlx::query!( + r#" + DELETE FROM seat_types + WHERE id = ANY($1) + "#, + &deleted_seats + ) + .execute(&mut *tx) + .await?; + } + + for seat in new_seats { + sqlx::query!( + r#" + INSERT INTO seat_types (id, name, description, price, capacity, event_id) + VALUES ($1, $2, $3, $4, $5, $6) + "#, + Uuid::new_v4(), + seat.name, + seat.description, + seat.price, + seat.capacity, + event_id + ) + .execute(&mut *tx) + .await?; + } + + for seat in updated_seats { + sqlx::query!( + r#" + UPDATE seat_types + SET name = $1, description = $2, price = $3, capacity = $4 + WHERE id = $5 + "#, + seat.name, + seat.description, + seat.price, + seat.capacity, + seat.id.unwrap() + ) + .execute(&mut *tx) + .await?; + } + + // Commit the transaction + tx.commit().await?; + + Ok(()) + } +} diff --git a/latent-backend/db/src/lib.rs b/latent-backend/db/src/lib.rs index 348ba1b..ca39170 100644 --- a/latent-backend/db/src/lib.rs +++ b/latent-backend/db/src/lib.rs @@ -1,17 +1,19 @@ +use log::{error, info}; use sqlx::postgres::PgPool; -use log::{info, error}; -mod config; -mod user; mod admin; +mod config; +mod event; mod location; +mod user; #[cfg(debug_assertions)] pub mod test; -pub use user::User; pub use admin::AdminType; +pub use event::{CreateEventInput, SeatTypeInput, UpdateEventInput, SeatUpdateInput, DBError}; pub use location::Location; +pub use user::User; pub struct Db { client: PgPool, @@ -26,7 +28,7 @@ impl Db { pub async fn init(&self) -> Result<(), sqlx::Error> { info!("Running database migrations..."); - + // First verify connection match sqlx::query("SELECT 1").execute(&self.client).await { Ok(_) => info!("Database connection successful"), @@ -41,7 +43,7 @@ impl Db { Ok(_) => { info!("Database migrations completed successfully"); Ok(()) - }, + } Err(e) => { error!("Migration failed: {}", e); Err(e.into()) @@ -49,4 +51,3 @@ impl Db { } } } - From 52ff11e0a7d78b661e04b329bff5148cd1b05587 Mon Sep 17 00:00:00 2001 From: ofcljaved Date: Sun, 26 Jan 2025 18:56:34 +0530 Subject: [PATCH 09/11] chore: all event test pass --- latent-backend/api/src/routes/admin.rs | 10 +++--- latent-backend/api/src/routes/event.rs | 10 +++--- latent-backend/db/src/event.rs | 43 ++++++++++++++------------ 3 files changed, 34 insertions(+), 29 deletions(-) diff --git a/latent-backend/api/src/routes/admin.rs b/latent-backend/api/src/routes/admin.rs index 80840f9..4603635 100644 --- a/latent-backend/api/src/routes/admin.rs +++ b/latent-backend/api/src/routes/admin.rs @@ -43,7 +43,7 @@ struct Location { id: String, name: String, description: String, - image_url: String, + imageUrl: String, } #[derive(Debug, Serialize, Deserialize, Object)] @@ -55,7 +55,7 @@ pub struct LocationResponse { pub struct CreateLocation { name: String, description: String, - image_url: String, + imageUrl: String, } #[derive(Debug, Serialize, Deserialize, Object)] @@ -150,7 +150,7 @@ impl AdminApi { id: l.id.to_string(), name: l.name.clone(), description: l.description.clone(), - image_url: l.image_url.clone(), + imageUrl: l.image_url.clone(), }) .collect(); @@ -170,12 +170,12 @@ impl AdminApi { let CreateLocation { name, description, - image_url, + imageUrl, } = body.0; let location = state .db - .create_location(name, description, image_url) + .create_location(name, description, imageUrl) .await?; Ok(payload::Json(CreateLocationResponse { diff --git a/latent-backend/api/src/routes/event.rs b/latent-backend/api/src/routes/event.rs index 0b0801d..a4f4af8 100644 --- a/latent-backend/api/src/routes/event.rs +++ b/latent-backend/api/src/routes/event.rs @@ -16,8 +16,8 @@ struct CreateEvent { name: String, description: String, banner: String, - location_id: String, - start_time: String, + locationId: String, + startTime: String, seats: Vec, } @@ -120,15 +120,15 @@ impl EventApi { ) -> poem::Result, AppError> { println!("Admin ID from token: {:?}", admin_id.id); - let location_id = Uuid::parse_str(body.0.location_id.as_str()).map_err(|_| { - AppError::InvalidCredentials(payload::Json(crate::error::ErrorBody { + let location_id = Uuid::parse_str(body.0.locationId.as_str()).map_err(|_| { + AppError::InternalServerErroInternalServerError(payload::Json(crate::error::ErrorBody { message: "Invalid location ID".to_string(), })) })?; let format = format_description!("[year]-[month]-[day] [hour]:[minute]:[second]"); let start_time = - PrimitiveDateTime::parse(body.0.start_time.as_str(), &format).map_err(|_| { + PrimitiveDateTime::parse(body.0.startTime.as_str(), &format).map_err(|_| { AppError::InvalidCredentials(payload::Json(crate::error::ErrorBody { message: "Invalid start time".to_string(), })) diff --git a/latent-backend/db/src/event.rs b/latent-backend/db/src/event.rs index 6d7864f..7ba6e6d 100644 --- a/latent-backend/db/src/event.rs +++ b/latent-backend/db/src/event.rs @@ -95,39 +95,44 @@ impl Db { let event = sqlx::query_as::<_, Event>( r#" - WITH new_event AS ( - INSERT INTO events (id, name, description, banner, admin_id, location_id, start_time) - VALUES ($1, $2, $3, $4, $5, $6, $7) - RETURNING id - ) - inserted_seats AS ( + INSERT INTO events (id, name, description, banner, admin_id, location_id, start_time) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING id + "#, + ) + .bind(Uuid::new_v4()) + .bind(input.name) + .bind(input.description) + .bind(input.banner) + .bind(input.admin_id) + .bind(input.location_id) + .bind(input.start_time) + .fetch_one(&self.client) + .await?; + + // Insert seats only if the array is not empty + if !input.seats.is_empty() { + sqlx::query( + r#" INSERT INTO seat_types (id, name, description, event_id, price, capacity) SELECT uuid_generate_v4() AS id, name, description, - new_event.id, + $1 AS event_id, price, capacity - FROM UNNEST($8::text[], $9::text[], $10::int[], $11::int[]) AS t(name, description, price, capacity) - RETURNING * - ) - SELECT id FROM new_event + FROM UNNEST($2::text[], $3::text[], $4::int[], $5::int[]) AS t(name, description, price, capacity) "#, ) - .bind(Uuid::new_v4()) - .bind(input.name) - .bind(input.description) - .bind(input.banner) - .bind(input.admin_id) - .bind(input.location_id) - .bind(input.start_time) + .bind(event.id) .bind(seat_names) .bind(seat_descriptions) .bind(seat_prices) .bind(seat_capacities) - .fetch_one(&self.client) + .execute(&self.client) .await?; + } info!("Event and seats created/updated successfully"); Ok(event) From 86ef908a6c969ca8fdb96ece5240da2e868c485a Mon Sep 17 00:00:00 2001 From: ofcljaved Date: Sun, 26 Jan 2025 19:18:41 +0530 Subject: [PATCH 10/11] fix: typo --- latent-backend/api/src/routes/event.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/latent-backend/api/src/routes/event.rs b/latent-backend/api/src/routes/event.rs index a4f4af8..1514f1c 100644 --- a/latent-backend/api/src/routes/event.rs +++ b/latent-backend/api/src/routes/event.rs @@ -121,7 +121,7 @@ impl EventApi { println!("Admin ID from token: {:?}", admin_id.id); let location_id = Uuid::parse_str(body.0.locationId.as_str()).map_err(|_| { - AppError::InternalServerErroInternalServerError(payload::Json(crate::error::ErrorBody { + AppError::InternalServerError(payload::Json(crate::error::ErrorBody { message: "Invalid location ID".to_string(), })) })?; From 1d005eedc1b3175f7772cae0c3216d9d7d83d317 Mon Sep 17 00:00:00 2001 From: ofcljaved Date: Sun, 26 Jan 2025 21:02:41 +0530 Subject: [PATCH 11/11] :chore: all js test passes --- latent-backend/api/src/routes/admin.rs | 2 -- latent-backend/api/src/routes/event.rs | 14 +++----------- latent-backend/db/src/event.rs | 4 +++- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/latent-backend/api/src/routes/admin.rs b/latent-backend/api/src/routes/admin.rs index 4603635..936a4c9 100644 --- a/latent-backend/api/src/routes/admin.rs +++ b/latent-backend/api/src/routes/admin.rs @@ -5,11 +5,9 @@ use crate::{ AppState, }; -use jsonwebtoken::{encode, EncodingKey, Header}; use poem::web::{Data, Json}; use poem_openapi::{payload, Object, OpenApi}; use serde::{Deserialize, Serialize}; -use std::time::{SystemTime, UNIX_EPOCH}; #[derive(Debug, Serialize, Deserialize)] struct Claims { diff --git a/latent-backend/api/src/routes/event.rs b/latent-backend/api/src/routes/event.rs index 1514f1c..a7ba5dc 100644 --- a/latent-backend/api/src/routes/event.rs +++ b/latent-backend/api/src/routes/event.rs @@ -56,7 +56,7 @@ struct EventResponse { banner: String, published: bool, ended: bool, - seats_types: Vec, + seatTypes: Vec, } #[derive(Debug, Serialize, Deserialize, Object)] @@ -253,7 +253,7 @@ impl EventApi { start_time: event.start_time.to_string(), published: event.published, ended: event.ended, - seats_types: event + seatTypes: event .seat_types .map(|seat_types_value| { serde_json::from_value::>(seat_types_value) @@ -299,7 +299,7 @@ impl EventApi { start_time: events.start_time.to_string(), published: events.published, ended: events.ended, - seats_types: events + seatTypes: events .seat_types .map(|seat_types_value| { serde_json::from_value::>(seat_types_value) @@ -339,14 +339,6 @@ impl EventApi { })) })?; - if body.0.seats.is_empty() { - return Err(AppError::BadRequest(payload::Json( - crate::error::ErrorBody { - message: "No seats provided".to_string(), - }, - ))); - } - let seats: Result, AppError> = body .0 .seats diff --git a/latent-backend/db/src/event.rs b/latent-backend/db/src/event.rs index 7ba6e6d..26984a3 100644 --- a/latent-backend/db/src/event.rs +++ b/latent-backend/db/src/event.rs @@ -1,4 +1,6 @@ use crate::Db; +use chrono::DateTime; +use chrono::Utc; use log::info; use serde::{Deserialize, Serialize}; use sqlx::types::time::OffsetDateTime; @@ -52,7 +54,7 @@ pub struct EventWithSeats { pub banner: String, pub admin_id: Uuid, pub location_id: Uuid, - pub start_time: chrono::NaiveDateTime, + pub start_time: DateTime, pub published: bool, pub ended: bool, pub seat_types: Option,