mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 09:11:08 -05:00
- Cleaned up and standardized assertions in ApiTestMixin for API response validation. - Updated ASGI settings to use os.environ for setting the DJANGO_SETTINGS_MODULE. - Removed unused imports and improved formatting in settings.py. - Refactored URL patterns in urls.py for better readability and organization. - Enhanced view functions in views.py for consistency and clarity. - Added .flake8 configuration for linting and style enforcement. - Introduced type stubs for django-environ to improve type checking with Pylance.
235 lines
6.3 KiB
Python
Executable File
235 lines
6.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
GitHub OAuth Device Flow Authentication for ThrillWiki CI/CD
|
|
This script implements GitHub's device flow to securely obtain access tokens.
|
|
"""
|
|
|
|
import sys
|
|
import time
|
|
import requests
|
|
import argparse
|
|
from pathlib import Path
|
|
|
|
# GitHub OAuth App Configuration
|
|
CLIENT_ID = "Iv23liOX5Hp75AxhUvIe"
|
|
TOKEN_FILE = ".github-token"
|
|
|
|
|
|
def parse_response(response):
|
|
"""Parse HTTP response and handle errors."""
|
|
if response.status_code in [200, 201]:
|
|
return response.json()
|
|
elif response.status_code == 401:
|
|
print("You are not authorized. Run the `login` command.")
|
|
sys.exit(1)
|
|
else:
|
|
print(f"HTTP {response.status_code}: {response.text}")
|
|
sys.exit(1)
|
|
|
|
|
|
def request_device_code():
|
|
"""Request a device code from GitHub."""
|
|
url = "https://github.com/login/device/code"
|
|
data = {"client_id": CLIENT_ID}
|
|
headers = {"Accept": "application/json"}
|
|
|
|
response = requests.post(url, data=data, headers=headers)
|
|
return parse_response(response)
|
|
|
|
|
|
def request_token(device_code):
|
|
"""Request an access token using the device code."""
|
|
url = "https://github.com/login/oauth/access_token"
|
|
data = {
|
|
"client_id": CLIENT_ID,
|
|
"device_code": device_code,
|
|
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
|
|
}
|
|
headers = {"Accept": "application/json"}
|
|
|
|
response = requests.post(url, data=data, headers=headers)
|
|
return parse_response(response)
|
|
|
|
|
|
def poll_for_token(device_code, interval):
|
|
"""Poll GitHub for the access token after user authorization."""
|
|
print("Waiting for authorization...")
|
|
|
|
while True:
|
|
response = request_token(device_code)
|
|
error = response.get("error")
|
|
access_token = response.get("access_token")
|
|
|
|
if error:
|
|
if error == "authorization_pending":
|
|
# User hasn't entered the code yet
|
|
print(".", end="", flush=True)
|
|
time.sleep(interval)
|
|
continue
|
|
elif error == "slow_down":
|
|
# Polling too fast
|
|
time.sleep(interval + 5)
|
|
continue
|
|
elif error == "expired_token":
|
|
print("\nThe device code has expired. Please run `login` again.")
|
|
sys.exit(1)
|
|
elif error == "access_denied":
|
|
print("\nLogin cancelled by user.")
|
|
sys.exit(1)
|
|
else:
|
|
print(f"\nError: {response}")
|
|
sys.exit(1)
|
|
|
|
# Success! Save the token
|
|
token_path = Path(TOKEN_FILE)
|
|
token_path.write_text(access_token)
|
|
token_path.chmod(0o600) # Read/write for owner only
|
|
|
|
print(f"\nToken saved to {TOKEN_FILE}")
|
|
break
|
|
|
|
|
|
def login():
|
|
"""Initiate the GitHub OAuth device flow login process."""
|
|
print("Starting GitHub authentication...")
|
|
|
|
device_response = request_device_code()
|
|
verification_uri = device_response["verification_uri"]
|
|
user_code = device_response["user_code"]
|
|
device_code = device_response["device_code"]
|
|
interval = device_response["interval"]
|
|
|
|
print(f"\nPlease visit: {verification_uri}")
|
|
print(f"and enter code: {user_code}")
|
|
print("\nWaiting for you to complete authorization in your browser...")
|
|
|
|
poll_for_token(device_code, interval)
|
|
print("Successfully authenticated!")
|
|
return True
|
|
|
|
|
|
def whoami():
|
|
"""Display information about the authenticated user."""
|
|
token_path = Path(TOKEN_FILE)
|
|
|
|
if not token_path.exists():
|
|
print("You are not authorized. Run the `login` command.")
|
|
sys.exit(1)
|
|
|
|
try:
|
|
token = token_path.read_text().strip()
|
|
except Exception as e:
|
|
print(f"Error reading token: {e}")
|
|
print("You may need to run the `login` command again.")
|
|
sys.exit(1)
|
|
|
|
url = "https://api.github.com/user"
|
|
headers = {
|
|
"Accept": "application/vnd.github+json",
|
|
"Authorization": f"Bearer {token}",
|
|
}
|
|
|
|
response = requests.get(url, headers=headers)
|
|
user_data = parse_response(response)
|
|
|
|
print(f"You are authenticated as: {user_data['login']}")
|
|
print(f"Name: {user_data.get('name', 'Not set')}")
|
|
print(f"Email: {user_data.get('email', 'Not public')}")
|
|
|
|
return user_data
|
|
|
|
|
|
def get_token():
|
|
"""Get the current access token if available."""
|
|
token_path = Path(TOKEN_FILE)
|
|
|
|
if not token_path.exists():
|
|
return None
|
|
|
|
try:
|
|
return token_path.read_text().strip()
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def validate_token():
|
|
"""Validate that the current token is still valid."""
|
|
token = get_token()
|
|
if not token:
|
|
return False
|
|
|
|
url = "https://api.github.com/user"
|
|
headers = {
|
|
"Accept": "application/vnd.github+json",
|
|
"Authorization": f"Bearer {token}",
|
|
}
|
|
|
|
try:
|
|
response = requests.get(url, headers=headers)
|
|
return response.status_code == 200
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
def ensure_authenticated():
|
|
"""Ensure user is authenticated, prompting login if necessary."""
|
|
if validate_token():
|
|
return get_token()
|
|
|
|
print("GitHub authentication required.")
|
|
login()
|
|
return get_token()
|
|
|
|
|
|
def logout():
|
|
"""Remove the stored access token."""
|
|
token_path = Path(TOKEN_FILE)
|
|
|
|
if token_path.exists():
|
|
token_path.unlink()
|
|
print("Successfully logged out.")
|
|
else:
|
|
print("You are not currently logged in.")
|
|
|
|
|
|
def main():
|
|
"""Main CLI interface."""
|
|
parser = argparse.ArgumentParser(
|
|
description="GitHub OAuth authentication for ThrillWiki CI/CD"
|
|
)
|
|
parser.add_argument(
|
|
"command",
|
|
choices=["login", "logout", "whoami", "token", "validate"],
|
|
help="Command to execute",
|
|
)
|
|
|
|
if len(sys.argv) == 1:
|
|
parser.print_help()
|
|
sys.exit(1)
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.command == "login":
|
|
login()
|
|
elif args.command == "logout":
|
|
logout()
|
|
elif args.command == "whoami":
|
|
whoami()
|
|
elif args.command == "token":
|
|
token = get_token()
|
|
if token:
|
|
print(token)
|
|
else:
|
|
print("No token available. Run `login` first.")
|
|
sys.exit(1)
|
|
elif args.command == "validate":
|
|
if validate_token():
|
|
print("Token is valid.")
|
|
else:
|
|
print("Token is invalid or missing.")
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|