[33e88f42] Keine Zusammenfassung angegeben.

Keine Zusammenfassung angegeben.
This commit is contained in:
2026-04-10 21:51:12 +00:00
parent c2f614d7ad
commit 5e0186c534
8 changed files with 460 additions and 22 deletions

View File

@@ -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():

View 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

View File

@@ -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})")

View File

@@ -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