Skip to content

Commit 8deafc2

Browse files
committed
improve session cards
1 parent f5908ef commit 8deafc2

File tree

2 files changed

+138
-142
lines changed

2 files changed

+138
-142
lines changed

includes/Services/Auth.php

Lines changed: 121 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -479,60 +479,6 @@ public function revoke_all_refresh_tokens( int $user_id ): bool {
479479
return delete_user_meta( $user_id, '_woocommerce_pos_refresh_tokens' );
480480
}
481481

482-
/**
483-
* Store refresh token JTI for tracking/revocation.
484-
*
485-
* @param int $user_id
486-
* @param string $jti
487-
* @param int $expires
488-
*/
489-
private function store_refresh_token_jti( int $user_id, string $jti, int $expires ): void {
490-
$refresh_tokens = get_user_meta( $user_id, '_woocommerce_pos_refresh_tokens', true );
491-
if ( ! \is_array( $refresh_tokens ) ) {
492-
$refresh_tokens = array();
493-
}
494-
495-
// Clean up expired tokens
496-
$refresh_tokens = array_filter( $refresh_tokens, function( $token ) {
497-
return $token['expires'] > time();
498-
});
499-
500-
// Capture session metadata
501-
$current_time = time();
502-
$ip_address = $this->get_client_ip();
503-
$user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '';
504-
$device_info = $this->parse_user_agent( $user_agent );
505-
506-
// Add new token with metadata
507-
$refresh_tokens[ $jti ] = array(
508-
'expires' => $expires,
509-
'created' => $current_time,
510-
'last_active' => $current_time,
511-
'ip_address' => $ip_address,
512-
'user_agent' => $user_agent,
513-
'device_info' => $device_info,
514-
);
515-
516-
update_user_meta( $user_id, '_woocommerce_pos_refresh_tokens', $refresh_tokens );
517-
}
518-
519-
/**
520-
* Check if refresh token is still valid (not revoked).
521-
*
522-
* @param int $user_id
523-
* @param string $jti
524-
*
525-
* @return bool
526-
*/
527-
private function is_refresh_token_valid( int $user_id, string $jti ): bool {
528-
$refresh_tokens = get_user_meta( $user_id, '_woocommerce_pos_refresh_tokens', true );
529-
if ( ! \is_array( $refresh_tokens ) ) {
530-
return false;
531-
}
532-
533-
return isset( $refresh_tokens[ $jti ] ) && $refresh_tokens[ $jti ]['expires'] > time();
534-
}
535-
536482
/**
537483
* Get all active sessions for a user.
538484
*
@@ -557,11 +503,11 @@ public function get_user_sessions( int $user_id ): array {
557503

558504
$sessions[] = array(
559505
'jti' => $jti,
560-
'created' => $token_data['created'] ?? $current_time,
506+
'created' => $token_data['created'] ?? $current_time,
561507
'last_active' => $token_data['last_active'] ?? $token_data['created'] ?? $current_time,
562508
'expires' => $token_data['expires'],
563-
'ip_address' => $token_data['ip_address'] ?? '',
564-
'user_agent' => $token_data['user_agent'] ?? '',
509+
'ip_address' => $token_data['ip_address'] ?? '',
510+
'user_agent' => $token_data['user_agent'] ?? '',
565511
'device_info' => $token_data['device_info'] ?? array(),
566512
);
567513
}
@@ -655,6 +601,103 @@ public function can_manage_user_sessions( int $target_user_id ): bool {
655601
return false;
656602
}
657603

604+
/**
605+
* Blacklist an access token by JTI (for instant revocation).
606+
*
607+
* @param string $jti Access token JTI.
608+
* @param int $ttl Time to live in seconds (until token expires).
609+
*
610+
* @return bool
611+
*/
612+
public function blacklist_access_token( string $jti, int $ttl ): bool {
613+
if ( empty( $jti ) ) {
614+
return false;
615+
}
616+
617+
// Use transient with TTL matching token expiration
618+
return set_transient( "wcpos_blacklist_{$jti}", true, $ttl );
619+
}
620+
621+
/**
622+
* Revoke session and blacklist current access token.
623+
*
624+
* @param int $user_id
625+
* @param string $refresh_jti Refresh token JTI.
626+
* @param string $access_jti Optional access token JTI to blacklist immediately.
627+
*
628+
* @return bool
629+
*/
630+
public function revoke_session_with_blacklist( int $user_id, string $refresh_jti, string $access_jti = '' ): bool {
631+
// Revoke the refresh token (session)
632+
$revoked = $this->revoke_session( $user_id, $refresh_jti );
633+
634+
// Blacklist the current access token if provided
635+
if ( $revoked && ! empty( $access_jti ) ) {
636+
// Calculate TTL - access tokens expire in 30 minutes by default
637+
$issued_at = time();
638+
$expire = apply_filters( 'woocommerce_pos_jwt_access_token_expire', $issued_at + ( HOUR_IN_SECONDS / 2 ), $issued_at );
639+
$ttl = max( 0, $expire - $issued_at );
640+
641+
$this->blacklist_access_token( $access_jti, $ttl );
642+
}
643+
644+
return $revoked;
645+
}
646+
647+
/**
648+
* Store refresh token JTI for tracking/revocation.
649+
*
650+
* @param int $user_id
651+
* @param string $jti
652+
* @param int $expires
653+
*/
654+
private function store_refresh_token_jti( int $user_id, string $jti, int $expires ): void {
655+
$refresh_tokens = get_user_meta( $user_id, '_woocommerce_pos_refresh_tokens', true );
656+
if ( ! \is_array( $refresh_tokens ) ) {
657+
$refresh_tokens = array();
658+
}
659+
660+
// Clean up expired tokens
661+
$refresh_tokens = array_filter( $refresh_tokens, function( $token ) {
662+
return $token['expires'] > time();
663+
});
664+
665+
// Capture session metadata
666+
$current_time = time();
667+
$ip_address = $this->get_client_ip();
668+
$user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '';
669+
$device_info = $this->parse_user_agent( $user_agent );
670+
671+
// Add new token with metadata
672+
$refresh_tokens[ $jti ] = array(
673+
'expires' => $expires,
674+
'created' => $current_time,
675+
'last_active' => $current_time,
676+
'ip_address' => $ip_address,
677+
'user_agent' => $user_agent,
678+
'device_info' => $device_info,
679+
);
680+
681+
update_user_meta( $user_id, '_woocommerce_pos_refresh_tokens', $refresh_tokens );
682+
}
683+
684+
/**
685+
* Check if refresh token is still valid (not revoked).
686+
*
687+
* @param int $user_id
688+
* @param string $jti
689+
*
690+
* @return bool
691+
*/
692+
private function is_refresh_token_valid( int $user_id, string $jti ): bool {
693+
$refresh_tokens = get_user_meta( $user_id, '_woocommerce_pos_refresh_tokens', true );
694+
if ( ! \is_array( $refresh_tokens ) ) {
695+
return false;
696+
}
697+
698+
return isset( $refresh_tokens[ $jti ] ) && $refresh_tokens[ $jti ]['expires'] > time();
699+
}
700+
658701
/**
659702
* Get client IP address.
660703
*
@@ -675,10 +718,11 @@ private function get_client_ip(): string {
675718
if ( ! empty( $_SERVER[ $header ] ) ) {
676719
$ip_address = sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) );
677720
// Handle comma-separated IPs (X-Forwarded-For can contain multiple IPs)
678-
if ( strpos( $ip_address, ',' ) !== false ) {
721+
if ( false !== strpos( $ip_address, ',' ) ) {
679722
$ip_parts = explode( ',', $ip_address );
680723
$ip_address = trim( $ip_parts[0] );
681724
}
725+
682726
break;
683727
}
684728
}
@@ -712,28 +756,35 @@ private function parse_user_agent( string $user_agent ): array {
712756
}
713757

714758
// Detect WooCommerce POS apps first (custom identifiers)
715-
// Your apps should add identifiers like: "WCPOS-iOS/1.0.0" or "WooCommercePOS-iOS/1.0.0"
716-
if ( preg_match( '/WCPOS[-_]?iOS|WooCommercePOS[-_]?iOS/i', $user_agent ) ) {
759+
// Check for Electron app (including just "WooCommercePOS" in user agent with Electron)
760+
if ( preg_match( '/Electron/i', $user_agent ) && preg_match( '/WooCommercePOS|WCPOS/i', $user_agent ) ) {
761+
$device_info['app_type'] = 'electron_app';
762+
$device_info['browser'] = 'WooCommerce POS';
763+
$device_info['device_type'] = 'desktop';
764+
// Try to extract WooCommercePOS version
765+
if ( preg_match( '/WooCommercePOS[\/\s]([0-9.]+)/i', $user_agent, $matches ) ) {
766+
$device_info['browser_version'] = $matches[1];
767+
} elseif ( preg_match( '/WCPOS[\/\s]([0-9.]+)/i', $user_agent, $matches ) ) {
768+
$device_info['browser_version'] = $matches[1];
769+
}
770+
} elseif ( preg_match( '/WCPOS[-_]?iOS|WooCommercePOS[-_]?iOS/i', $user_agent ) ) {
717771
$device_info['app_type'] = 'ios_app';
718772
$device_info['browser'] = 'WooCommerce POS';
719-
$device_info['device_type'] = preg_match( '/ipad/i', $user_agent ) ? 'tablet' : 'mobile';
773+
// Default to tablet unless explicitly detected as phone
774+
$device_info['device_type'] = preg_match( '/iphone|ipod/i', $user_agent ) ? 'mobile' : 'tablet';
720775
if ( preg_match( '/WCPOS[-_]?iOS[\/\s]([0-9.]+)/i', $user_agent, $matches ) ) {
721776
$device_info['browser_version'] = $matches[1];
777+
} elseif ( preg_match( '/WooCommercePOS[\/\s]([0-9.]+)/i', $user_agent, $matches ) ) {
778+
$device_info['browser_version'] = $matches[1];
722779
}
723780
} elseif ( preg_match( '/WCPOS[-_]?Android|WooCommercePOS[-_]?Android/i', $user_agent ) ) {
724781
$device_info['app_type'] = 'android_app';
725782
$device_info['browser'] = 'WooCommerce POS';
726-
$device_info['device_type'] = preg_match( '/tablet/i', $user_agent ) ? 'tablet' : 'mobile';
783+
// Default to tablet unless explicitly detected as mobile
784+
$device_info['device_type'] = preg_match( '/mobile/i', $user_agent ) && ! preg_match( '/tablet/i', $user_agent ) ? 'mobile' : 'tablet';
727785
if ( preg_match( '/WCPOS[-_]?Android[\/\s]([0-9.]+)/i', $user_agent, $matches ) ) {
728786
$device_info['browser_version'] = $matches[1];
729-
}
730-
} elseif ( preg_match( '/WCPOS[-_]?Electron|WooCommercePOS[-_]?Electron|Electron.*WCPOS/i', $user_agent ) ) {
731-
$device_info['app_type'] = 'electron_app';
732-
$device_info['browser'] = 'WooCommerce POS';
733-
$device_info['device_type'] = 'desktop';
734-
if ( preg_match( '/WCPOS[-_]?Electron[\/\s]([0-9.]+)/i', $user_agent, $matches ) ) {
735-
$device_info['browser_version'] = $matches[1];
736-
} elseif ( preg_match( '/Electron\/([0-9.]+)/i', $user_agent, $matches ) ) {
787+
} elseif ( preg_match( '/WooCommercePOS[\/\s]([0-9.]+)/i', $user_agent, $matches ) ) {
737788
$device_info['browser_version'] = $matches[1];
738789
}
739790
}
@@ -798,23 +849,6 @@ private function parse_user_agent( string $user_agent ): array {
798849
return $device_info;
799850
}
800851

801-
/**
802-
* Blacklist an access token by JTI (for instant revocation).
803-
*
804-
* @param string $jti Access token JTI.
805-
* @param int $ttl Time to live in seconds (until token expires).
806-
*
807-
* @return bool
808-
*/
809-
public function blacklist_access_token( string $jti, int $ttl ): bool {
810-
if ( empty( $jti ) ) {
811-
return false;
812-
}
813-
814-
// Use transient with TTL matching token expiration
815-
return set_transient( "wcpos_blacklist_{$jti}", true, $ttl );
816-
}
817-
818852
/**
819853
* Check if an access token is blacklisted.
820854
*
@@ -830,30 +864,4 @@ private function is_access_token_blacklisted( string $jti ): bool {
830864
// Check transient
831865
return false !== get_transient( "wcpos_blacklist_{$jti}" );
832866
}
833-
834-
/**
835-
* Revoke session and blacklist current access token.
836-
*
837-
* @param int $user_id
838-
* @param string $refresh_jti Refresh token JTI.
839-
* @param string $access_jti Optional access token JTI to blacklist immediately.
840-
*
841-
* @return bool
842-
*/
843-
public function revoke_session_with_blacklist( int $user_id, string $refresh_jti, string $access_jti = '' ): bool {
844-
// Revoke the refresh token (session)
845-
$revoked = $this->revoke_session( $user_id, $refresh_jti );
846-
847-
// Blacklist the current access token if provided
848-
if ( $revoked && ! empty( $access_jti ) ) {
849-
// Calculate TTL - access tokens expire in 30 minutes by default
850-
$issued_at = time();
851-
$expire = apply_filters( 'woocommerce_pos_jwt_access_token_expire', $issued_at + ( HOUR_IN_SECONDS / 2 ), $issued_at );
852-
$ttl = max( 0, $expire - $issued_at );
853-
854-
$this->blacklist_access_token( $access_jti, $ttl );
855-
}
856-
857-
return $revoked;
858-
}
859867
}

