chore: add spec-kit and standardize signatures
- Added GitHub spec-kit for development workflow - Standardized file signatures to Descomplicar® format - Updated development configuration 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
905
src/includes/services/class-session-service.php
Normal file
905
src/includes/services/class-session-service.php
Normal file
@@ -0,0 +1,905 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Session Service
|
||||
*
|
||||
* Handles user session management, security and monitoring
|
||||
*
|
||||
* @package KiviCare_API
|
||||
* @subpackage Services
|
||||
* @version 1.0.0
|
||||
* @author Descomplicar® <dev@descomplicar.pt>
|
||||
* @link https://descomplicar.pt
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
namespace KiviCare_API\Services;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class Session_Service
|
||||
*
|
||||
* Session management and security monitoring for KiviCare API
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Session_Service {
|
||||
|
||||
/**
|
||||
* Maximum concurrent sessions per user
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private static $max_concurrent_sessions = 3;
|
||||
|
||||
/**
|
||||
* Session timeout (in seconds) - 30 minutes
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private static $session_timeout = 1800;
|
||||
|
||||
/**
|
||||
* Maximum failed login attempts
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private static $max_failed_attempts = 5;
|
||||
|
||||
/**
|
||||
* Lockout duration (in seconds) - 15 minutes
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private static $lockout_duration = 900;
|
||||
|
||||
/**
|
||||
* Initialize the session service
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function init() {
|
||||
// Hook into authentication events
|
||||
add_action( 'kivicare_user_authenticated', array( self::class, 'on_user_authenticated' ), 10, 2 );
|
||||
add_action( 'kivicare_user_logout', array( self::class, 'on_user_logout' ), 10, 1 );
|
||||
add_action( 'kivicare_failed_login', array( self::class, 'on_failed_login' ), 10, 1 );
|
||||
|
||||
// Cleanup expired sessions
|
||||
add_action( 'kivicare_cleanup_sessions', array( self::class, 'cleanup_expired_sessions' ) );
|
||||
|
||||
// Schedule cleanup if not already scheduled
|
||||
if ( ! wp_next_scheduled( 'kivicare_cleanup_sessions' ) ) {
|
||||
wp_schedule_event( time(), 'hourly', 'kivicare_cleanup_sessions' );
|
||||
}
|
||||
|
||||
// Monitor session activity
|
||||
add_action( 'init', array( self::class, 'monitor_session_activity' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new session for user
|
||||
*
|
||||
* @param int $user_id User ID
|
||||
* @param string $ip_address IP address
|
||||
* @param string $user_agent User agent
|
||||
* @return string Session ID
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function create_session( $user_id, $ip_address, $user_agent ) {
|
||||
// Generate unique session ID
|
||||
$session_id = wp_generate_uuid4();
|
||||
|
||||
// Check concurrent session limit
|
||||
self::enforce_concurrent_session_limit( $user_id );
|
||||
|
||||
// Create session data
|
||||
$session_data = array(
|
||||
'session_id' => $session_id,
|
||||
'user_id' => $user_id,
|
||||
'ip_address' => $ip_address,
|
||||
'user_agent' => $user_agent,
|
||||
'created_at' => current_time( 'mysql' ),
|
||||
'last_activity' => current_time( 'mysql' ),
|
||||
'expires_at' => date( 'Y-m-d H:i:s', time() + self::$session_timeout ),
|
||||
'is_active' => 1
|
||||
);
|
||||
|
||||
// Store session in database
|
||||
self::store_session( $session_data );
|
||||
|
||||
// Update user session metadata
|
||||
self::update_user_session_meta( $user_id, $session_id, $ip_address );
|
||||
|
||||
return $session_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate session
|
||||
*
|
||||
* @param string $session_id Session ID
|
||||
* @param int $user_id User ID
|
||||
* @return bool|array Session data or false if invalid
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function validate_session( $session_id, $user_id ) {
|
||||
$session = self::get_session( $session_id );
|
||||
|
||||
if ( ! $session ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if session belongs to user
|
||||
if ( (int) $session['user_id'] !== $user_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if session is active
|
||||
if ( ! $session['is_active'] ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if session is expired
|
||||
if ( strtotime( $session['expires_at'] ) < time() ) {
|
||||
self::expire_session( $session_id );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for session timeout based on last activity
|
||||
$last_activity = strtotime( $session['last_activity'] );
|
||||
if ( ( time() - $last_activity ) > self::$session_timeout ) {
|
||||
self::expire_session( $session_id );
|
||||
return false;
|
||||
}
|
||||
|
||||
return $session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update session activity
|
||||
*
|
||||
* @param string $session_id Session ID
|
||||
* @return bool Success status
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function update_session_activity( $session_id ) {
|
||||
global $wpdb;
|
||||
|
||||
$result = $wpdb->update(
|
||||
$wpdb->prefix . 'kivicare_sessions',
|
||||
array(
|
||||
'last_activity' => current_time( 'mysql' ),
|
||||
'expires_at' => date( 'Y-m-d H:i:s', time() + self::$session_timeout )
|
||||
),
|
||||
array( 'session_id' => $session_id ),
|
||||
array( '%s', '%s' ),
|
||||
array( '%s' )
|
||||
);
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expire session
|
||||
*
|
||||
* @param string $session_id Session ID
|
||||
* @return bool Success status
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function expire_session( $session_id ) {
|
||||
global $wpdb;
|
||||
|
||||
$result = $wpdb->update(
|
||||
$wpdb->prefix . 'kivicare_sessions',
|
||||
array(
|
||||
'is_active' => 0,
|
||||
'ended_at' => current_time( 'mysql' )
|
||||
),
|
||||
array( 'session_id' => $session_id ),
|
||||
array( '%d', '%s' ),
|
||||
array( '%s' )
|
||||
);
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expire all sessions for user
|
||||
*
|
||||
* @param int $user_id User ID
|
||||
* @return bool Success status
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function expire_user_sessions( $user_id ) {
|
||||
global $wpdb;
|
||||
|
||||
$result = $wpdb->update(
|
||||
$wpdb->prefix . 'kivicare_sessions',
|
||||
array(
|
||||
'is_active' => 0,
|
||||
'ended_at' => current_time( 'mysql' )
|
||||
),
|
||||
array(
|
||||
'user_id' => $user_id,
|
||||
'is_active' => 1
|
||||
),
|
||||
array( '%d', '%s' ),
|
||||
array( '%d', '%d' )
|
||||
);
|
||||
|
||||
// Clear user session metadata
|
||||
delete_user_meta( $user_id, 'kivicare_current_session' );
|
||||
delete_user_meta( $user_id, 'kivicare_session_ip' );
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active sessions for user
|
||||
*
|
||||
* @param int $user_id User ID
|
||||
* @return array Array of active sessions
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_user_sessions( $user_id ) {
|
||||
global $wpdb;
|
||||
|
||||
$sessions = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$wpdb->prefix}kivicare_sessions
|
||||
WHERE user_id = %d AND is_active = 1 AND expires_at > NOW()
|
||||
ORDER BY last_activity DESC",
|
||||
$user_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
return array_map( array( self::class, 'format_session_data' ), $sessions );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user account is locked
|
||||
*
|
||||
* @param int|string $user_identifier User ID or username
|
||||
* @return bool True if account is locked
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function is_account_locked( $user_identifier ) {
|
||||
if ( is_numeric( $user_identifier ) ) {
|
||||
$user = get_user_by( 'id', $user_identifier );
|
||||
} else {
|
||||
$user = get_user_by( 'login', $user_identifier );
|
||||
if ( ! $user && is_email( $user_identifier ) ) {
|
||||
$user = get_user_by( 'email', $user_identifier );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $user ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$lockout_time = get_user_meta( $user->ID, 'kivicare_lockout_time', true );
|
||||
|
||||
if ( ! $lockout_time ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if lockout has expired
|
||||
if ( time() > $lockout_time ) {
|
||||
delete_user_meta( $user->ID, 'kivicare_lockout_time' );
|
||||
delete_user_meta( $user->ID, 'kivicare_failed_attempts' );
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get remaining lockout time
|
||||
*
|
||||
* @param int $user_id User ID
|
||||
* @return int Remaining lockout time in seconds
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_lockout_remaining_time( $user_id ) {
|
||||
$lockout_time = get_user_meta( $user_id, 'kivicare_lockout_time', true );
|
||||
|
||||
if ( ! $lockout_time ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$remaining = $lockout_time - time();
|
||||
return max( 0, $remaining );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session statistics for user
|
||||
*
|
||||
* @param int $user_id User ID
|
||||
* @return array Session statistics
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_user_session_stats( $user_id ) {
|
||||
global $wpdb;
|
||||
|
||||
$stats = array(
|
||||
'active_sessions' => 0,
|
||||
'total_sessions_today' => 0,
|
||||
'total_sessions_this_month' => 0,
|
||||
'last_login' => null,
|
||||
'last_ip' => null,
|
||||
'failed_attempts_today' => 0,
|
||||
'is_locked' => false,
|
||||
'lockout_remaining' => 0
|
||||
);
|
||||
|
||||
// Active sessions
|
||||
$stats['active_sessions'] = (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->prefix}kivicare_sessions
|
||||
WHERE user_id = %d AND is_active = 1 AND expires_at > NOW()",
|
||||
$user_id
|
||||
)
|
||||
);
|
||||
|
||||
// Sessions today
|
||||
$stats['total_sessions_today'] = (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->prefix}kivicare_sessions
|
||||
WHERE user_id = %d AND DATE(created_at) = CURDATE()",
|
||||
$user_id
|
||||
)
|
||||
);
|
||||
|
||||
// Sessions this month
|
||||
$stats['total_sessions_this_month'] = (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->prefix}kivicare_sessions
|
||||
WHERE user_id = %d AND MONTH(created_at) = MONTH(CURDATE()) AND YEAR(created_at) = YEAR(CURDATE())",
|
||||
$user_id
|
||||
)
|
||||
);
|
||||
|
||||
// Last login info
|
||||
$last_session = $wpdb->get_row(
|
||||
$wpdb->prepare(
|
||||
"SELECT created_at, ip_address FROM {$wpdb->prefix}kivicare_sessions
|
||||
WHERE user_id = %d ORDER BY created_at DESC LIMIT 1",
|
||||
$user_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
if ( $last_session ) {
|
||||
$stats['last_login'] = $last_session['created_at'];
|
||||
$stats['last_ip'] = $last_session['ip_address'];
|
||||
}
|
||||
|
||||
// Failed attempts today
|
||||
$stats['failed_attempts_today'] = (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->prefix}kivicare_failed_logins
|
||||
WHERE user_id = %d AND DATE(attempted_at) = CURDATE()",
|
||||
$user_id
|
||||
)
|
||||
);
|
||||
|
||||
// Lockout status
|
||||
$stats['is_locked'] = self::is_account_locked( $user_id );
|
||||
$stats['lockout_remaining'] = self::get_lockout_remaining_time( $user_id );
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitor session activity for security
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function monitor_session_activity() {
|
||||
// Only monitor for authenticated API requests
|
||||
if ( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
if ( ! $user_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current session from auth header or meta
|
||||
$session_id = self::get_current_session_id( $user_id );
|
||||
|
||||
if ( $session_id ) {
|
||||
// Update session activity
|
||||
self::update_session_activity( $session_id );
|
||||
|
||||
// Check for suspicious activity
|
||||
self::detect_suspicious_activity( $user_id, $session_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current session ID for user
|
||||
*
|
||||
* @param int $user_id User ID
|
||||
* @return string|null Session ID or null
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function get_current_session_id( $user_id ) {
|
||||
// Try to get from JWT token first (would need to be added to JWT payload)
|
||||
// For now, get from user meta (set during authentication)
|
||||
return get_user_meta( $user_id, 'kivicare_current_session', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect suspicious session activity
|
||||
*
|
||||
* @param int $user_id User ID
|
||||
* @param string $session_id Session ID
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function detect_suspicious_activity( $user_id, $session_id ) {
|
||||
$session = self::get_session( $session_id );
|
||||
if ( ! $session ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$current_ip = self::get_client_ip();
|
||||
$session_ip = $session['ip_address'];
|
||||
|
||||
// Check for IP address change
|
||||
if ( $current_ip !== $session_ip ) {
|
||||
self::log_security_event( $user_id, 'ip_change', array(
|
||||
'session_id' => $session_id,
|
||||
'original_ip' => $session_ip,
|
||||
'new_ip' => $current_ip
|
||||
) );
|
||||
|
||||
// Optionally expire session or require re-authentication
|
||||
if ( apply_filters( 'kivicare_expire_on_ip_change', false ) ) {
|
||||
self::expire_session( $session_id );
|
||||
}
|
||||
}
|
||||
|
||||
// Check for unusual activity patterns
|
||||
self::check_activity_patterns( $user_id, $session_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for unusual activity patterns
|
||||
*
|
||||
* @param int $user_id User ID
|
||||
* @param string $session_id Session ID
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function check_activity_patterns( $user_id, $session_id ) {
|
||||
// This could be extended to check:
|
||||
// - Rapid API calls (possible bot activity)
|
||||
// - Access to unusual resources
|
||||
// - Concurrent sessions from different locations
|
||||
// - Time-based anomalies (access at unusual hours)
|
||||
|
||||
do_action( 'kivicare_check_activity_patterns', $user_id, $session_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle user authentication event
|
||||
*
|
||||
* @param int $user_id User ID
|
||||
* @param array $context Authentication context
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function on_user_authenticated( $user_id, $context ) {
|
||||
$ip_address = $context['ip_address'] ?? self::get_client_ip();
|
||||
$user_agent = $context['user_agent'] ?? ( $_SERVER['HTTP_USER_AGENT'] ?? '' );
|
||||
|
||||
// Create new session
|
||||
$session_id = self::create_session( $user_id, $ip_address, $user_agent );
|
||||
|
||||
// Log successful authentication
|
||||
self::log_security_event( $user_id, 'login', array(
|
||||
'session_id' => $session_id,
|
||||
'ip_address' => $ip_address,
|
||||
'user_agent' => $user_agent
|
||||
) );
|
||||
|
||||
// Clear failed login attempts
|
||||
delete_user_meta( $user_id, 'kivicare_failed_attempts' );
|
||||
delete_user_meta( $user_id, 'kivicare_lockout_time' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle user logout event
|
||||
*
|
||||
* @param int $user_id User ID
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function on_user_logout( $user_id ) {
|
||||
$session_id = get_user_meta( $user_id, 'kivicare_current_session', true );
|
||||
|
||||
if ( $session_id ) {
|
||||
self::expire_session( $session_id );
|
||||
}
|
||||
|
||||
// Log logout
|
||||
self::log_security_event( $user_id, 'logout', array(
|
||||
'session_id' => $session_id
|
||||
) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle failed login event
|
||||
*
|
||||
* @param array $context Failed login context
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function on_failed_login( $context ) {
|
||||
$user_id = $context['user_id'] ?? null;
|
||||
$username = $context['username'] ?? '';
|
||||
$ip_address = $context['ip_address'] ?? self::get_client_ip();
|
||||
|
||||
// Log failed attempt
|
||||
self::log_failed_login_attempt( $user_id, $username, $ip_address );
|
||||
|
||||
if ( $user_id ) {
|
||||
// Increment failed attempts counter
|
||||
$failed_attempts = (int) get_user_meta( $user_id, 'kivicare_failed_attempts', true ) + 1;
|
||||
update_user_meta( $user_id, 'kivicare_failed_attempts', $failed_attempts );
|
||||
|
||||
// Check if account should be locked
|
||||
if ( $failed_attempts >= self::$max_failed_attempts ) {
|
||||
self::lock_account( $user_id );
|
||||
}
|
||||
}
|
||||
|
||||
// Log security event
|
||||
self::log_security_event( $user_id, 'failed_login', $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock user account
|
||||
*
|
||||
* @param int $user_id User ID
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function lock_account( $user_id ) {
|
||||
$lockout_time = time() + self::$lockout_duration;
|
||||
update_user_meta( $user_id, 'kivicare_lockout_time', $lockout_time );
|
||||
|
||||
// Expire all active sessions
|
||||
self::expire_user_sessions( $user_id );
|
||||
|
||||
// Log security event
|
||||
self::log_security_event( $user_id, 'account_locked', array(
|
||||
'lockout_duration' => self::$lockout_duration,
|
||||
'lockout_until' => date( 'Y-m-d H:i:s', $lockout_time )
|
||||
) );
|
||||
|
||||
// Send notification (could be extended)
|
||||
do_action( 'kivicare_account_locked', $user_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce concurrent session limit
|
||||
*
|
||||
* @param int $user_id User ID
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function enforce_concurrent_session_limit( $user_id ) {
|
||||
global $wpdb;
|
||||
|
||||
// Get active sessions count
|
||||
$active_sessions = (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->prefix}kivicare_sessions
|
||||
WHERE user_id = %d AND is_active = 1 AND expires_at > NOW()",
|
||||
$user_id
|
||||
)
|
||||
);
|
||||
|
||||
// If at limit, expire oldest session
|
||||
if ( $active_sessions >= self::$max_concurrent_sessions ) {
|
||||
$oldest_session = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT session_id FROM {$wpdb->prefix}kivicare_sessions
|
||||
WHERE user_id = %d AND is_active = 1 AND expires_at > NOW()
|
||||
ORDER BY last_activity ASC LIMIT 1",
|
||||
$user_id
|
||||
)
|
||||
);
|
||||
|
||||
if ( $oldest_session ) {
|
||||
self::expire_session( $oldest_session );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store session in database
|
||||
*
|
||||
* @param array $session_data Session data
|
||||
* @return bool Success status
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function store_session( $session_data ) {
|
||||
global $wpdb;
|
||||
|
||||
// Create table if it doesn't exist
|
||||
self::create_sessions_table();
|
||||
|
||||
$result = $wpdb->insert(
|
||||
$wpdb->prefix . 'kivicare_sessions',
|
||||
$session_data,
|
||||
array( '%s', '%d', '%s', '%s', '%s', '%s', '%s', '%d' )
|
||||
);
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session from database
|
||||
*
|
||||
* @param string $session_id Session ID
|
||||
* @return array|null Session data or null
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function get_session( $session_id ) {
|
||||
global $wpdb;
|
||||
|
||||
return $wpdb->get_row(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$wpdb->prefix}kivicare_sessions WHERE session_id = %s",
|
||||
$session_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user session metadata
|
||||
*
|
||||
* @param int $user_id User ID
|
||||
* @param string $session_id Session ID
|
||||
* @param string $ip_address IP address
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function update_user_session_meta( $user_id, $session_id, $ip_address ) {
|
||||
update_user_meta( $user_id, 'kivicare_current_session', $session_id );
|
||||
update_user_meta( $user_id, 'kivicare_session_ip', $ip_address );
|
||||
update_user_meta( $user_id, 'kivicare_last_activity', current_time( 'mysql' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Log failed login attempt
|
||||
*
|
||||
* @param int|null $user_id User ID (if found)
|
||||
* @param string $username Username attempted
|
||||
* @param string $ip_address IP address
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function log_failed_login_attempt( $user_id, $username, $ip_address ) {
|
||||
global $wpdb;
|
||||
|
||||
// Create table if it doesn't exist
|
||||
self::create_failed_logins_table();
|
||||
|
||||
$wpdb->insert(
|
||||
$wpdb->prefix . 'kivicare_failed_logins',
|
||||
array(
|
||||
'user_id' => $user_id,
|
||||
'username' => $username,
|
||||
'ip_address' => $ip_address,
|
||||
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
|
||||
'attempted_at' => current_time( 'mysql' )
|
||||
),
|
||||
array( '%d', '%s', '%s', '%s', '%s' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log security event
|
||||
*
|
||||
* @param int $user_id User ID
|
||||
* @param string $event Event type
|
||||
* @param array $data Event data
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function log_security_event( $user_id, $event, $data = array() ) {
|
||||
global $wpdb;
|
||||
|
||||
// Create table if it doesn't exist
|
||||
self::create_security_log_table();
|
||||
|
||||
$wpdb->insert(
|
||||
$wpdb->prefix . 'kivicare_security_log',
|
||||
array(
|
||||
'user_id' => $user_id,
|
||||
'event_type' => $event,
|
||||
'event_data' => wp_json_encode( $data ),
|
||||
'ip_address' => self::get_client_ip(),
|
||||
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
|
||||
'created_at' => current_time( 'mysql' )
|
||||
),
|
||||
array( '%d', '%s', '%s', '%s', '%s', '%s' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format session data for API response
|
||||
*
|
||||
* @param array $session_data Raw session data
|
||||
* @return array Formatted session data
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function format_session_data( $session_data ) {
|
||||
return array(
|
||||
'session_id' => $session_data['session_id'],
|
||||
'ip_address' => $session_data['ip_address'],
|
||||
'user_agent' => $session_data['user_agent'],
|
||||
'created_at' => $session_data['created_at'],
|
||||
'last_activity' => $session_data['last_activity'],
|
||||
'expires_at' => $session_data['expires_at'],
|
||||
'is_current' => get_user_meta( $session_data['user_id'], 'kivicare_current_session', true ) === $session_data['session_id']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup expired sessions
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function cleanup_expired_sessions() {
|
||||
global $wpdb;
|
||||
|
||||
// Delete expired sessions
|
||||
$wpdb->query(
|
||||
"DELETE FROM {$wpdb->prefix}kivicare_sessions
|
||||
WHERE expires_at < NOW() OR (is_active = 0 AND ended_at < DATE_SUB(NOW(), INTERVAL 30 DAY))"
|
||||
);
|
||||
|
||||
// Clean up old failed login attempts
|
||||
$wpdb->query(
|
||||
"DELETE FROM {$wpdb->prefix}kivicare_failed_logins
|
||||
WHERE attempted_at < DATE_SUB(NOW(), INTERVAL 7 DAY)"
|
||||
);
|
||||
|
||||
// Clean up old security log entries (keep 90 days)
|
||||
$wpdb->query(
|
||||
"DELETE FROM {$wpdb->prefix}kivicare_security_log
|
||||
WHERE created_at < DATE_SUB(NOW(), INTERVAL 90 DAY)"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get client IP address
|
||||
*
|
||||
* @return string IP address
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function get_client_ip() {
|
||||
$ip_keys = array(
|
||||
'HTTP_CF_CONNECTING_IP',
|
||||
'HTTP_X_FORWARDED_FOR',
|
||||
'HTTP_X_FORWARDED',
|
||||
'HTTP_X_CLUSTER_CLIENT_IP',
|
||||
'HTTP_FORWARDED_FOR',
|
||||
'HTTP_FORWARDED',
|
||||
'REMOTE_ADDR'
|
||||
);
|
||||
|
||||
foreach ( $ip_keys as $key ) {
|
||||
if ( ! empty( $_SERVER[ $key ] ) ) {
|
||||
$ip = $_SERVER[ $key ];
|
||||
if ( strpos( $ip, ',' ) !== false ) {
|
||||
$ip = explode( ',', $ip )[0];
|
||||
}
|
||||
$ip = trim( $ip );
|
||||
if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
|
||||
return $ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create sessions table
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function create_sessions_table() {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = $wpdb->prefix . 'kivicare_sessions';
|
||||
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
|
||||
$sql = "CREATE TABLE IF NOT EXISTS {$table_name} (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
session_id varchar(36) NOT NULL,
|
||||
user_id bigint(20) unsigned NOT NULL,
|
||||
ip_address varchar(45) NOT NULL,
|
||||
user_agent text NOT NULL,
|
||||
created_at datetime NOT NULL,
|
||||
last_activity datetime NOT NULL,
|
||||
expires_at datetime NOT NULL,
|
||||
ended_at datetime NULL,
|
||||
is_active tinyint(1) NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY session_id (session_id),
|
||||
KEY user_id (user_id),
|
||||
KEY expires_at (expires_at),
|
||||
KEY is_active (is_active)
|
||||
) {$charset_collate};";
|
||||
|
||||
require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
|
||||
dbDelta( $sql );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create failed logins table
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function create_failed_logins_table() {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = $wpdb->prefix . 'kivicare_failed_logins';
|
||||
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
|
||||
$sql = "CREATE TABLE IF NOT EXISTS {$table_name} (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
user_id bigint(20) unsigned NULL,
|
||||
username varchar(60) NOT NULL,
|
||||
ip_address varchar(45) NOT NULL,
|
||||
user_agent text NOT NULL,
|
||||
attempted_at datetime NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY user_id (user_id),
|
||||
KEY ip_address (ip_address),
|
||||
KEY attempted_at (attempted_at)
|
||||
) {$charset_collate};";
|
||||
|
||||
require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
|
||||
dbDelta( $sql );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create security log table
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function create_security_log_table() {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = $wpdb->prefix . 'kivicare_security_log';
|
||||
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
|
||||
$sql = "CREATE TABLE IF NOT EXISTS {$table_name} (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
user_id bigint(20) unsigned NULL,
|
||||
event_type varchar(50) NOT NULL,
|
||||
event_data longtext NULL,
|
||||
ip_address varchar(45) NOT NULL,
|
||||
user_agent text NOT NULL,
|
||||
created_at datetime NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY user_id (user_id),
|
||||
KEY event_type (event_type),
|
||||
KEY created_at (created_at)
|
||||
) {$charset_collate};";
|
||||
|
||||
require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
|
||||
dbDelta( $sql );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user