#!/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