@@ -67,6 +67,95 @@ public function register_routes(): void {
6767 ),
6868 )
6969 );
70+
71+ // Get user sessions
72+ register_rest_route (
73+ $ this ->namespace ,
74+ '/ ' . $ this ->rest_base . '/sessions ' ,
75+ array (
76+ 'methods ' => WP_REST_Server::READABLE ,
77+ 'callback ' => array ( $ this , 'get_sessions ' ),
78+ 'permission_callback ' => array ( $ this , 'check_session_permissions ' ),
79+ 'args ' => array (
80+ 'user_id ' => array (
81+ 'description ' => __ ( 'The user ID to get sessions for. Defaults to current user. ' , 'woocommerce-pos ' ),
82+ 'type ' => 'integer ' ,
83+ 'required ' => false ,
84+ 'validate_callback ' => function ( $ param ) {
85+ return is_numeric ( $ param );
86+ },
87+ ),
88+ ),
89+ )
90+ );
91+
92+ // Delete all sessions or all except current
93+ register_rest_route (
94+ $ this ->namespace ,
95+ '/ ' . $ this ->rest_base . '/sessions ' ,
96+ array (
97+ 'methods ' => WP_REST_Server::DELETABLE ,
98+ 'callback ' => array ( $ this , 'delete_all_sessions ' ),
99+ 'permission_callback ' => array ( $ this , 'check_session_permissions ' ),
100+ 'args ' => array (
101+ 'user_id ' => array (
102+ 'description ' => __ ( 'The user ID to delete sessions for. ' , 'woocommerce-pos ' ),
103+ 'type ' => 'integer ' ,
104+ 'required ' => true ,
105+ 'validate_callback ' => function ( $ param ) {
106+ return is_numeric ( $ param );
107+ },
108+ ),
109+ 'except_current ' => array (
110+ 'description ' => __ ( 'Whether to keep the current session. ' , 'woocommerce-pos ' ),
111+ 'type ' => 'boolean ' ,
112+ 'required ' => false ,
113+ 'default ' => false ,
114+ ),
115+ ),
116+ )
117+ );
118+
119+ // Delete specific session by JTI
120+ register_rest_route (
121+ $ this ->namespace ,
122+ '/ ' . $ this ->rest_base . '/sessions/(?P<jti>[a-f0-9\-]+) ' ,
123+ array (
124+ 'methods ' => WP_REST_Server::DELETABLE ,
125+ 'callback ' => array ( $ this , 'delete_session ' ),
126+ 'permission_callback ' => array ( $ this , 'check_session_permissions ' ),
127+ 'args ' => array (
128+ 'jti ' => array (
129+ 'description ' => __ ( 'The session JTI to delete. ' , 'woocommerce-pos ' ),
130+ 'type ' => 'string ' ,
131+ 'required ' => true ,
132+ 'validate_callback ' => function ( $ param ) {
133+ // Validate UUID format
134+ return preg_match ( '/^[a-f0-9\-]{36}$/i ' , $ param );
135+ },
136+ ),
137+ 'user_id ' => array (
138+ 'description ' => __ ( 'The user ID that owns the session. ' , 'woocommerce-pos ' ),
139+ 'type ' => 'integer ' ,
140+ 'required ' => true ,
141+ 'validate_callback ' => function ( $ param ) {
142+ return is_numeric ( $ param );
143+ },
144+ ),
145+ ),
146+ )
147+ );
148+
149+ // Get all users with active sessions (admin/manager only)
150+ register_rest_route (
151+ $ this ->namespace ,
152+ '/ ' . $ this ->rest_base . '/users/sessions ' ,
153+ array (
154+ 'methods ' => WP_REST_Server::READABLE ,
155+ 'callback ' => array ( $ this , 'get_all_users_sessions ' ),
156+ 'permission_callback ' => array ( $ this , 'check_admin_permissions ' ),
157+ )
158+ );
70159 }
71160
72161
@@ -183,4 +272,260 @@ public function refresh_token( WP_REST_Request $request ): WP_REST_Response {
183272
184273 return rest_ensure_response ( $ response_data );
185274 }
275+
276+ /**
277+ * Get sessions for a user.
278+ *
279+ * @param WP_REST_Request $request The REST request object.
280+ *
281+ * @return WP_REST_Response
282+ */
283+ public function get_sessions ( WP_REST_Request $ request ): WP_REST_Response {
284+ $ user_id = $ request ->get_param ( 'user_id ' );
285+
286+ // Default to current user if not specified
287+ if ( empty ( $ user_id ) ) {
288+ $ user_id = get_current_user_id ();
289+ }
290+
291+ $ auth_service = AuthService::instance ();
292+ $ sessions = $ auth_service ->get_user_sessions ( (int ) $ user_id );
293+
294+ // Get current JTI if available from the request token
295+ $ current_jti = $ this ->get_current_jti_from_request ( $ request );
296+
297+ // Mark the current session
298+ foreach ( $ sessions as &$ session ) {
299+ $ session ['is_current ' ] = ( ! empty ( $ current_jti ) && $ session ['jti ' ] === $ current_jti );
300+ }
301+
302+ return rest_ensure_response ( array (
303+ 'user_id ' => $ user_id ,
304+ 'sessions ' => $ sessions ,
305+ ) );
306+ }
307+
308+ /**
309+ * Delete a specific session.
310+ *
311+ * @param WP_REST_Request $request The REST request object.
312+ *
313+ * @return WP_REST_Response
314+ */
315+ public function delete_session ( WP_REST_Request $ request ): WP_REST_Response {
316+ $ jti = $ request ->get_param ( 'jti ' );
317+ $ user_id = $ request ->get_param ( 'user_id ' );
318+
319+ if ( empty ( $ jti ) || empty ( $ user_id ) ) {
320+ return rest_ensure_response ( array (
321+ 'success ' => false ,
322+ 'message ' => __ ( 'Missing required parameters. ' , 'woocommerce-pos ' ),
323+ ), 400 );
324+ }
325+
326+ $ auth_service = AuthService::instance ();
327+ $ result = $ auth_service ->revoke_session ( (int ) $ user_id , $ jti );
328+
329+ if ( $ result ) {
330+ return rest_ensure_response ( array (
331+ 'success ' => true ,
332+ 'message ' => __ ( 'Session revoked successfully. ' , 'woocommerce-pos ' ),
333+ ) );
334+ }
335+
336+ return rest_ensure_response ( array (
337+ 'success ' => false ,
338+ 'message ' => __ ( 'Failed to revoke session. ' , 'woocommerce-pos ' ),
339+ ), 404 );
340+ }
341+
342+ /**
343+ * Delete all sessions for a user.
344+ *
345+ * @param WP_REST_Request $request The REST request object.
346+ *
347+ * @return WP_REST_Response
348+ */
349+ public function delete_all_sessions ( WP_REST_Request $ request ): WP_REST_Response {
350+ $ user_id = $ request ->get_param ( 'user_id ' );
351+ $ except_current = $ request ->get_param ( 'except_current ' );
352+
353+ if ( empty ( $ user_id ) ) {
354+ return rest_ensure_response ( array (
355+ 'success ' => false ,
356+ 'message ' => __ ( 'Missing user_id parameter. ' , 'woocommerce-pos ' ),
357+ ), 400 );
358+ }
359+
360+ $ auth_service = AuthService::instance ();
361+
362+ if ( $ except_current ) {
363+ // Get current JTI from request
364+ $ current_jti = $ this ->get_current_jti_from_request ( $ request );
365+
366+ if ( empty ( $ current_jti ) ) {
367+ return rest_ensure_response ( array (
368+ 'success ' => false ,
369+ 'message ' => __ ( 'Could not determine current session. ' , 'woocommerce-pos ' ),
370+ ), 400 );
371+ }
372+
373+ $ result = $ auth_service ->revoke_all_sessions_except ( (int ) $ user_id , $ current_jti );
374+ } else {
375+ $ result = $ auth_service ->revoke_all_refresh_tokens ( (int ) $ user_id );
376+ }
377+
378+ if ( $ result ) {
379+ return rest_ensure_response ( array (
380+ 'success ' => true ,
381+ 'message ' => __ ( 'Sessions revoked successfully. ' , 'woocommerce-pos ' ),
382+ ) );
383+ }
384+
385+ return rest_ensure_response ( array (
386+ 'success ' => false ,
387+ 'message ' => __ ( 'Failed to revoke sessions. ' , 'woocommerce-pos ' ),
388+ ), 500 );
389+ }
390+
391+ /**
392+ * Get all users with active sessions (admin/manager only).
393+ *
394+ * @param WP_REST_Request $request The REST request object.
395+ *
396+ * @return WP_REST_Response
397+ */
398+ public function get_all_users_sessions ( WP_REST_Request $ request ): WP_REST_Response {
399+ global $ wpdb ;
400+
401+ $ auth_service = AuthService::instance ();
402+
403+ // Get all users who have refresh tokens
404+ $ user_ids = $ wpdb ->get_col (
405+ "SELECT DISTINCT user_id
406+ FROM {$ wpdb ->usermeta }
407+ WHERE meta_key = '_woocommerce_pos_refresh_tokens' "
408+ );
409+
410+ $ users_data = array ();
411+
412+ foreach ( $ user_ids as $ user_id ) {
413+ $ user = get_user_by ( 'id ' , $ user_id );
414+ if ( ! $ user ) {
415+ continue ;
416+ }
417+
418+ $ sessions = $ auth_service ->get_user_sessions ( (int ) $ user_id );
419+
420+ // Only include users with active sessions
421+ if ( empty ( $ sessions ) ) {
422+ continue ;
423+ }
424+
425+ // Find the most recent activity
426+ $ last_active = 0 ;
427+ foreach ( $ sessions as $ session ) {
428+ if ( $ session ['last_active ' ] > $ last_active ) {
429+ $ last_active = $ session ['last_active ' ];
430+ }
431+ }
432+
433+ $ users_data [] = array (
434+ 'user_id ' => (int ) $ user_id ,
435+ 'username ' => $ user ->user_login ,
436+ 'display_name ' => $ user ->display_name ,
437+ 'avatar_url ' => get_avatar_url ( $ user_id , array ( 'size ' => 96 ) ),
438+ 'session_count ' => count ( $ sessions ),
439+ 'last_active ' => $ last_active ,
440+ 'sessions ' => $ sessions ,
441+ );
442+ }
443+
444+ // Sort by last_active descending (most recent first)
445+ usort ( $ users_data , function ( $ a , $ b ) {
446+ return $ b ['last_active ' ] - $ a ['last_active ' ];
447+ });
448+
449+ return rest_ensure_response ( array (
450+ 'users ' => $ users_data ,
451+ 'total ' => count ( $ users_data ),
452+ ) );
453+ }
454+
455+ /**
456+ * Check session management permissions.
457+ *
458+ * @param WP_REST_Request $request The REST request object.
459+ *
460+ * @return bool
461+ */
462+ public function check_session_permissions ( WP_REST_Request $ request ): bool {
463+ // User must be logged in
464+ if ( ! is_user_logged_in () ) {
465+ return false ;
466+ }
467+
468+ $ target_user_id = $ request ->get_param ( 'user_id ' );
469+
470+ // Default to current user if not specified (for GET requests)
471+ if ( empty ( $ target_user_id ) ) {
472+ $ target_user_id = get_current_user_id ();
473+ }
474+
475+ $ auth_service = AuthService::instance ();
476+
477+ return $ auth_service ->can_manage_user_sessions ( (int ) $ target_user_id );
478+ }
479+
480+ /**
481+ * Check admin/manager permissions.
482+ *
483+ * @param WP_REST_Request $request The REST request object.
484+ *
485+ * @return bool
486+ */
487+ public function check_admin_permissions ( WP_REST_Request $ request ): bool {
488+ // Only administrators and shop managers
489+ return current_user_can ( 'manage_options ' ) || current_user_can ( 'manage_woocommerce ' );
490+ }
491+
492+ /**
493+ * Get current JTI from the request's authorization token.
494+ *
495+ * @param WP_REST_Request $request The REST request object.
496+ *
497+ * @return string|null
498+ */
499+ private function get_current_jti_from_request ( WP_REST_Request $ request ): ?string {
500+ // Try to get the token from Authorization header
501+ $ auth_header = $ request ->get_header ( 'authorization ' );
502+
503+ if ( empty ( $ auth_header ) ) {
504+ // Try query parameter
505+ $ auth_header = $ request ->get_param ( 'authorization ' );
506+ }
507+
508+ if ( empty ( $ auth_header ) ) {
509+ return null ;
510+ }
511+
512+ // Extract token from "Bearer TOKEN"
513+ $ token = str_replace ( 'Bearer ' , '' , $ auth_header );
514+
515+ if ( empty ( $ token ) ) {
516+ return null ;
517+ }
518+
519+ // Try to decode the token to get JTI
520+ $ auth_service = AuthService::instance ();
521+ $ decoded = $ auth_service ->validate_token ( $ token , 'refresh ' );
522+
523+ if ( is_wp_error ( $ decoded ) ) {
524+ // If it's not a refresh token, it might be an access token
525+ // Access tokens don't have JTI, so return null
526+ return null ;
527+ }
528+
529+ return $ decoded ->jti ?? null ;
530+ }
186531}
0 commit comments