diff --git a/Cargo.lock b/Cargo.lock index 4049ed6..1fede50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -120,15 +120,6 @@ version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" -[[package]] -name = "arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" -dependencies = [ - "derive_arbitrary", -] - [[package]] name = "arrayvec" version = "0.7.6" @@ -438,8 +429,6 @@ dependencies = [ "tracing", "tracing-subscriber", "urlencoding", - "utoipa", - "utoipa-swagger-ui", "uuid", ] @@ -775,17 +764,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "derive_arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.115", -] - [[package]] name = "derive_more" version = "2.1.1" @@ -994,7 +972,6 @@ checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", - "zlib-rs", ] [[package]] @@ -2641,40 +2618,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rust-embed" -version = "8.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27" -dependencies = [ - "rust-embed-impl", - "rust-embed-utils", - "walkdir", -] - -[[package]] -name = "rust-embed-impl" -version = "8.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa" -dependencies = [ - "proc-macro2", - "quote", - "rust-embed-utils", - "syn 2.0.115", - "walkdir", -] - -[[package]] -name = "rust-embed-utils" -version = "8.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" -dependencies = [ - "sha2", - "walkdir", -] - [[package]] name = "rust_decimal" version = "1.40.0" @@ -2766,15 +2709,6 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - [[package]] name = "schannel" version = "0.1.28" @@ -4011,48 +3945,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "utoipa" -version = "5.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993" -dependencies = [ - "indexmap 2.13.0", - "serde", - "serde_json", - "utoipa-gen", -] - -[[package]] -name = "utoipa-gen" -version = "5.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b" -dependencies = [ - "proc-macro2", - "quote", - "regex", - "syn 2.0.115", -] - -[[package]] -name = "utoipa-swagger-ui" -version = "9.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d047458f1b5b65237c2f6dc6db136945667f40a7668627b3490b9513a3d43a55" -dependencies = [ - "axum", - "base64 0.22.1", - "mime_guess", - "regex", - "rust-embed", - "serde", - "serde_json", - "url", - "utoipa", - "zip", -] - [[package]] name = "uuid" version = "1.21.0" @@ -4083,16 +3975,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - [[package]] name = "want" version = "0.3.1" @@ -4273,15 +4155,6 @@ dependencies = [ "wasite", ] -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "windows-core" version = "0.62.2" @@ -4795,44 +4668,12 @@ dependencies = [ "syn 2.0.115", ] -[[package]] -name = "zip" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12598812502ed0105f607f941c386f43d441e00148fce9dec3ca5ffb0bde9308" -dependencies = [ - "arbitrary", - "crc32fast", - "flate2", - "indexmap 2.13.0", - "memchr", - "zopfli", -] - -[[package]] -name = "zlib-rs" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7948af682ccbc3342b6e9420e8c51c1fe5d7bf7756002b4a3c6cabfe96a7e3c" - [[package]] name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" -[[package]] -name = "zopfli" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" -dependencies = [ - "bumpalo", - "crc32fast", - "log", - "simd-adler32", -] - [[package]] name = "zstd" version = "0.13.3" diff --git a/Cargo.toml b/Cargo.toml index d4baf90..4ae924e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,5 +22,3 @@ jsonwebtoken = "9.3.1" anyhow = "1.0.95" reqwest = { version = "0.12.12", features = ["json", "rustls-tls"] } serde_json = "1.0.138" -utoipa = { version = "5.4.0", features = ["axum_extras", "chrono", "macros"] } -utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] } diff --git a/migration/src/lib.rs b/migration/src/lib.rs index 61a1736..48eb6b5 100644 --- a/migration/src/lib.rs +++ b/migration/src/lib.rs @@ -2,7 +2,6 @@ pub use sea_orm_migration::prelude::*; mod m20220101_000001_create_event_table; mod m20250215_213300_create_user_table; -mod m20260216_215000_refactor_user_id_to_sub; pub struct Migrator; @@ -12,7 +11,6 @@ impl MigratorTrait for Migrator { vec![ Box::new(m20220101_000001_create_event_table::Migration), Box::new(m20250215_213300_create_user_table::Migration), - Box::new(m20260216_215000_refactor_user_id_to_sub::Migration), ] } } diff --git a/migration/src/m20260216_215000_refactor_user_id_to_sub.rs b/migration/src/m20260216_215000_refactor_user_id_to_sub.rs deleted file mode 100644 index cc20bc2..0000000 --- a/migration/src/m20260216_215000_refactor_user_id_to_sub.rs +++ /dev/null @@ -1,123 +0,0 @@ -use sea_orm_migration::prelude::*; - -#[derive(DeriveMigrationName)] -pub struct Migration; - -#[async_trait::async_trait] -impl MigrationTrait for Migration { - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - // Drop foreign key - manager - .drop_foreign_key( - ForeignKey::drop() - .name("fk-event-user_id") - .table(Event::Table) - .to_owned(), - ) - .await?; - - // Add user_sub column - manager - .alter_table( - Table::alter() - .table(Event::Table) - .add_column(ColumnDef::new(Event::UserSub).string().null()) - .to_owned(), - ) - .await?; - - // Copy user_sub from user table via user_id - manager - .exec_stmt( - Query::update() - .table(Event::Table) - .values([( - Event::UserSub, - SimpleExpr::SubQuery( - None, - Box::new( - Query::select() - .column(User::Sub) - .from(User::Table) - .and_where( - Expr::col((User::Table, User::Id)) - .eq(Expr::col((Event::Table, Event::UserId))), - ) - .to_owned() - .into_sub_query_statement(), - ), - ), - )]) - .to_owned(), - ) - .await?; - - // Drop user_id column - manager - .alter_table( - Table::alter() - .table(Event::Table) - .drop_column(Event::UserId) - .to_owned(), - ) - .await?; - - // Create new foreign key to user.sub - manager - .create_foreign_key( - ForeignKey::create() - .name("fk-event-user_sub") - .from(Event::Table, Event::UserSub) - .to(User::Table, User::Sub) - .on_delete(ForeignKeyAction::Cascade) - .to_owned(), - ) - .await - } - - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - manager - .drop_foreign_key( - ForeignKey::drop() - .name("fk-event-user_sub") - .table(Event::Table) - .to_owned(), - ) - .await?; - - manager - .alter_table( - Table::alter() - .table(Event::Table) - .drop_column(Event::UserSub) - .add_column(ColumnDef::new(Event::UserId).integer()) - .to_owned(), - ) - .await?; - - manager - .create_foreign_key( - ForeignKey::create() - .name("fk-event-user_id") - .from(Event::Table, Event::UserId) - .to(User::Table, User::Id) - .on_delete(ForeignKeyAction::Cascade) - .to_owned(), - ) - .await - } -} - -#[derive(DeriveIden)] -enum User { - Table, - Id, - Sub, -} - -#[derive(DeriveIden)] -enum Event { - Table, - UserId, - UserSub, -} diff --git a/src/auth.rs b/src/auth.rs index e5f41fa..8e1f3e1 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -11,7 +11,6 @@ use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter}; use serde::{Deserialize, Serialize}; use std::sync::Arc; use tokio::sync::RwLock; -use utoipa::ToSchema; #[derive(Clone, FromRef)] pub struct AppState { @@ -219,10 +218,9 @@ pub struct Claims { pub email: Option, pub preferred_username: Option, pub exp: usize, - pub groups: Option>, } -#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct CurrentUser { pub id: i32, pub sub: String, @@ -275,48 +273,3 @@ where Err((axum::http::StatusCode::UNAUTHORIZED, "Unauthorized").into_response()) } } - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct ServiceAccount { - pub sub: String, -} - -impl FromRequestParts for ServiceAccount -where - AppState: FromRef, - S: Send + Sync, -{ - type Rejection = Response; - - async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { - let state = AppState::from_ref(state); - let oidc = state.oidc; - - let auth_header = parts - .headers - .get(header::AUTHORIZATION) - .and_then(|h| h.to_str().ok()); - - if let Some(auth_header) = auth_header { - if auth_header.starts_with("Bearer ") { - let token = &auth_header[7..]; - if let Ok(claims) = oidc.validate_token(token).await { - if claims - .groups - .as_ref() - .map(|g| g.contains(&"agent".to_string())) - .unwrap_or(false) - { - return Ok(ServiceAccount { sub: claims.sub }); - } - } - } - } - - Err(( - axum::http::StatusCode::UNAUTHORIZED, - "Unauthorized Service Account", - ) - .into_response()) - } -} diff --git a/src/entities/event.rs b/src/entities/event.rs index 03303eb..495d3f3 100644 --- a/src/entities/event.rs +++ b/src/entities/event.rs @@ -1,13 +1,12 @@ use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; -use utoipa::ToSchema; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize, ToSchema)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[sea_orm(table_name = "event")] pub struct Model { #[sea_orm(primary_key)] pub id: i64, - pub user_sub: String, + pub user_id: Option, pub name: String, pub from: DateTime, pub to: DateTime, @@ -17,8 +16,8 @@ pub struct Model { pub enum Relation { #[sea_orm( belongs_to = "super::user::Entity", - from = "Column::UserSub", - to = "super::user::Column::Sub" + from = "Column::UserId", + to = "super::user::Column::Id" )] User, } diff --git a/src/entities/user.rs b/src/entities/user.rs index 8a2bf14..f68ec42 100644 --- a/src/entities/user.rs +++ b/src/entities/user.rs @@ -1,8 +1,7 @@ use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; -use utoipa::ToSchema; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize, ToSchema)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] #[sea_orm(table_name = "user")] pub struct Model { #[sea_orm(primary_key)] diff --git a/src/handlers/auth.rs b/src/handlers/auth.rs index 25940db..d3b1d80 100644 --- a/src/handlers/auth.rs +++ b/src/handlers/auth.rs @@ -1,7 +1,7 @@ use crate::auth::AppState; use crate::entities::user; -use crate::handlers::Json; use axum::{ + Json, extract::{Query, State}, response::{IntoResponse, Redirect, Response}, }; @@ -15,15 +15,12 @@ pub struct AuthCallback { pub state: String, } -#[tracing::instrument(skip(state))] pub async fn login(State(state): State) -> Response { - tracing::info!("Initiating login redirect"); let oidc = state.oidc; let (auth_url, _csrf_token, _nonce) = oidc.auth_url(); Redirect::to(&auth_url).into_response() } -#[tracing::instrument(skip(state, params), fields(code = %params.code))] pub async fn callback( State(state): State, Query(params): Query, @@ -45,23 +42,19 @@ pub async fn callback( .unwrap_or(None); if existing_user.is_none() { - tracing::info!("Creating new user for sub {}", sub); let new_user = user::ActiveModel { - sub: Set(sub.clone()), + sub: Set(sub), email: Set(email), name: Set(name), ..Default::default() }; new_user.insert(&db).await.unwrap(); } else { - tracing::debug!("Updating existing user for sub {}", sub); let mut user: user::ActiveModel = existing_user.unwrap().into(); user.name = Set(name); user.update(&db).await.unwrap(); } - tracing::info!("User {} logged in successfully", sub); - let mut redirect_url = format!("/#access_token={}", tokens.id_token); if let Some(refresh_token) = tokens.refresh_token { redirect_url.push_str(&format!("&refresh_token={}", refresh_token)); @@ -89,7 +82,6 @@ pub async fn callback( } } -#[tracing::instrument(skip(state, payload))] pub async fn refresh( State(state): State, Json(payload): Json, @@ -97,10 +89,7 @@ pub async fn refresh( let oidc = state.oidc; match oidc.refresh_token(payload.refresh_token).await { - Ok(tokens) => { - tracing::info!("Token refreshed successfully"); - Json(tokens).into_response() - } + Ok(tokens) => Json(tokens).into_response(), Err(e) => { tracing::error!("Token refresh error: {:?}", e); (axum::http::StatusCode::UNAUTHORIZED, "Refresh failed").into_response() @@ -108,7 +97,7 @@ pub async fn refresh( } } -#[derive(Debug, Deserialize)] +#[derive(Deserialize)] pub struct RefreshRequest { pub refresh_token: String, } @@ -117,19 +106,18 @@ pub async fn logout() -> Response { Redirect::to("/").into_response() } -#[utoipa::path( - get, - path = "/auth/me", - responses( - (status = 200, description = "Current user profile", body = crate::auth::CurrentUser), - (status = 401, description = "Unauthorized") - ), - security( - ("oidc" = []) - ) -)] -#[tracing::instrument(skip(user))] -pub async fn me(user: crate::auth::CurrentUser) -> Json { - tracing::debug!("User profile requested for {}", user.sub); - Json(user) +pub async fn me( + crate::auth::CurrentUser { + id, + sub, + email, + name, + }: crate::auth::CurrentUser, +) -> Json { + Json(crate::auth::CurrentUser { + id, + sub, + email, + name, + }) } diff --git a/src/handlers/event.rs b/src/handlers/event.rs index a733f7d..66ec494 100644 --- a/src/handlers/event.rs +++ b/src/handlers/event.rs @@ -1,38 +1,25 @@ use crate::auth::{AppState, CurrentUser}; use crate::entities::{event, event::Entity as Event}; -use crate::handlers::Json; use axum::{ + Json, extract::{Path, Query, State}, http::StatusCode, }; -use chrono::{DateTime, TimeZone, Utc}; use sea_orm::*; -use sea_orm::{ActiveValue::Set, sea_query::Expr}; use serde::{Deserialize, Serialize}; -use utoipa::{IntoParams, ToSchema}; - -#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] +#[derive(Clone, Serialize, Deserialize)] pub struct CreateEventRequest { pub name: String, pub from: String, pub to: String, } -#[derive(Debug, Deserialize, IntoParams)] +#[derive(Deserialize)] pub struct ListEventsQuery { pub upcoming: Option, } -#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] -pub struct EventResponse { - pub id: i64, - pub user_sub: String, - pub name: String, - pub from: DateTime, - pub to: DateTime, -} - fn parse_datetime(dt_str: &str) -> Result { if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(dt_str, "%Y-%m-%dT%H:%M") { return Ok(dt); @@ -43,23 +30,9 @@ fn parse_datetime(dt_str: &str) -> Result { dt_str.parse().map_err(|_| StatusCode::BAD_REQUEST) } -#[utoipa::path( - post, - path = "/events", - request_body = CreateEventRequest, - responses( - (status = 200, description = "Event created successfully", body = event::Model), - (status = 400, description = "Invalid request payload"), - (status = 401, description = "Unauthorized") - ), - security( - ("oidc" = []) - ) -)] -#[tracing::instrument(skip(state, payload), fields(user_sub = %sub, event_name = %payload.name))] pub async fn create_event( State(state): State, - CurrentUser { sub, .. }: CurrentUser, + CurrentUser { id, .. }: CurrentUser, Json(payload): Json, ) -> Result, StatusCode> { let db = state.db; @@ -70,7 +43,7 @@ pub async fn create_event( name: Set(payload.name), from: Set(from), to: Set(to), - user_sub: Set(sub.clone()), + user_id: Set(Some(id)), ..Default::default() }; @@ -79,174 +52,97 @@ pub async fn create_event( StatusCode::INTERNAL_SERVER_ERROR })?; - tracing::info!("Created event {} for user {}", result.id, sub); + tracing::info!("Created event {} for user {}", result.id, id); Ok(Json(result)) } -#[utoipa::path( - get, - path = "/events", - params(ListEventsQuery), - responses( - (status = 200, description = "List of events", body = [event::Model]), - (status = 401, description = "Unauthorized") - ), - security( - ("oidc" = []) - ) -)] -#[tracing::instrument(skip(state), fields(user_sub = %sub))] pub async fn list_events( State(state): State, - CurrentUser { sub, .. }: CurrentUser, + CurrentUser { id, .. }: CurrentUser, Query(query): Query, ) -> Result>, StatusCode> { let db = state.db; - tracing::debug!("Listing events for user"); - let mut find = Event::find().filter(event::Column::UserSub.eq(sub.clone())); + let mut find = Event::find().filter(event::Column::UserId.eq(id)); if let Some(true) = query.upcoming { let now = chrono::Utc::now().naive_utc(); find = find.filter(event::Column::From.gte(now)); } - let events = find.all(&db).await.map_err(|e| { - tracing::error!("Failed to list events: {:?}", e); - StatusCode::INTERNAL_SERVER_ERROR - })?; - - tracing::info!("Listed {} events for user {}", events.len(), sub); + let events = find + .all(&db) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(events)) } -#[utoipa::path( - get, - path = "/events/{id}", - params( - ("id" = i32, Path, description = "Event database id") - ), - responses( - (status = 200, description = "Event details", body = event::Model), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Event not found") - ), - security( - ("oidc" = []) - ) -)] -#[tracing::instrument(skip(state), fields(user_sub = %sub, event_id = %event_id))] pub async fn get_event( State(state): State, - CurrentUser { sub, .. }: CurrentUser, + CurrentUser { id, .. }: CurrentUser, Path(event_id): Path, ) -> Result, StatusCode> { let db = state.db; let event = Event::find_by_id(event_id) - .filter(event::Column::UserSub.eq(sub.clone())) + .filter(event::Column::UserId.eq(id)) .one(&db) .await - .map_err(|e| { - tracing::error!("Failed to fetch event {}: {:?}", event_id, e); - StatusCode::INTERNAL_SERVER_ERROR - })? - .ok_or_else(|| { - tracing::warn!("Event {} not found for user {}", event_id, sub); - StatusCode::NOT_FOUND - })?; - - tracing::info!("Fetched event {} for user {}", event_id, sub); + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .ok_or(StatusCode::NOT_FOUND)?; Ok(Json(event)) } -#[utoipa::path( - put, - path = "/events/{id}", - params( - ("id" = i32, Path, description = "Event database id") - ), - request_body = CreateEventRequest, - responses( - (status = 200, description = "Event updated successfully", body = event::Model), - (status = 400, description = "Invalid request payload"), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Event not found") - ), - security( - ("oidc" = []) - ) -)] -#[tracing::instrument(skip(state, payload), fields(user_sub = %sub, event_id = %event_id, event_name = %payload.name))] pub async fn update_event( State(state): State, - CurrentUser { sub, .. }: CurrentUser, + CurrentUser { id, .. }: CurrentUser, Path(event_id): Path, Json(payload): Json, -) -> Result, StatusCode> { +) -> Result, StatusCode> { let db = state.db; let from = parse_datetime(&payload.from)?; let to = parse_datetime(&payload.to)?; - let result = Event::update_many() - .col_expr(event::Column::Name, Expr::value(payload.name.clone())) - .col_expr(event::Column::From, Expr::value(from)) - .col_expr(event::Column::To, Expr::value(to)) - .filter(event::Column::Id.eq(event_id as i64)) - .filter(event::Column::UserSub.eq(sub.clone())) - .exec(&db) + let event = Event::find_by_id(event_id) + .filter(event::Column::UserId.eq(id)) + .one(&db) .await - .map_err(|e| { - tracing::error!("Service failed to update event {}: {:?}", event_id, e); - StatusCode::INTERNAL_SERVER_ERROR - })?; + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .ok_or(StatusCode::NOT_FOUND)?; - if result.rows_affected == 0 { - return Err(StatusCode::NOT_FOUND); - } + let mut event: event::ActiveModel = event.into(); + event.name = Set(payload.name); + event.from = Set(from); + event.to = Set(to); - Ok(Json(EventResponse { - id: event_id as i64, - user_sub: sub, - name: payload.name, - from: Utc.from_utc_datetime(&from), - to: Utc.from_utc_datetime(&to), - })) + let result = event.update(&db).await.map_err(|e| { + tracing::error!("Failed to update event {}: {:?}", event_id, e); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + tracing::info!("Updated event {} for user {}", event_id, id); + + Ok(Json(result)) } -#[utoipa::path( - delete, - path = "/events/{id}", - params( - ("id" = i32, Path, description = "Event database id") - ), - responses( - (status = 204, description = "Event deleted successfully"), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Event not found") - ), - security( - ("oidc" = []) - ) -)] -#[tracing::instrument(skip(state), fields(user_sub = %sub, event_id = %event_id))] pub async fn delete_event( State(state): State, - CurrentUser { sub, .. }: CurrentUser, + CurrentUser { id, .. }: CurrentUser, Path(event_id): Path, ) -> Result { let db = state.db; - Event::delete_by_id(event_id as i64) - .filter(event::Column::UserSub.eq(sub.clone())) - .exec(&db) + let event = Event::find_by_id(event_id) + .filter(event::Column::UserId.eq(id)) + .one(&db) .await - .map_err(|e| { - tracing::error!("Failed to delete event {}: {:?}", event_id, e); - StatusCode::INTERNAL_SERVER_ERROR - })?; + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .ok_or(StatusCode::NOT_FOUND)?; - tracing::info!("Deleted event {} for user {}", event_id, sub); + event + .delete(&db) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(StatusCode::NO_CONTENT) } diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 4e58d6c..ec21bb6 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -1,49 +1,2 @@ pub mod auth; pub mod event; -pub mod service; - -use axum::{ - extract::Request, - extract::{FromRequest, rejection::JsonRejection}, - http::StatusCode, - response::{IntoResponse, Response}, -}; -use serde::Serialize; -use std::future::Future; -use std::pin::Pin; - -pub struct Json(pub T); - -impl FromRequest for Json -where - axum::Json: FromRequest, - S: Send + Sync, - T: 'static, -{ - type Rejection = Response; - - fn from_request( - req: Request, - state: &S, - ) -> Pin> + Send + '_>> { - let fut = axum::Json::::from_request(req, state); - Box::pin(async move { - match fut.await { - Ok(value) => Ok(Self(value.0)), - Err(rejection) => { - tracing::error!(%rejection, "Json extraction failed"); - Err((StatusCode::BAD_REQUEST, rejection.body_text()).into_response()) - } - } - }) - } -} - -impl IntoResponse for Json -where - T: Serialize, -{ - fn into_response(self) -> Response { - axum::Json(self.0).into_response() - } -} diff --git a/src/handlers/service.rs b/src/handlers/service.rs deleted file mode 100644 index 7d90c59..0000000 --- a/src/handlers/service.rs +++ /dev/null @@ -1,228 +0,0 @@ -use crate::auth::{AppState, ServiceAccount}; -use crate::entities::{event, event::Entity as Event}; -use crate::handlers::Json; -use axum::{ - extract::{Path, Query, State}, - http::StatusCode, -}; -use chrono::{DateTime, TimeZone, Utc}; -use sea_orm::{ActiveValue::Set, sea_query::Expr, *}; -use serde::{Deserialize, Serialize}; - -use utoipa::{IntoParams, ToSchema}; - -#[derive(Serialize, ToSchema)] -pub struct ServiceEventResponse { - pub id: i64, - pub user_sub: String, - pub name: String, - pub from: DateTime, - pub to: DateTime, -} - -impl From for ServiceEventResponse { - fn from(m: event::Model) -> Self { - Self { - id: m.id, - user_sub: m.user_sub, - name: m.name, - from: Utc.from_utc_datetime(&m.from), - to: Utc.from_utc_datetime(&m.to), - } - } -} - -#[derive(Clone, Serialize, Deserialize, ToSchema)] -pub struct ServiceCreateEventRequest { - pub name: String, - pub from: String, - pub to: String, - pub user_sub: String, -} - -#[derive(Clone, Serialize, Deserialize, ToSchema)] -pub struct ServiceUpdateEventRequest { - pub name: String, - pub from: String, - pub to: String, - pub user_sub: String, -} - -#[derive(Deserialize, IntoParams)] -pub struct ServiceListEventsQuery { - pub user_sub: String, - pub upcoming: Option, -} - -fn parse_datetime(dt_str: &str) -> Result { - if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(dt_str, "%Y-%m-%dT%H:%M") { - return Ok(dt); - } - if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(dt_str, "%Y-%m-%dT%H:%M:%S") { - return Ok(dt); - } - dt_str.parse().map_err(|_| StatusCode::BAD_REQUEST) -} - -#[utoipa::path( - post, - path = "/service/v1/events", - request_body = ServiceCreateEventRequest, - responses( - (status = 200, description = "Event created successfully", body = ServiceEventResponse), - (status = 400, description = "Invalid request payload"), - (status = 401, description = "Unauthorized") - ), - security( - ("oidc" = []) - ) -)] -pub async fn service_create_event( - State(state): State, - _service_account: ServiceAccount, - Json(payload): Json, -) -> Result, StatusCode> { - let db = state.db; - let from = parse_datetime(&payload.from)?; - let to = parse_datetime(&payload.to)?; - - let new_event = event::ActiveModel { - name: Set(payload.name), - from: Set(from), - to: Set(to), - user_sub: Set(payload.user_sub), - ..Default::default() - }; - - let result = new_event.insert(&db).await.map_err(|e| { - tracing::error!("Service failed to create event: {:?}", e); - StatusCode::INTERNAL_SERVER_ERROR - })?; - - tracing::info!("Service created event {}", result.id); - - Ok(Json(result.into())) -} - -#[utoipa::path( - get, - path = "/service/v1/events", - params(ServiceListEventsQuery), - responses( - (status = 200, description = "List of events", body = [ServiceEventResponse]), - (status = 401, description = "Unauthorized") - ), - security( - ("oidc" = []) - ) -)] -pub async fn service_list_events( - State(state): State, - _service_account: ServiceAccount, - Query(query): Query, -) -> Result>, StatusCode> { - let db = state.db; - let mut find = Event::find(); - - find = find.filter(event::Column::UserSub.eq(query.user_sub)); - - if let Some(true) = query.upcoming { - let now = chrono::Utc::now().naive_utc(); - find = find.filter(event::Column::From.gte(now)); - } - - let events = find - .all(&db) - .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - - let response = events.into_iter().map(ServiceEventResponse::from).collect(); - - Ok(Json(response)) -} - -#[utoipa::path( - put, - path = "/service/v1/events/{id}", - params( - ("id" = i32, Path, description = "Event database id"), - ServiceListEventsQuery - ), - request_body = ServiceUpdateEventRequest, - responses( - (status = 200, description = "Event updated successfully", body = ServiceEventResponse), - (status = 400, description = "Invalid request payload"), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Event not found") - ), - security( - ("oidc" = []) - ) -)] -pub async fn service_update_event( - State(state): State, - _service_account: ServiceAccount, - Path(event_id): Path, - Json(payload): Json, -) -> Result, StatusCode> { - let db = state.db; - let from = parse_datetime(&payload.from)?; - let to = parse_datetime(&payload.to)?; - - let result = Event::update_many() - .col_expr(event::Column::Name, Expr::value(payload.name.clone())) - .col_expr(event::Column::From, Expr::value(from)) - .col_expr(event::Column::To, Expr::value(to)) - .filter(event::Column::Id.eq(event_id as i64)) - .filter(event::Column::UserSub.eq(payload.user_sub.clone())) - .exec(&db) - .await - .map_err(|e| { - tracing::error!("Service failed to update event {}: {:?}", event_id, e); - StatusCode::INTERNAL_SERVER_ERROR - })?; - - if result.rows_affected == 0 { - return Err(StatusCode::NOT_FOUND); - } - - Ok(Json(ServiceEventResponse { - id: event_id as i64, - user_sub: payload.user_sub, - name: payload.name, - from: Utc.from_utc_datetime(&from), - to: Utc.from_utc_datetime(&to), - })) -} - -#[utoipa::path( - delete, - path = "/service/v1/events/{id}", - params( - ("id" = i32, Path, description = "Event database id") - ), - responses( - (status = 204, description = "Event deleted successfully"), - (status = 401, description = "Unauthorized"), - (status = 404, description = "Event not found") - ), - security( - ("oidc" = []) - ) -)] -pub async fn service_delete_event( - State(state): State, - _service_account: ServiceAccount, - Path(event_id): Path, -) -> Result { - let db = state.db; - - Event::delete_by_id(event_id as i64) - .exec(&db) - .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - - tracing::info!("Service deleted event {}", event_id); - - Ok(StatusCode::NO_CONTENT) -} diff --git a/src/main.rs b/src/main.rs index f0e3123..60fec08 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,6 @@ mod config; mod db; mod entities; mod handlers; -mod middleware; mod routes; use crate::auth::{AppState, OidcClient}; @@ -48,9 +47,8 @@ async fn main() { // Create router let app = create_router(state) - .fallback_service(tower_http::services::ServeDir::new("static")) - .layer(axum::middleware::from_fn(middleware::log_errors)) - .layer(tower_http::cors::CorsLayer::permissive()); + .layer(tower_http::cors::CorsLayer::permissive()) + .fallback_service(tower_http::services::ServeDir::new("static")); // Start server let addr = SocketAddr::from(([127, 0, 0, 1], config.port)); diff --git a/src/middleware.rs b/src/middleware.rs deleted file mode 100644 index aafb250..0000000 --- a/src/middleware.rs +++ /dev/null @@ -1,20 +0,0 @@ -use axum::{body::Body, extract::Request, middleware::Next, response::Response}; -use tracing::error; - -pub async fn log_errors(req: Request, next: Next) -> Response { - let method = req.method().clone(); - let uri = req.uri().clone(); - - let response = next.run(req).await; - - if response.status().is_client_error() || response.status().is_server_error() { - error!( - method = ?method, - uri = ?uri, - status = ?response.status(), - "Error response returned" - ); - } - - response -} diff --git a/src/routes.rs b/src/routes.rs index 3997e8e..f38888f 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -2,46 +2,12 @@ use crate::auth::AppState; use crate::handlers::{auth, event}; use axum::{ Router, - routing::{get, post, put}, + routing::{get, post}, }; -use utoipa::OpenApi; -use utoipa_swagger_ui::SwaggerUi; - -#[derive(OpenApi)] -#[openapi( - paths( - crate::handlers::auth::me, - crate::handlers::event::create_event, - crate::handlers::event::list_events, - crate::handlers::event::get_event, - crate::handlers::event::update_event, - crate::handlers::event::delete_event, - crate::handlers::service::service_create_event, - crate::handlers::service::service_list_events, - crate::handlers::service::service_update_event, - crate::handlers::service::service_delete_event, - ), - components( - schemas( - crate::auth::CurrentUser, - crate::entities::user::Model, - crate::entities::event::Model, - crate::handlers::event::CreateEventRequest, - crate::handlers::service::ServiceCreateEventRequest, - crate::handlers::service::ServiceEventResponse, - ) - ), - tags( - (name = "calendar", description = "Calendar Management API") - ) -)] -struct ApiDoc; - pub fn create_router(state: AppState) -> Router { // Explicitly set the router state type to AppState Router::::new() - .merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi())) .route("/health", get(health_check)) .route("/auth/login", get(auth::login)) .route("/auth/callback", get(auth::callback)) @@ -55,17 +21,6 @@ pub fn create_router(state: AppState) -> Router { .put(event::update_event) .delete(event::delete_event), ) - // Service endpoints - .route( - "/service/v1/events", - post(crate::handlers::service::service_create_event) - .get(crate::handlers::service::service_list_events), - ) - .route( - "/service/v1/events/{id}", - put(crate::handlers::service::service_update_event) - .delete(crate::handlers::service::service_delete_event), - ) .with_state(state) }