1- use crate :: { error:: ApiError , state:: AppState } ;
1+ use crate :: { error:: ApiError , middleware:: AuthenticatedUser , state:: AppState } ;
2+ use axum:: extract:: Query ;
23use axum:: {
3- extract:: { Query , State } ,
4+ extract:: { Extension , State } ,
45 http:: StatusCode ,
56 response:: Redirect ,
67 routing:: get,
7- Router ,
8+ Json , Router ,
89} ;
9- use serde:: Deserialize ;
10+ use serde:: { Deserialize , Serialize } ;
1011use services:: SessionId ;
12+ use utoipa:: ToSchema ;
1113
1214/// Query parameters for OAuth callback
1315#[ derive( Debug , Deserialize ) ]
@@ -23,6 +25,13 @@ pub struct OAuthInitQuery {
2325 pub frontend_callback : Option < String > ,
2426}
2527
28+ /// Request body for logout
29+ #[ derive( Debug , Deserialize , Serialize , ToSchema ) ]
30+ pub struct LogoutRequest {
31+ /// Session ID to revoke
32+ pub session_id : SessionId ,
33+ }
34+
2635/// Request body for mock login (test only)
2736#[ cfg( feature = "test" ) ]
2837#[ derive( Debug , Deserialize ) ]
@@ -148,9 +157,10 @@ pub async fn oauth_callback(
148157 tracing:: info!( "Redirecting to frontend: {}" , frontend_url) ;
149158
150159 let mut callback_url = format ! (
151- "{}/auth/callback?token={}&expires_at={}" ,
160+ "{}/auth/callback?token={}&session_id={}& expires_at={}" ,
152161 frontend_url,
153162 urlencoding:: encode( & token) ,
163+ urlencoding:: encode( & session. session_id. to_string( ) ) ,
154164 urlencoding:: encode( & session. expires_at. to_rfc3339( ) )
155165 ) ;
156166 if is_new_user {
@@ -214,14 +224,15 @@ pub async fn github_login(
214224
215225/// Handler for logout
216226#[ utoipa:: path(
217- get ,
227+ post ,
218228 path = "/v1/auth/logout" ,
219229 tag = "Auth" ,
220- params(
221- ( "session_id" = uuid:: Uuid , Query , description = "Session ID to revoke" )
222- ) ,
230+ request_body = LogoutRequest ,
223231 responses(
224232 ( status = 204 , description = "Successfully logged out" ) ,
233+ ( status = 401 , description = "Unauthorized" , body = crate :: error:: ApiErrorResponse ) ,
234+ ( status = 403 , description = "Forbidden - session does not belong to authenticated user" , body = crate :: error:: ApiErrorResponse ) ,
235+ ( status = 404 , description = "Session not found" , body = crate :: error:: ApiErrorResponse ) ,
225236 ( status = 500 , description = "Internal server error" , body = crate :: error:: ApiErrorResponse )
226237 ) ,
227238 security(
@@ -230,9 +241,41 @@ pub async fn github_login(
230241) ]
231242pub async fn logout (
232243 State ( app_state) : State < AppState > ,
233- Query ( session_id) : Query < SessionId > ,
244+ Extension ( authenticated_user) : Extension < AuthenticatedUser > ,
245+ Json ( request) : Json < LogoutRequest > ,
234246) -> Result < StatusCode , ApiError > {
235- tracing:: info!( "Logout requested for session_id: {}" , session_id) ;
247+ let session_id = request. session_id ;
248+ tracing:: info!(
249+ "Logout requested for session_id: {} by user_id: {}" ,
250+ session_id,
251+ authenticated_user. user_id
252+ ) ;
253+
254+ // Verify that the session belongs to the authenticated user
255+ let session = app_state
256+ . session_repository
257+ . get_session_by_id ( session_id)
258+ . await
259+ . map_err ( |e| {
260+ tracing:: error!( "Failed to get session {}: {}" , session_id, e) ;
261+ ApiError :: logout_failed ( )
262+ } ) ?;
263+
264+ let session = session. ok_or_else ( || {
265+ tracing:: warn!( "Session {} not found" , session_id) ;
266+ ApiError :: session_id_not_found ( )
267+ } ) ?;
268+
269+ // Verify that the session belongs to the authenticated user
270+ if session. user_id != authenticated_user. user_id {
271+ tracing:: warn!(
272+ "User {} attempted to logout session {} which belongs to user {}" ,
273+ authenticated_user. user_id,
274+ session_id,
275+ session. user_id
276+ ) ;
277+ return Err ( ApiError :: forbidden ( "You can only logout your own sessions" ) ) ;
278+ }
236279
237280 app_state
238281 . oauth_service
@@ -243,7 +286,11 @@ pub async fn logout(
243286 ApiError :: logout_failed ( )
244287 } ) ?;
245288
246- tracing:: info!( "Session {} successfully revoked" , session_id) ;
289+ tracing:: info!(
290+ "Session {} successfully revoked by user_id: {}" ,
291+ session_id,
292+ authenticated_user. user_id
293+ ) ;
247294
248295 Ok ( StatusCode :: NO_CONTENT )
249296}
@@ -317,16 +364,14 @@ pub async fn mock_login(
317364 } ) )
318365}
319366
320- /// Create OAuth router with all routes
367+ /// Create OAuth router with all routes (excluding logout, which requires auth)
321368pub fn create_oauth_router ( ) -> Router < AppState > {
322369 let router = Router :: new ( )
323370 // OAuth initiation routes
324371 . route ( "/google" , get ( google_login) )
325372 . route ( "/github" , get ( github_login) )
326373 // Unified callback route for all providers
327- . route ( "/callback" , get ( oauth_callback) )
328- // Logout route
329- . route ( "/logout" , get ( logout) ) ;
374+ . route ( "/callback" , get ( oauth_callback) ) ;
330375
331376 // Add mock login route only in test builds
332377 #[ cfg( feature = "test" ) ]
0 commit comments