mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 14:51:08 -05:00
- Updated database settings to use dj_database_url for environment-based configuration - Added dj-database-url dependency - Configured PostGIS backend for spatial data support - Set default DATABASE_URL for production PostgreSQL connection
632 lines
22 KiB
Python
Executable File
632 lines
22 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""
|
||
ThrillWiki GitHub PAT Setup Helper
|
||
Interactive script for setting up GitHub Personal Access Tokens with proper validation
|
||
and integration with the automation system.
|
||
|
||
Features:
|
||
- Guided GitHub PAT creation process
|
||
- Token validation and permission checking
|
||
- Integration with existing github-auth.py patterns
|
||
- Clear instructions for PAT scope requirements
|
||
- Secure token storage with proper file permissions
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import json
|
||
import time
|
||
import getpass
|
||
import requests
|
||
import argparse
|
||
import subprocess
|
||
from pathlib import Path
|
||
from urllib.parse import urlencode
|
||
|
||
# Configuration
|
||
SCRIPT_DIR = Path(__file__).parent
|
||
PROJECT_DIR = SCRIPT_DIR.parent.parent
|
||
CONFIG_SCRIPT = SCRIPT_DIR / "automation-config.sh"
|
||
GITHUB_AUTH_SCRIPT = PROJECT_DIR / "scripts" / "github-auth.py"
|
||
TOKEN_FILE = PROJECT_DIR / ".github-pat"
|
||
|
||
# GitHub API Configuration
|
||
GITHUB_API_BASE = "https://api.github.com"
|
||
REQUEST_TIMEOUT = 30
|
||
|
||
# Token scope requirements for different use cases
|
||
TOKEN_SCOPES = {
|
||
"public": {
|
||
"description": "Public repositories only",
|
||
"scopes": ["public_repo"],
|
||
"note": "Suitable for public repositories and basic automation"
|
||
},
|
||
"private": {
|
||
"description": "Private repositories access",
|
||
"scopes": ["repo"],
|
||
"note": "Required for private repositories and full automation features"
|
||
},
|
||
"full": {
|
||
"description": "Full automation capabilities",
|
||
"scopes": ["repo", "workflow", "read:org"],
|
||
"note": "Recommended for complete automation setup with GitHub Actions"
|
||
}
|
||
}
|
||
|
||
class Colors:
|
||
"""ANSI color codes for terminal output"""
|
||
RED = '\033[0;31m'
|
||
GREEN = '\033[0;32m'
|
||
YELLOW = '\033[1;33m'
|
||
BLUE = '\033[0;34m'
|
||
PURPLE = '\033[0;35m'
|
||
CYAN = '\033[0;36m'
|
||
BOLD = '\033[1m'
|
||
NC = '\033[0m' # No Color
|
||
|
||
def print_colored(message, color=Colors.NC):
|
||
"""Print colored message to terminal"""
|
||
print(f"{color}{message}{Colors.NC}")
|
||
|
||
def print_error(message):
|
||
"""Print error message"""
|
||
print_colored(f"❌ Error: {message}", Colors.RED)
|
||
|
||
def print_success(message):
|
||
"""Print success message"""
|
||
print_colored(f"✅ {message}", Colors.GREEN)
|
||
|
||
def print_warning(message):
|
||
"""Print warning message"""
|
||
print_colored(f"⚠️ Warning: {message}", Colors.YELLOW)
|
||
|
||
def print_info(message):
|
||
"""Print info message"""
|
||
print_colored(f"ℹ️ {message}", Colors.BLUE)
|
||
|
||
def print_step(step, total, message):
|
||
"""Print step progress"""
|
||
print_colored(f"\n[{step}/{total}] {message}", Colors.CYAN)
|
||
|
||
def validate_token_format(token):
|
||
"""Validate GitHub token format"""
|
||
if not token:
|
||
return False
|
||
|
||
# GitHub token patterns
|
||
patterns = [
|
||
lambda t: t.startswith('ghp_') and len(t) >= 40, # Classic PAT
|
||
lambda t: t.startswith('github_pat_') and len(t) >= 50, # Fine-grained PAT
|
||
lambda t: t.startswith('gho_') and len(t) >= 40, # OAuth token
|
||
lambda t: t.startswith('ghu_') and len(t) >= 40, # User token
|
||
lambda t: t.startswith('ghs_') and len(t) >= 40, # Server token
|
||
]
|
||
|
||
return any(pattern(token) for pattern in patterns)
|
||
|
||
def test_github_token(token, timeout=REQUEST_TIMEOUT):
|
||
"""Test GitHub token by making API call"""
|
||
if not token:
|
||
return False, "No token provided"
|
||
|
||
try:
|
||
headers = {
|
||
'Authorization': f'Bearer {token}',
|
||
'Accept': 'application/vnd.github+json',
|
||
'X-GitHub-Api-Version': '2022-11-28'
|
||
}
|
||
|
||
response = requests.get(
|
||
f"{GITHUB_API_BASE}/user",
|
||
headers=headers,
|
||
timeout=timeout
|
||
)
|
||
|
||
if response.status_code == 200:
|
||
user_data = response.json()
|
||
return True, f"Valid token for user: {user_data.get('login', 'unknown')}"
|
||
elif response.status_code == 401:
|
||
return False, "Invalid or expired token"
|
||
elif response.status_code == 403:
|
||
return False, "Token lacks required permissions"
|
||
else:
|
||
return False, f"API request failed with HTTP {response.status_code}"
|
||
|
||
except requests.exceptions.RequestException as e:
|
||
return False, f"Network error: {str(e)}"
|
||
|
||
def get_token_permissions(token, timeout=REQUEST_TIMEOUT):
|
||
"""Get token permissions and scopes"""
|
||
if not token:
|
||
return None, "No token provided"
|
||
|
||
try:
|
||
headers = {
|
||
'Authorization': f'Bearer {token}',
|
||
'Accept': 'application/vnd.github+json',
|
||
'X-GitHub-Api-Version': '2022-11-28'
|
||
}
|
||
|
||
# Get user info and check token in response headers
|
||
response = requests.get(
|
||
f"{GITHUB_API_BASE}/user",
|
||
headers=headers,
|
||
timeout=timeout
|
||
)
|
||
|
||
if response.status_code == 200:
|
||
scopes = response.headers.get('X-OAuth-Scopes', '').split(', ')
|
||
scopes = [scope.strip() for scope in scopes if scope.strip()]
|
||
|
||
return scopes, None
|
||
else:
|
||
return None, f"Failed to get permissions: HTTP {response.status_code}"
|
||
|
||
except requests.exceptions.RequestException as e:
|
||
return None, f"Network error: {str(e)}"
|
||
|
||
def check_repository_access(token, repo_url=None, timeout=REQUEST_TIMEOUT):
|
||
"""Check if token can access the repository"""
|
||
if not token:
|
||
return False, "No token provided"
|
||
|
||
# Try to determine repository from git remote
|
||
if not repo_url:
|
||
try:
|
||
result = subprocess.run(
|
||
['git', 'remote', 'get-url', 'origin'],
|
||
cwd=PROJECT_DIR,
|
||
capture_output=True,
|
||
text=True,
|
||
timeout=10
|
||
)
|
||
if result.returncode == 0:
|
||
repo_url = result.stdout.strip()
|
||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||
pass
|
||
|
||
if not repo_url:
|
||
return None, "Could not determine repository URL"
|
||
|
||
# Extract owner/repo from URL
|
||
if 'github.com' in repo_url:
|
||
# Handle both SSH and HTTPS URLs
|
||
if repo_url.startswith('git@github.com:'):
|
||
repo_path = repo_url.replace('git@github.com:', '').replace('.git', '')
|
||
elif 'github.com/' in repo_url:
|
||
repo_path = repo_url.split('github.com/')[-1].replace('.git', '')
|
||
else:
|
||
return None, "Could not parse repository URL"
|
||
|
||
try:
|
||
headers = {
|
||
'Authorization': f'Bearer {token}',
|
||
'Accept': 'application/vnd.github+json',
|
||
'X-GitHub-Api-Version': '2022-11-28'
|
||
}
|
||
|
||
response = requests.get(
|
||
f"{GITHUB_API_BASE}/repos/{repo_path}",
|
||
headers=headers,
|
||
timeout=timeout
|
||
)
|
||
|
||
if response.status_code == 200:
|
||
repo_data = response.json()
|
||
return True, f"Access confirmed for {repo_data.get('full_name', repo_path)}"
|
||
elif response.status_code == 404:
|
||
return False, "Repository not found or no access"
|
||
elif response.status_code == 403:
|
||
return False, "Access denied - insufficient permissions"
|
||
else:
|
||
return False, f"Access check failed: HTTP {response.status_code}"
|
||
|
||
except requests.exceptions.RequestException as e:
|
||
return None, f"Network error: {str(e)}"
|
||
|
||
return None, "Not a GitHub repository"
|
||
|
||
def show_pat_instructions():
|
||
"""Show detailed PAT creation instructions"""
|
||
print_colored("\n" + "="*60, Colors.BOLD)
|
||
print_colored("GitHub Personal Access Token (PAT) Setup Guide", Colors.BOLD)
|
||
print_colored("="*60, Colors.BOLD)
|
||
|
||
print("\n🔐 Why do you need a GitHub PAT?")
|
||
print(" • Access private repositories")
|
||
print(" • Avoid GitHub API rate limits")
|
||
print(" • Enable automated repository operations")
|
||
print(" • Secure authentication without passwords")
|
||
|
||
print("\n📋 Step-by-step PAT creation:")
|
||
print(" 1. Go to: https://github.com/settings/tokens")
|
||
print(" 2. Click 'Generate new token' → 'Generate new token (classic)'")
|
||
print(" 3. Enter a descriptive note: 'ThrillWiki Automation'")
|
||
print(" 4. Set expiration (recommended: 90 days for security)")
|
||
print(" 5. Select appropriate scopes:")
|
||
|
||
print("\n🎯 Recommended scope configurations:")
|
||
for scope_type, config in TOKEN_SCOPES.items():
|
||
print(f"\n {scope_type.upper()} REPOSITORIES:")
|
||
print(f" • Description: {config['description']}")
|
||
print(f" • Required scopes: {', '.join(config['scopes'])}")
|
||
print(f" • Note: {config['note']}")
|
||
|
||
print("\n⚡ Quick setup for most users:")
|
||
print(" • Select 'repo' scope for full repository access")
|
||
print(" • This enables all automation features")
|
||
|
||
print("\n🔒 Security best practices:")
|
||
print(" • Use descriptive token names")
|
||
print(" • Set reasonable expiration dates")
|
||
print(" • Regenerate tokens regularly")
|
||
print(" • Never share tokens in public")
|
||
print(" • Delete unused tokens immediately")
|
||
|
||
print("\n📱 After creating your token:")
|
||
print(" • Copy the token immediately (it won't be shown again)")
|
||
print(" • Return to this script and paste it when prompted")
|
||
print(" • The script will validate and securely store your token")
|
||
|
||
def interactive_token_setup():
|
||
"""Interactive token setup process"""
|
||
print_colored("\n🚀 ThrillWiki GitHub PAT Setup", Colors.BOLD)
|
||
print_colored("================================", Colors.BOLD)
|
||
|
||
# Check if token already exists
|
||
if TOKEN_FILE.exists():
|
||
try:
|
||
existing_token = TOKEN_FILE.read_text().strip()
|
||
if existing_token:
|
||
print_info("Existing GitHub token found")
|
||
|
||
# Test existing token
|
||
valid, message = test_github_token(existing_token)
|
||
if valid:
|
||
print_success(f"Current token is valid: {message}")
|
||
|
||
choice = input("\nDo you want to replace the existing token? (y/N): ").strip().lower()
|
||
if choice not in ['y', 'yes']:
|
||
print_info("Keeping existing token")
|
||
return True
|
||
else:
|
||
print_warning(f"Current token is invalid: {message}")
|
||
print_info("Setting up new token...")
|
||
except Exception as e:
|
||
print_warning(f"Could not read existing token: {e}")
|
||
|
||
# Show instructions
|
||
print("\n" + "="*50)
|
||
choice = input("Do you want to see PAT creation instructions? (Y/n): ").strip().lower()
|
||
if choice not in ['n', 'no']:
|
||
show_pat_instructions()
|
||
|
||
# Get token from user
|
||
print_step(1, 3, "Enter your GitHub Personal Access Token")
|
||
print("📋 Please paste your GitHub PAT below:")
|
||
print(" (Input will be hidden for security)")
|
||
|
||
while True:
|
||
try:
|
||
token = getpass.getpass("GitHub PAT: ").strip()
|
||
|
||
if not token:
|
||
print_error("No token entered. Please try again.")
|
||
continue
|
||
|
||
# Validate format
|
||
if not validate_token_format(token):
|
||
print_error("Invalid token format. GitHub tokens should start with 'ghp_', 'github_pat_', etc.")
|
||
retry = input("Try again? (Y/n): ").strip().lower()
|
||
if retry in ['n', 'no']:
|
||
return False
|
||
continue
|
||
|
||
break
|
||
|
||
except KeyboardInterrupt:
|
||
print("\nSetup cancelled by user")
|
||
return False
|
||
|
||
# Test token
|
||
print_step(2, 3, "Validating GitHub token")
|
||
print("🔍 Testing token with GitHub API...")
|
||
|
||
valid, message = test_github_token(token)
|
||
if not valid:
|
||
print_error(f"Token validation failed: {message}")
|
||
return False
|
||
|
||
print_success(message)
|
||
|
||
# Check permissions
|
||
print("🔐 Checking token permissions...")
|
||
scopes, error = get_token_permissions(token)
|
||
if error:
|
||
print_warning(f"Could not check permissions: {error}")
|
||
else:
|
||
print_success(f"Token scopes: {', '.join(scopes) if scopes else 'None detected'}")
|
||
|
||
# Check for recommended scopes
|
||
has_repo = 'repo' in scopes or 'public_repo' in scopes
|
||
if not has_repo:
|
||
print_warning("Token may lack repository access permissions")
|
||
|
||
# Check repository access
|
||
print("📁 Checking repository access...")
|
||
access, access_message = check_repository_access(token)
|
||
if access is True:
|
||
print_success(access_message)
|
||
elif access is False:
|
||
print_warning(access_message)
|
||
else:
|
||
print_info(access_message or "Repository access check skipped")
|
||
|
||
# Store token
|
||
print_step(3, 3, "Storing GitHub token securely")
|
||
|
||
try:
|
||
# Backup existing token if it exists
|
||
if TOKEN_FILE.exists():
|
||
backup_file = TOKEN_FILE.with_suffix('.backup')
|
||
TOKEN_FILE.rename(backup_file)
|
||
print_info(f"Existing token backed up to: {backup_file}")
|
||
|
||
# Write new token
|
||
TOKEN_FILE.write_text(token)
|
||
TOKEN_FILE.chmod(0o600) # Read/write for owner only
|
||
|
||
print_success(f"Token stored securely in: {TOKEN_FILE}")
|
||
|
||
# Try to update configuration via config script
|
||
try:
|
||
if CONFIG_SCRIPT.exists():
|
||
subprocess.run([
|
||
'bash', '-c',
|
||
f'source {CONFIG_SCRIPT} && store_github_token "{token}"'
|
||
], check=False, capture_output=True)
|
||
print_success("Token added to automation configuration")
|
||
except Exception as e:
|
||
print_warning(f"Could not update automation config: {e}")
|
||
|
||
print_success("GitHub PAT setup completed successfully!")
|
||
return True
|
||
|
||
except Exception as e:
|
||
print_error(f"Failed to store token: {e}")
|
||
return False
|
||
|
||
def validate_existing_token():
|
||
"""Validate existing GitHub token"""
|
||
print_colored("\n🔍 GitHub Token Validation", Colors.BOLD)
|
||
print_colored("===========================", Colors.BOLD)
|
||
|
||
if not TOKEN_FILE.exists():
|
||
print_error("No GitHub token file found")
|
||
print_info(f"Expected location: {TOKEN_FILE}")
|
||
return False
|
||
|
||
try:
|
||
token = TOKEN_FILE.read_text().strip()
|
||
if not token:
|
||
print_error("Token file is empty")
|
||
return False
|
||
|
||
print_info("Validating stored token...")
|
||
|
||
# Format validation
|
||
if not validate_token_format(token):
|
||
print_error("Token format is invalid")
|
||
return False
|
||
|
||
print_success("Token format is valid")
|
||
|
||
# API validation
|
||
valid, message = test_github_token(token)
|
||
if not valid:
|
||
print_error(f"Token validation failed: {message}")
|
||
return False
|
||
|
||
print_success(message)
|
||
|
||
# Check permissions
|
||
scopes, error = get_token_permissions(token)
|
||
if error:
|
||
print_warning(f"Could not check permissions: {error}")
|
||
else:
|
||
print_success(f"Token scopes: {', '.join(scopes) if scopes else 'None detected'}")
|
||
|
||
# Check repository access
|
||
access, access_message = check_repository_access(token)
|
||
if access is True:
|
||
print_success(access_message)
|
||
elif access is False:
|
||
print_warning(access_message)
|
||
else:
|
||
print_info(access_message or "Repository access check inconclusive")
|
||
|
||
print_success("Token validation completed")
|
||
return True
|
||
|
||
except Exception as e:
|
||
print_error(f"Error reading token: {e}")
|
||
return False
|
||
|
||
def remove_token():
|
||
"""Remove stored GitHub token"""
|
||
print_colored("\n🗑️ GitHub Token Removal", Colors.BOLD)
|
||
print_colored("=========================", Colors.BOLD)
|
||
|
||
if not TOKEN_FILE.exists():
|
||
print_info("No GitHub token file found")
|
||
return True
|
||
|
||
try:
|
||
# Backup before removal
|
||
backup_file = TOKEN_FILE.with_suffix('.removed')
|
||
TOKEN_FILE.rename(backup_file)
|
||
print_success(f"Token removed and backed up to: {backup_file}")
|
||
|
||
# Try to remove from config
|
||
try:
|
||
if CONFIG_SCRIPT.exists():
|
||
subprocess.run([
|
||
'bash', '-c',
|
||
f'source {CONFIG_SCRIPT} && remove_github_token'
|
||
], check=False, capture_output=True)
|
||
print_success("Token removed from automation configuration")
|
||
except Exception as e:
|
||
print_warning(f"Could not update automation config: {e}")
|
||
|
||
print_success("GitHub token removed successfully")
|
||
return True
|
||
|
||
except Exception as e:
|
||
print_error(f"Error removing token: {e}")
|
||
return False
|
||
|
||
def show_token_status():
|
||
"""Show current token status"""
|
||
print_colored("\n📊 GitHub Token Status", Colors.BOLD)
|
||
print_colored("======================", Colors.BOLD)
|
||
|
||
# Check token file
|
||
print(f"📁 Token file: {TOKEN_FILE}")
|
||
if TOKEN_FILE.exists():
|
||
print_success("Token file exists")
|
||
|
||
# Check permissions
|
||
perms = oct(TOKEN_FILE.stat().st_mode)[-3:]
|
||
if perms == '600':
|
||
print_success(f"File permissions: {perms} (secure)")
|
||
else:
|
||
print_warning(f"File permissions: {perms} (should be 600)")
|
||
|
||
# Quick validation
|
||
try:
|
||
token = TOKEN_FILE.read_text().strip()
|
||
if token:
|
||
if validate_token_format(token):
|
||
print_success("Token format is valid")
|
||
|
||
# Quick API test
|
||
valid, message = test_github_token(token, timeout=10)
|
||
if valid:
|
||
print_success(f"Token is valid: {message}")
|
||
else:
|
||
print_error(f"Token is invalid: {message}")
|
||
else:
|
||
print_error("Token format is invalid")
|
||
else:
|
||
print_error("Token file is empty")
|
||
except Exception as e:
|
||
print_error(f"Error reading token: {e}")
|
||
else:
|
||
print_warning("Token file not found")
|
||
|
||
# Check config integration
|
||
print(f"\n⚙️ Configuration: {CONFIG_SCRIPT}")
|
||
if CONFIG_SCRIPT.exists():
|
||
print_success("Configuration script available")
|
||
else:
|
||
print_warning("Configuration script not found")
|
||
|
||
# Check existing GitHub auth script
|
||
print(f"\n🔐 GitHub auth script: {GITHUB_AUTH_SCRIPT}")
|
||
if GITHUB_AUTH_SCRIPT.exists():
|
||
print_success("GitHub auth script available")
|
||
else:
|
||
print_warning("GitHub auth script not found")
|
||
|
||
def main():
|
||
"""Main CLI interface"""
|
||
parser = argparse.ArgumentParser(
|
||
description="ThrillWiki GitHub PAT Setup Helper",
|
||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||
epilog="""
|
||
Examples:
|
||
%(prog)s setup # Interactive token setup
|
||
%(prog)s validate # Validate existing token
|
||
%(prog)s status # Show token status
|
||
%(prog)s remove # Remove stored token
|
||
%(prog)s --help # Show this help
|
||
|
||
For detailed PAT creation instructions, run: %(prog)s setup
|
||
"""
|
||
)
|
||
|
||
parser.add_argument(
|
||
'command',
|
||
choices=['setup', 'validate', 'status', 'remove', 'help'],
|
||
help='Command to execute'
|
||
)
|
||
|
||
parser.add_argument(
|
||
'--token',
|
||
help='GitHub token to validate (for validate command)'
|
||
)
|
||
|
||
parser.add_argument(
|
||
'--force',
|
||
action='store_true',
|
||
help='Force operation without prompts'
|
||
)
|
||
|
||
if len(sys.argv) == 1:
|
||
parser.print_help()
|
||
sys.exit(1)
|
||
|
||
args = parser.parse_args()
|
||
|
||
try:
|
||
if args.command == 'setup':
|
||
success = interactive_token_setup()
|
||
sys.exit(0 if success else 1)
|
||
|
||
elif args.command == 'validate':
|
||
if args.token:
|
||
# Validate provided token
|
||
print_info("Validating provided token...")
|
||
if validate_token_format(args.token):
|
||
valid, message = test_github_token(args.token)
|
||
if valid:
|
||
print_success(message)
|
||
sys.exit(0)
|
||
else:
|
||
print_error(message)
|
||
sys.exit(1)
|
||
else:
|
||
print_error("Invalid token format")
|
||
sys.exit(1)
|
||
else:
|
||
# Validate existing token
|
||
success = validate_existing_token()
|
||
sys.exit(0 if success else 1)
|
||
|
||
elif args.command == 'status':
|
||
show_token_status()
|
||
sys.exit(0)
|
||
|
||
elif args.command == 'remove':
|
||
if not args.force:
|
||
confirm = input("Are you sure you want to remove the GitHub token? (y/N): ").strip().lower()
|
||
if confirm not in ['y', 'yes']:
|
||
print_info("Operation cancelled")
|
||
sys.exit(0)
|
||
|
||
success = remove_token()
|
||
sys.exit(0 if success else 1)
|
||
|
||
elif args.command == 'help':
|
||
parser.print_help()
|
||
sys.exit(0)
|
||
|
||
except KeyboardInterrupt:
|
||
print("\nOperation cancelled by user")
|
||
sys.exit(1)
|
||
except Exception as e:
|
||
print_error(f"Unexpected error: {e}")
|
||
sys.exit(1)
|
||
|
||
if __name__ == "__main__":
|
||
main() |