[33e88f42] Keine Zusammenfassung angegeben.
Keine Zusammenfassung angegeben.
This commit is contained in:
@@ -22,6 +22,12 @@ class Job(Base):
|
||||
account_type = Column(String, index=True) # 'kiga' or 'schule'
|
||||
last_updated = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
class GmailToken(Base):
|
||||
__tablename__ = "gmail_tokens"
|
||||
id = Column(Integer, primary_key=True)
|
||||
token_json = Column(String) # Stores the full credentials JSON
|
||||
updated_at = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
|
||||
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
def get_db():
|
||||
|
||||
129
fotograf-de-scraper/backend/gmail_service.py
Normal file
129
fotograf-de-scraper/backend/gmail_service.py
Normal file
@@ -0,0 +1,129 @@
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
import datetime
|
||||
from typing import Optional, List, Dict, Any
|
||||
from google.oauth2.credentials import Credentials
|
||||
from google_auth_oauthlib.flow import Flow
|
||||
from googleapiclient.discovery import build
|
||||
from google.auth.transport.requests import Request
|
||||
from sqlalchemy.orm import Session
|
||||
from database import GmailToken
|
||||
import base64
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
logger = logging.getLogger("gmail-service")
|
||||
|
||||
# Scopes required for sending emails
|
||||
SCOPES = ['https://www.googleapis.com/auth/gmail.send']
|
||||
|
||||
class GmailService:
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
self.client_id = os.getenv("google_fotograf_client_id")
|
||||
self.client_secret = os.getenv("google_fotograf_secret")
|
||||
|
||||
# Redirect URI - must match what was configured in Google Console
|
||||
# We try to detect the public URL, fallback to duckdns
|
||||
self.redirect_uri = os.getenv("GOOGLE_REDIRECT_URI", "https://floke-ai.duckdns.org/fotograf-de-api/api/auth/callback")
|
||||
|
||||
def _get_client_config(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"web": {
|
||||
"client_id": self.client_id,
|
||||
"project_id": "fotograf-tool",
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"client_secret": self.client_secret,
|
||||
"redirect_uris": [self.redirect_uri]
|
||||
}
|
||||
}
|
||||
|
||||
def get_auth_url(self) -> str:
|
||||
flow = Flow.from_client_config(
|
||||
self._get_client_config(),
|
||||
scopes=SCOPES,
|
||||
redirect_uri=self.redirect_uri
|
||||
)
|
||||
auth_url, _ = flow.authorization_url(prompt='consent', access_type='offline')
|
||||
return auth_url
|
||||
|
||||
def handle_callback(self, code: str):
|
||||
flow = Flow.from_client_config(
|
||||
self._get_client_config(),
|
||||
scopes=SCOPES,
|
||||
redirect_uri=self.redirect_uri
|
||||
)
|
||||
flow.fetch_token(code=code)
|
||||
credentials = flow.credentials
|
||||
self._save_token(credentials)
|
||||
return credentials
|
||||
|
||||
def _save_token(self, credentials):
|
||||
token_data = {
|
||||
'token': credentials.token,
|
||||
'refresh_token': credentials.refresh_token,
|
||||
'token_uri': credentials.token_uri,
|
||||
'client_id': credentials.client_id,
|
||||
'client_secret': credentials.client_secret,
|
||||
'scopes': credentials.scopes
|
||||
}
|
||||
|
||||
db_token = self.db.query(GmailToken).first()
|
||||
if not db_token:
|
||||
db_token = GmailToken(token_json=json.dumps(token_data))
|
||||
self.db.add(db_token)
|
||||
else:
|
||||
db_token.token_json = json.dumps(token_data)
|
||||
|
||||
self.db.commit()
|
||||
logger.info("Gmail OAuth token saved to database.")
|
||||
|
||||
def get_credentials(self) -> Optional[Credentials]:
|
||||
db_token = self.db.query(GmailToken).first()
|
||||
if not db_token:
|
||||
return None
|
||||
|
||||
token_data = json.loads(db_token.token_json)
|
||||
creds = Credentials.from_authorized_user_info(token_data, SCOPES)
|
||||
|
||||
if creds and creds.expired and creds.refresh_token:
|
||||
logger.info("Gmail token expired, refreshing...")
|
||||
creds.refresh(Request())
|
||||
self._save_token(creds)
|
||||
|
||||
return creds
|
||||
|
||||
def is_authenticated(self) -> bool:
|
||||
try:
|
||||
creds = self.get_credentials()
|
||||
return creds is not None and creds.valid
|
||||
except Exception as e:
|
||||
logger.error(f"Auth check failed: {e}")
|
||||
return False
|
||||
|
||||
def send_email(self, to: str, subject: str, body_html: str) -> bool:
|
||||
creds = self.get_credentials()
|
||||
if not creds:
|
||||
logger.error("Cannot send email: Not authenticated.")
|
||||
return False
|
||||
|
||||
try:
|
||||
service = build('gmail', 'v1', credentials=creds)
|
||||
message = MIMEText(body_html, 'html')
|
||||
message['to'] = to
|
||||
message['subject'] = subject
|
||||
|
||||
raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
|
||||
|
||||
send_result = service.users().messages().send(
|
||||
userId='me',
|
||||
body={'raw': raw_message}
|
||||
).execute()
|
||||
|
||||
logger.info(f"Email sent to {to}. Message ID: {send_result['id']}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send email to {to}: {e}")
|
||||
return False
|
||||
@@ -746,14 +746,16 @@ def process_reminder_analysis(task_id: str, job_id: str, account_type: str):
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Depends, BackgroundTasks, UploadFile, File, Form
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
|
||||
from typing import List, Dict, Any, Optional
|
||||
from pydantic import BaseModel
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from database import get_db, Job as DBJob, engine, Base
|
||||
import math
|
||||
import uuid
|
||||
from qr_generator import get_calendly_events, overlay_text_on_pdf, get_calendly_event_types
|
||||
from gmail_service import GmailService
|
||||
|
||||
# --- API Endpoints ---
|
||||
|
||||
@@ -914,6 +916,31 @@ async def download_latest_file():
|
||||
async def health_check():
|
||||
return {"status": "ok"}
|
||||
|
||||
# --- Gmail API Endpoints ---
|
||||
|
||||
@app.get("/api/auth/google")
|
||||
async def get_google_auth_url(db: Session = Depends(get_db)):
|
||||
service = GmailService(db)
|
||||
return {"url": service.get_auth_url()}
|
||||
|
||||
@app.get("/api/auth/callback")
|
||||
async def google_auth_callback(code: str, db: Session = Depends(get_db)):
|
||||
service = GmailService(db)
|
||||
try:
|
||||
service.handle_callback(code)
|
||||
# Redirect back to frontend
|
||||
# The frontend lives at /fotograf-de/ through NGINX
|
||||
frontend_url = os.getenv("FRONTEND_URL", "https://floke-ai.duckdns.org/fotograf-de/")
|
||||
return RedirectResponse(url=frontend_url)
|
||||
except Exception as e:
|
||||
logger.error(f"Auth callback failed: {e}")
|
||||
return JSONResponse(status_code=500, content={"message": f"Authentifizierung fehlgeschlagen: {str(e)}"})
|
||||
|
||||
@app.get("/api/gmail/status")
|
||||
async def get_gmail_status(db: Session = Depends(get_db)):
|
||||
service = GmailService(db)
|
||||
return {"authenticated": service.is_authenticated()}
|
||||
|
||||
@app.get("/api/jobs", response_model=List[Dict[str, Any]])
|
||||
async def get_jobs(account_type: str, force_refresh: bool = False, db: Session = Depends(get_db)):
|
||||
logger.info(f"API Request: GET /api/jobs for {account_type} (force_refresh={force_refresh})")
|
||||
@@ -1034,6 +1061,34 @@ async def download_task_csv(task_id: str):
|
||||
logger.error(f"Export error: {e}")
|
||||
raise HTTPException(status_code=500, detail="CSV Export fehlgeschlagen.")
|
||||
|
||||
class BulkEmailRequest(BaseModel):
|
||||
emails: List[Dict[str, str]]
|
||||
|
||||
@app.post("/api/gmail/send-bulk")
|
||||
async def send_bulk_emails(request: BulkEmailRequest, db: Session = Depends(get_db)):
|
||||
service = GmailService(db)
|
||||
if not service.is_authenticated():
|
||||
raise HTTPException(status_code=401, detail="Gmail nicht authentifiziert.")
|
||||
|
||||
success_count = 0
|
||||
failed_emails = []
|
||||
|
||||
for email_data in request.emails:
|
||||
to = email_data.get("to")
|
||||
subject = email_data.get("subject")
|
||||
body = email_data.get("body")
|
||||
|
||||
if service.send_email(to, subject, body):
|
||||
success_count += 1
|
||||
else:
|
||||
failed_emails.append(to)
|
||||
|
||||
return {
|
||||
"total": len(request.emails),
|
||||
"success": success_count,
|
||||
"failed": failed_emails
|
||||
}
|
||||
|
||||
@app.get("/api/jobs/{job_id}/generate-pdf")
|
||||
async def generate_pdf(job_id: str, account_type: str, db: Session = Depends(get_db)):
|
||||
logger.info(f"API Request: Generate PDF for job {job_id} ({account_type})")
|
||||
|
||||
@@ -12,3 +12,6 @@ requests==2.31.0
|
||||
reportlab==4.0.9
|
||||
PyPDF2==3.0.1
|
||||
tzdata
|
||||
google-api-python-client==2.122.0
|
||||
google-auth-httplib2==0.2.0
|
||||
google-auth-oauthlib==1.2.0
|
||||
|
||||
Reference in New Issue
Block a user