| import asyncio |
| import json |
| import os |
| import sys |
| from google.oauth2.credentials import Credentials |
| from google_auth_oauthlib.flow import InstalledAppFlow |
|
|
| |
| backend_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
| if backend_dir not in sys.path: |
| sys.path.append(backend_dir) |
|
|
| from dotenv import load_dotenv |
|
|
| |
| from core.token_storage import token_storage |
|
|
| load_dotenv() |
|
|
| |
| SCOPES = [ |
| 'https://www.googleapis.com/auth/gmail.readonly', |
| 'https://www.googleapis.com/auth/gmail.send', |
| 'https://www.googleapis.com/auth/gmail.compose', |
| 'https://www.googleapis.com/auth/gmail.modify' |
| ] |
|
|
| import http.server |
| import socketserver |
| import urllib.parse |
| import webbrowser |
| from google_auth_oauthlib.flow import Flow |
|
|
|
|
| async def reauth_gmail(): |
| print("--- Gmail Re-authentication ---") |
| |
| client_id = os.getenv("GOOGLE_CLIENT_ID") |
| client_secret = os.getenv("GOOGLE_CLIENT_SECRET") |
| |
| if not client_id or not client_secret: |
| print("ERROR: GOOGLE_CLIENT_ID or GOOGLE_CLIENT_SECRET not found in environment.") |
| return |
|
|
| client_config = { |
| "web": { |
| "client_id": client_id, |
| "client_secret": client_secret, |
| "auth_uri": "https://accounts.google.com/o/oauth2/v2/auth", |
| "token_uri": "https://oauth2.googleapis.com/token", |
| } |
| } |
|
|
| |
| redirect_uri = "http://localhost:8080/" |
| |
| flow = Flow.from_client_config( |
| client_config, |
| scopes=SCOPES, |
| redirect_uri=redirect_uri |
| ) |
|
|
| auth_url, _ = flow.authorization_url(prompt='consent', access_type='offline') |
| |
| print(f"\n1. Opening browser for Gmail Authorization...") |
| webbrowser.open(auth_url) |
|
|
| |
| PORT = 8080 |
| CODE = None |
|
|
| class OAuthCallbackHandler(http.server.SimpleHTTPRequestHandler): |
| def do_GET(self): |
| nonlocal CODE |
| query = urllib.parse.urlparse(self.path).query |
| params = urllib.parse.parse_qs(query) |
| |
| if "code" in params: |
| CODE = params["code"][0] |
| self.send_response(200) |
| self.send_header("Content-type", "text/html") |
| self.end_headers() |
| |
| html = """ |
| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Success | Atom Authentication</title> |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet"> |
| <style> |
| :root { |
| --bg-color: #050505; |
| --card-bg: rgba(20, 20, 20, 0.7); |
| --accent-color: #ea4335; |
| --text-primary: #ffffff; |
| --text-secondary: #a0a0a0; |
| } |
| body { |
| margin: 0; |
| padding: 0; |
| font-family: 'Inter', sans-serif; |
| background-color: var(--bg-color); |
| color: var(--text-primary); |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| height: 100vh; |
| overflow: hidden; |
| } |
| .background { |
| position: absolute; |
| width: 100%; |
| height: 100%; |
| background: radial-gradient(circle at 50% 50%, #7f1d1d 0%, #050505 70%); |
| z-index: -1; |
| filter: blur(80px); |
| opacity: 0.5; |
| } |
| .card { |
| background: var(--card-bg); |
| backdrop-filter: blur(12px); |
| -webkit-backdrop-filter: blur(12px); |
| border: 1px solid rgba(255, 255, 255, 0.1); |
| border-radius: 24px; |
| padding: 48px; |
| text-align: center; |
| max-width: 400px; |
| width: 90%; |
| box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5); |
| transform: translateY(0); |
| animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1); |
| } |
| @keyframes slideUp { |
| from { transform: translateY(20px); opacity: 0; } |
| to { transform: translateY(0); opacity: 1; } |
| } |
| .icon-container { |
| width: 80px; |
| height: 80px; |
| background: rgba(234, 67, 53, 0.1); |
| border-radius: 50%; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| margin: 0 auto 24px; |
| } |
| .checkmark { |
| width: 40px; |
| height: 40px; |
| stroke: var(--accent-color); |
| stroke-width: 3; |
| stroke-linecap: round; |
| stroke-linejoin: round; |
| fill: none; |
| stroke-dasharray: 100; |
| stroke-dashoffset: 100; |
| animation: dash 0.8s ease-in-out forwards 0.3s; |
| } |
| @keyframes dash { |
| to { stroke-dashoffset: 0; } |
| } |
| h1 { |
| font-size: 28px; |
| font-weight: 700; |
| margin: 0 0 12px; |
| letter-spacing: -0.02em; |
| } |
| p { |
| font-size: 16px; |
| color: var(--text-secondary); |
| line-height: 1.5; |
| margin-bottom: 32px; |
| } |
| .status-tag { |
| display: inline-block; |
| padding: 6px 12px; |
| background: rgba(34, 197, 94, 0.1); |
| color: #22c55e; |
| font-weight: 600; |
| font-size: 12px; |
| border-radius: 100px; |
| text-transform: uppercase; |
| letter-spacing: 0.05em; |
| margin-bottom: 16px; |
| } |
| .footer { |
| position: absolute; |
| bottom: 40px; |
| font-size: 12px; |
| color: rgba(255, 255, 255, 0.3); |
| letter-spacing: 0.1em; |
| text-transform: uppercase; |
| } |
| </style> |
| </head> |
| <body> |
| <div class="background"></div> |
| <div class="card"> |
| <div class="status-tag">Connected</div> |
| <div class="icon-container"> |
| <svg class="checkmark" viewBox="0 0 52 52"> |
| <path d="M14.1 27.2l7.1 7.2 16.7-16.8"/> |
| </svg> |
| </div> |
| <h1>Gmail authenticated</h1> |
| <p>Your Gmail account is now successfully linked to Atom. You can close this tab and return to the terminal.</p> |
| </div> |
| <div class="footer">Powered by Atom AI</div> |
| </body> |
| </html> |
| """ |
| self.wfile.write(html.encode()) |
| else: |
| self.send_response(400) |
| self.end_headers() |
| self.wfile.write(b"<h1>Authentication Failed!</h1>") |
|
|
| print(f"2. Waiting for callback on {redirect_uri}...") |
| |
| with socketserver.TCPServer(("127.0.0.1", PORT), OAuthCallbackHandler) as httpd: |
| httpd.handle_request() |
|
|
| if CODE: |
| print(f"3. Exchanging code for tokens...") |
| flow.fetch_token(code=CODE) |
| creds = flow.credentials |
|
|
| |
| token_data = { |
| "access_token": creds.token, |
| "refresh_token": creds.refresh_token, |
| "token_uri": creds.token_uri, |
| "client_id": creds.client_id, |
| "client_secret": creds.client_secret, |
| "scopes": creds.scopes, |
| "token_type": "Bearer" |
| } |
|
|
| |
| token_storage.save_token("google", token_data) |
| print("\n✅ SUCCESS: Gmail token updated and saved to oauth_tokens.json") |
| else: |
| print("\n❌ Failed to get authorization code.") |
|
|
| if __name__ == "__main__": |
| asyncio.run(reauth_gmail()) |
|
|