Skip to content

Commit f89abe8

Browse files
committed
Add Sessions tab to POS settings
1 parent ec17a89 commit f89abe8

File tree

14 files changed

+2210
-19
lines changed

14 files changed

+2210
-19
lines changed

includes/API/Auth.php

Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)