packages/settings/src/screens/sessions/session-card.tsx

Lines changed: 17 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -45,40 +45,42 @@ const SessionCard: React.FC<SessionCardProps> = ({ session, onDelete, isDeleting
4545
};
4646

4747
const getDeviceIcon = (deviceInfo: Session['device_info']) => {
48-
// Check app type first for native apps
48+
// Check app type first
4949
switch (deviceInfo.app_type) {
5050
case 'ios_app':
51-
return '📱'; // iOS app icon
51+
// Default to tablet for iOS unless explicitly iPhone
52+
return deviceInfo.device_type === 'mobile' ? '📱' : '📲';
5253
case 'android_app':
53-
return '🤖'; // Android app icon
54+
// Default to tablet for Android unless explicitly mobile
55+
return deviceInfo.device_type === 'mobile' ? '📱' : '📲';
5456
case 'electron_app':
55-
return '🖥️'; // Desktop app icon
57+
return '💻'; // Desktop app icon
5658
case 'web':
5759
default:
58-
// Fall back to device type for web browsers
60+
// Web browser - use globe for desktop, phone/tablet for mobile
5961
switch (deviceInfo.device_type) {
6062
case 'mobile':
6163
return '📱';
6264
case 'tablet':
6365
return '📲';
6466
case 'desktop':
6567
default:
66-
return '💻';
68+
return '🌐'; // Globe icon for web
6769
}
6870
}
6971
};
7072

7173
const getAppTypeLabel = (appType?: string) => {
7274
switch (appType) {
7375
case 'ios_app':
74-
return t('iOS App', { _tags: 'wp-admin-settings' });
76+
return t('iOS Application', { _tags: 'wp-admin-settings' });
7577
case 'android_app':
76-
return t('Android App', { _tags: 'wp-admin-settings' });
78+
return t('Android Application', { _tags: 'wp-admin-settings' });
7779
case 'electron_app':
78-
return t('Desktop App', { _tags: 'wp-admin-settings' });
80+
return t('Desktop Application', { _tags: 'wp-admin-settings' });
7981
case 'web':
8082
default:
81-
return t('Web', { _tags: 'wp-admin-settings' });
83+
return t('Web Application', { _tags: 'wp-admin-settings' });
8284
}
8385
};
8486

@@ -129,16 +131,17 @@ const SessionCard: React.FC<SessionCardProps> = ({ session, onDelete, isDeleting
129131
{/* Title and button */}
130132
<div className="wcpos:flex wcpos:items-start wcpos:justify-between wcpos:gap-2">
131133
<div className="wcpos:flex-1">
132-
<h3 className="wcpos:font-medium wcpos:text-sm wcpos:text-gray-900 wcpos:leading-tight">
133-
{session.device_info.browser}{' '}
134+
<h3 className="wcpos:font-semibold wcpos:text-sm wcpos:text-gray-900 wcpos:leading-tight">
135+
{getAppTypeLabel(session.device_info.app_type)}
134136
{session.device_info.browser_version && (
135137
<span className="wcpos:text-gray-500 wcpos:font-normal wcpos:text-xs">
138+
{' '}
136139
{session.device_info.browser_version}
137140
</span>
138141
)}
139142
</h3>
140143
<p className="wcpos:text-xs wcpos:text-gray-600 wcpos:mt-0.5">
141-
{session.device_info.os}
144+
{session.device_info.browser}{session.device_info.os}
142145
</p>
143146
</div>
144147

@@ -157,23 +160,8 @@ const SessionCard: React.FC<SessionCardProps> = ({ session, onDelete, isDeleting
157160
)}
158161
</div>
159162

160-
{/* Platform badge and last active */}
163+
{/* Last active */}
161164
<div className="wcpos:flex wcpos:items-center wcpos:gap-2 wcpos:mt-2 wcpos:mb-2">
162-
<span
163-
className={classNames(
164-
'wcpos:inline-flex wcpos:items-center wcpos:px-1.5 wcpos:py-0.5 wcpos:rounded wcpos:text-xs wcpos:font-medium',
165-
session.device_info.app_type === 'web'
166-
? 'wcpos:bg-blue-100 wcpos:text-blue-800'
167-
: session.device_info.app_type === 'ios_app'
168-
? 'wcpos:bg-purple-100 wcpos:text-purple-800'
169-
: session.device_info.app_type === 'android_app'
170-
? 'wcpos:bg-green-100 wcpos:text-green-800'
171-
: 'wcpos:bg-gray-100 wcpos:text-gray-800'
172-
)}
173-
>
174-
{getAppTypeLabel(session.device_info.app_type)}
175-
</span>
176-
<span className="wcpos:text-xs wcpos:text-gray-400"></span>
177165
<span className="wcpos:text-xs wcpos:text-gray-600">
178166
{getTimeAgo(session.last_active)}
179167
</span>

0 commit comments

Comments
 (0)