- Bump DESK_MOLONI version to 3.0.1 across module - Normalize hooks to after_client_* and instantiate PerfexHooks safely - Fix OAuthController view path and API client class name - Add missing admin views for webhook config/logs; adjust view loading - Harden client portal routes and admin routes mapping - Make Dashboard/Logs/Queue tolerant to optional model methods - Align log details query with existing schema; avoid broken joins This makes the module operational in Perfex (admin + client), reduces 404s, and avoids fatal errors due to inconsistent tables/methods.
465 lines
14 KiB
Bash
465 lines
14 KiB
Bash
#!/bin/bash
|
|
# Desk-Moloni v3.0 OAuth Token Refresh Script
|
|
#
|
|
# Automatically refreshes OAuth tokens before expiration to maintain
|
|
# continuous API connectivity without manual intervention.
|
|
|
|
set -euo pipefail
|
|
|
|
# Script configuration
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
MODULE_DIR="$(dirname "$SCRIPT_DIR")"
|
|
CLI_DIR="$MODULE_DIR/cli"
|
|
LOG_DIR="$MODULE_DIR/logs"
|
|
LOCK_FILE="$MODULE_DIR/locks/token_refresh.lock"
|
|
|
|
# Configuration
|
|
REFRESH_THRESHOLD=300 # Refresh 5 minutes before expiry
|
|
MAX_ATTEMPTS=3
|
|
RETRY_DELAY=60
|
|
|
|
# Colors for output
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m'
|
|
|
|
# Logging functions
|
|
log_info() {
|
|
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
|
local message="[$timestamp] [INFO] $1"
|
|
echo -e "${BLUE}$message${NC}"
|
|
echo "$message" >> "$LOG_DIR/token_refresh.log" 2>/dev/null || true
|
|
}
|
|
|
|
log_success() {
|
|
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
|
local message="[$timestamp] [SUCCESS] $1"
|
|
echo -e "${GREEN}$message${NC}"
|
|
echo "$message" >> "$LOG_DIR/token_refresh.log" 2>/dev/null || true
|
|
}
|
|
|
|
log_warning() {
|
|
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
|
local message="[$timestamp] [WARNING] $1"
|
|
echo -e "${YELLOW}$message${NC}"
|
|
echo "$message" >> "$LOG_DIR/token_refresh.log" 2>/dev/null || true
|
|
}
|
|
|
|
log_error() {
|
|
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
|
local message="[$timestamp] [ERROR] $1"
|
|
echo -e "${RED}$message${NC}"
|
|
echo "$message" >> "$LOG_DIR/token_refresh.log" 2>/dev/null || true
|
|
}
|
|
|
|
# Help function
|
|
show_help() {
|
|
cat << EOF
|
|
Desk-Moloni v3.0 OAuth Token Refresh Script
|
|
|
|
Usage: $0 [OPTIONS]
|
|
|
|
Options:
|
|
-h, --help Show this help message
|
|
-t, --threshold SECONDS Refresh threshold in seconds (default: $REFRESH_THRESHOLD)
|
|
-a, --attempts COUNT Maximum retry attempts (default: $MAX_ATTEMPTS)
|
|
-d, --delay SECONDS Retry delay in seconds (default: $RETRY_DELAY)
|
|
--dry-run Show what would be done without changes
|
|
--force Force refresh even if not needed
|
|
--check-only Only check token status, don't refresh
|
|
|
|
Description:
|
|
This script automatically checks OAuth token expiration and refreshes
|
|
tokens when they are close to expiring. It's designed to run as a cron
|
|
job to maintain continuous API connectivity.
|
|
|
|
The script will:
|
|
1. Check current token expiration time
|
|
2. Compare against refresh threshold
|
|
3. Attempt to refresh if needed
|
|
4. Retry on failures with exponential backoff
|
|
5. Log all activities for monitoring
|
|
|
|
Examples:
|
|
$0 # Normal token refresh check
|
|
$0 --force # Force refresh regardless of expiration
|
|
$0 --check-only # Just check status, don't refresh
|
|
$0 --dry-run # Preview what would be done
|
|
|
|
EOF
|
|
}
|
|
|
|
# Parse command line arguments
|
|
DRY_RUN=false
|
|
FORCE_REFRESH=false
|
|
CHECK_ONLY=false
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
-h|--help)
|
|
show_help
|
|
exit 0
|
|
;;
|
|
-t|--threshold)
|
|
REFRESH_THRESHOLD="$2"
|
|
shift 2
|
|
;;
|
|
-a|--attempts)
|
|
MAX_ATTEMPTS="$2"
|
|
shift 2
|
|
;;
|
|
-d|--delay)
|
|
RETRY_DELAY="$2"
|
|
shift 2
|
|
;;
|
|
--dry-run)
|
|
DRY_RUN=true
|
|
shift
|
|
;;
|
|
--force)
|
|
FORCE_REFRESH=true
|
|
shift
|
|
;;
|
|
--check-only)
|
|
CHECK_ONLY=true
|
|
shift
|
|
;;
|
|
*)
|
|
log_error "Unknown option: $1"
|
|
show_help
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Ensure required directories exist
|
|
ensure_directories() {
|
|
local dirs=("$LOG_DIR" "$(dirname "$LOCK_FILE")")
|
|
|
|
for dir in "${dirs[@]}"; do
|
|
if [[ ! -d "$dir" ]]; then
|
|
mkdir -p "$dir" 2>/dev/null || true
|
|
fi
|
|
done
|
|
}
|
|
|
|
# Get token information using PHP
|
|
get_token_info() {
|
|
local token_info
|
|
|
|
if ! token_info=$(php -r "
|
|
require_once '$MODULE_DIR/config/bootstrap.php';
|
|
|
|
try {
|
|
// Load configuration service
|
|
require_once '$MODULE_DIR/src/Services/ConfigService.php';
|
|
\$configService = new DeskMoloni\Services\ConfigService();
|
|
|
|
// Get token information
|
|
\$accessToken = \$configService->get('oauth_access_token');
|
|
\$refreshToken = \$configService->get('oauth_refresh_token');
|
|
\$expiresAt = \$configService->get('oauth_expires_at');
|
|
|
|
if (empty(\$accessToken)) {
|
|
echo 'NO_TOKEN';
|
|
exit(0);
|
|
}
|
|
|
|
\$currentTime = time();
|
|
\$expiryTime = \$expiresAt ? (int)\$expiresAt : 0;
|
|
\$timeUntilExpiry = \$expiryTime - \$currentTime;
|
|
|
|
// Output format: STATUS|EXPIRES_IN|HAS_REFRESH_TOKEN
|
|
echo 'VALID|' . \$timeUntilExpiry . '|' . (!empty(\$refreshToken) ? '1' : '0');
|
|
|
|
} catch (Exception \$e) {
|
|
echo 'ERROR|' . \$e->getMessage();
|
|
}
|
|
" 2>/dev/null); then
|
|
log_error "Failed to get token information"
|
|
return 1
|
|
fi
|
|
|
|
echo "$token_info"
|
|
}
|
|
|
|
# Check if token needs refresh
|
|
needs_refresh() {
|
|
local token_info
|
|
token_info=$(get_token_info)
|
|
|
|
if [[ "$token_info" == "NO_TOKEN" ]]; then
|
|
log_warning "No OAuth token found"
|
|
return 2 # Special case: no token at all
|
|
fi
|
|
|
|
if [[ "$token_info" =~ ^ERROR\| ]]; then
|
|
log_error "Error checking token: ${token_info#ERROR|}"
|
|
return 1
|
|
fi
|
|
|
|
IFS='|' read -r status expires_in has_refresh <<< "$token_info"
|
|
|
|
if [[ "$status" != "VALID" ]]; then
|
|
log_error "Token is not valid: $status"
|
|
return 1
|
|
fi
|
|
|
|
log_info "Token expires in ${expires_in} seconds"
|
|
|
|
# Check if we have a refresh token
|
|
if [[ "$has_refresh" != "1" ]]; then
|
|
log_error "No refresh token available"
|
|
return 1
|
|
fi
|
|
|
|
# Check if refresh is needed
|
|
if [[ "$FORCE_REFRESH" == true ]]; then
|
|
log_info "Force refresh requested"
|
|
return 0
|
|
fi
|
|
|
|
if [[ "$expires_in" -le "$REFRESH_THRESHOLD" ]]; then
|
|
log_info "Token needs refresh (expires in ${expires_in}s, threshold: ${REFRESH_THRESHOLD}s)"
|
|
return 0
|
|
fi
|
|
|
|
log_info "Token refresh not needed (expires in ${expires_in}s)"
|
|
return 3 # No refresh needed
|
|
}
|
|
|
|
# Perform token refresh
|
|
refresh_token() {
|
|
local attempt=1
|
|
|
|
while [[ $attempt -le $MAX_ATTEMPTS ]]; do
|
|
log_info "Token refresh attempt $attempt/$MAX_ATTEMPTS"
|
|
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
log_info "Would attempt to refresh OAuth token"
|
|
return 0
|
|
fi
|
|
|
|
# Attempt refresh using PHP
|
|
local refresh_result
|
|
if refresh_result=$(php -r "
|
|
require_once '$MODULE_DIR/config/bootstrap.php';
|
|
|
|
try {
|
|
require_once '$MODULE_DIR/src/Services/AuthService.php';
|
|
\$authService = new DeskMoloni\Services\AuthService();
|
|
|
|
\$result = \$authService->refreshToken();
|
|
|
|
if (\$result['success']) {
|
|
echo 'SUCCESS|New token expires in ' . \$result['expires_in'] . ' seconds';
|
|
} else {
|
|
echo 'FAILED|' . (\$result['error'] ?? 'Unknown error');
|
|
}
|
|
|
|
} catch (Exception \$e) {
|
|
echo 'ERROR|' . \$e->getMessage();
|
|
}
|
|
" 2>/dev/null); then
|
|
|
|
IFS='|' read -r result_status result_message <<< "$refresh_result"
|
|
|
|
case "$result_status" in
|
|
SUCCESS)
|
|
log_success "Token refreshed successfully: $result_message"
|
|
return 0
|
|
;;
|
|
FAILED)
|
|
log_error "Token refresh failed: $result_message"
|
|
;;
|
|
ERROR)
|
|
log_error "Error during token refresh: $result_message"
|
|
;;
|
|
*)
|
|
log_error "Unexpected refresh result: $refresh_result"
|
|
;;
|
|
esac
|
|
else
|
|
log_error "Failed to execute token refresh"
|
|
fi
|
|
|
|
# Increment attempt counter
|
|
((attempt++))
|
|
|
|
# Wait before retry (exponential backoff)
|
|
if [[ $attempt -le $MAX_ATTEMPTS ]]; then
|
|
local wait_time=$((RETRY_DELAY * attempt))
|
|
log_info "Retrying in ${wait_time} seconds..."
|
|
sleep "$wait_time"
|
|
fi
|
|
done
|
|
|
|
log_error "Token refresh failed after $MAX_ATTEMPTS attempts"
|
|
return 1
|
|
}
|
|
|
|
# Send notification about token issues
|
|
send_notification() {
|
|
local subject="$1"
|
|
local message="$2"
|
|
|
|
# Log the notification
|
|
log_warning "NOTIFICATION: $subject - $message"
|
|
|
|
# Try to send notification via PHP if notification system is configured
|
|
php -r "
|
|
require_once '$MODULE_DIR/config/bootstrap.php';
|
|
|
|
try {
|
|
require_once '$MODULE_DIR/src/Services/NotificationService.php';
|
|
\$notificationService = new DeskMoloni\Services\NotificationService();
|
|
\$notificationService->sendAlert('$subject', '$message');
|
|
} catch (Exception \$e) {
|
|
// Notification service may not be configured, that's OK
|
|
}
|
|
" 2>/dev/null || true
|
|
}
|
|
|
|
# Check token status and report
|
|
check_token_status() {
|
|
local token_info
|
|
token_info=$(get_token_info)
|
|
|
|
log_info "=== TOKEN STATUS REPORT ==="
|
|
|
|
case "$token_info" in
|
|
NO_TOKEN)
|
|
log_error "❌ No OAuth token configured"
|
|
log_info "Please configure OAuth credentials in the admin panel"
|
|
return 1
|
|
;;
|
|
ERROR*)
|
|
log_error "❌ Error checking token: ${token_info#ERROR|}"
|
|
return 1
|
|
;;
|
|
VALID*)
|
|
IFS='|' read -r status expires_in has_refresh <<< "$token_info"
|
|
|
|
local hours=$((expires_in / 3600))
|
|
local minutes=$(((expires_in % 3600) / 60))
|
|
|
|
if [[ "$expires_in" -gt "$REFRESH_THRESHOLD" ]]; then
|
|
log_success "✅ Token is valid and fresh"
|
|
log_info " Expires in: ${hours}h ${minutes}m"
|
|
log_info " Refresh token: $([ "$has_refresh" == "1" ] && echo "Available" || echo "Missing")"
|
|
elif [[ "$expires_in" -gt 0 ]]; then
|
|
log_warning "⚠️ Token expires soon"
|
|
log_info " Expires in: ${hours}h ${minutes}m"
|
|
log_info " Refresh token: $([ "$has_refresh" == "1" ] && echo "Available" || echo "Missing")"
|
|
else
|
|
log_error "❌ Token has expired"
|
|
log_info " Expired: $((-expires_in)) seconds ago"
|
|
log_info " Refresh token: $([ "$has_refresh" == "1" ] && echo "Available" || echo "Missing")"
|
|
fi
|
|
|
|
return 0
|
|
;;
|
|
*)
|
|
log_error "❌ Unknown token status: $token_info"
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Main execution function
|
|
main() {
|
|
log_info "Starting OAuth token refresh check"
|
|
|
|
# Check if only status check is requested
|
|
if [[ "$CHECK_ONLY" == true ]]; then
|
|
check_token_status
|
|
exit $?
|
|
fi
|
|
|
|
# Check if token needs refresh
|
|
local refresh_needed=0
|
|
needs_refresh || refresh_needed=$?
|
|
|
|
case $refresh_needed in
|
|
0) # Needs refresh
|
|
if refresh_token; then
|
|
log_success "Token refresh completed successfully"
|
|
|
|
# Verify the new token
|
|
local new_token_info
|
|
new_token_info=$(get_token_info)
|
|
if [[ "$new_token_info" =~ ^VALID\|([0-9]+)\| ]]; then
|
|
local new_expires_in="${BASH_REMATCH[1]}"
|
|
local new_hours=$((new_expires_in / 3600))
|
|
log_success "New token expires in ${new_hours} hours"
|
|
fi
|
|
else
|
|
log_error "Token refresh failed"
|
|
send_notification "OAuth Token Refresh Failed" "Failed to refresh Moloni API token after $MAX_ATTEMPTS attempts. Manual intervention may be required."
|
|
exit 1
|
|
fi
|
|
;;
|
|
1) # Error
|
|
log_error "Error checking token refresh requirements"
|
|
exit 1
|
|
;;
|
|
2) # No token
|
|
log_warning "No OAuth token configured - skipping refresh"
|
|
send_notification "OAuth Token Missing" "No OAuth token is configured for Moloni API. Please configure OAuth credentials."
|
|
exit 0
|
|
;;
|
|
3) # No refresh needed
|
|
log_info "Token refresh not required at this time"
|
|
exit 0
|
|
;;
|
|
*)
|
|
log_error "Unexpected error code: $refresh_needed"
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Cleanup function
|
|
cleanup() {
|
|
# Remove lock file if it exists and we created it
|
|
if [[ -f "$LOCK_FILE" ]] && [[ "${LOCK_ACQUIRED:-0}" == "1" ]]; then
|
|
rm -f "$LOCK_FILE"
|
|
fi
|
|
}
|
|
|
|
# Set up cleanup trap
|
|
trap cleanup EXIT
|
|
|
|
# Acquire lock to prevent concurrent execution
|
|
acquire_lock() {
|
|
if [[ -f "$LOCK_FILE" ]]; then
|
|
local lock_pid
|
|
lock_pid=$(cat "$LOCK_FILE" 2>/dev/null || echo "")
|
|
|
|
if [[ -n "$lock_pid" ]] && kill -0 "$lock_pid" 2>/dev/null; then
|
|
log_warning "Another token refresh process is running (PID: $lock_pid)"
|
|
exit 0
|
|
else
|
|
log_info "Removing stale lock file"
|
|
rm -f "$LOCK_FILE"
|
|
fi
|
|
fi
|
|
|
|
echo $$ > "$LOCK_FILE"
|
|
export LOCK_ACQUIRED=1
|
|
log_info "Lock acquired (PID: $$)"
|
|
}
|
|
|
|
# Initialize and run
|
|
initialize() {
|
|
ensure_directories
|
|
acquire_lock
|
|
main "$@"
|
|
}
|
|
|
|
# Execute if called directly
|
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|
initialize "$@"
|
|
fi |