mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-21 06:31:09 -05:00
feat: complete monorepo structure with frontend and shared resources
- Add complete backend/ directory with full Django application - Add frontend/ directory with Vite + TypeScript setup ready for Next.js - Add comprehensive shared/ directory with: - Complete documentation and memory-bank archives - Media files and avatars (letters, park/ride images) - Deployment scripts and automation tools - Shared types and utilities - Add architecture/ directory with migration guides - Configure pnpm workspace for monorepo development - Update .gitignore to exclude .django_tailwind_cli/ build artifacts - Preserve all historical documentation in shared/docs/memory-bank/ - Set up proper structure for full-stack development with shared resources
This commit is contained in:
234
shared/scripts/github-auth.py
Executable file
234
shared/scripts/github-auth.py
Executable file
@@ -0,0 +1,234 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user