diff --git a/src/config.py b/src/config.py index 1eb65cc..8c2c6c2 100644 --- a/src/config.py +++ b/src/config.py @@ -80,6 +80,7 @@ class Settings(BaseSettings): teams_enabled: bool = False microsoft_app_id: str = "" microsoft_app_secret: str = "" + microsoft_app_tenant_id: str = "botframework.com" # "botframework.com" for multi-tenant, tenant ID for single-tenant model_config = SettingsConfigDict( env_file=".env", diff --git a/src/services/teams_service.py b/src/services/teams_service.py index fecb985..f480f82 100644 --- a/src/services/teams_service.py +++ b/src/services/teams_service.py @@ -21,9 +21,10 @@ _JWKS_TTL = 3600 # 1 hour class TeamsService: """Handles Microsoft Teams bot interactions for post approval notifications.""" - def __init__(self, app_id: str, app_secret: str): + def __init__(self, app_id: str, app_secret: str, tenant_id: str = "botframework.com"): self._app_id = app_id self._app_secret = app_secret + self._tenant_id = tenant_id # ==================== BOT AUTH TOKEN ==================== @@ -37,7 +38,7 @@ class TeamsService: async with httpx.AsyncClient(timeout=15.0) as client: resp = await client.post( - "https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token", + f"https://login.microsoftonline.com/{self._tenant_id}/oauth2/v2.0/token", data={ "grant_type": "client_credentials", "client_id": self._app_id, @@ -214,4 +215,8 @@ class TeamsService: # Module-level singleton — only created when Teams is enabled teams_service: Optional[TeamsService] = None if settings.teams_enabled: - teams_service = TeamsService(settings.microsoft_app_id, settings.microsoft_app_secret) + teams_service = TeamsService( + settings.microsoft_app_id, + settings.microsoft_app_secret, + settings.microsoft_app_tenant_id, + ) diff --git a/src/web/app.py b/src/web/app.py index 966ac9d..6f477ae 100644 --- a/src/web/app.py +++ b/src/web/app.py @@ -2,9 +2,9 @@ from contextlib import asynccontextmanager from pathlib import Path -from fastapi import FastAPI +from fastapi import FastAPI, Request from fastapi.staticfiles import StaticFiles -from fastapi.responses import FileResponse, RedirectResponse +from fastapi.responses import FileResponse, RedirectResponse, JSONResponse from starlette.middleware.base import BaseHTTPMiddleware from starlette.middleware.gzip import GZipMiddleware from loguru import logger @@ -116,6 +116,82 @@ app.add_middleware(GZipMiddleware, minimum_size=500) app.mount("/static", StaticFiles(directory=Path(__file__).parent / "static"), name="static") +def _load_env_file(path: Path) -> dict: + """Load key=value pairs from a simple .env file.""" + data = {} + if not path.exists(): + return data + for line in path.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, val = line.split("=", 1) + data[key.strip()] = val.strip() + return data + + +def _send_contact_email(payload: dict) -> bool: + """Send contact email using landing.env SMTP config.""" + import smtplib + from email.mime.text import MIMEText + from email.mime.multipart import MIMEMultipart + + root = Path(__file__).resolve().parents[3] + env_path = root / "landing" / "landing.env" + if not env_path.exists(): + env_path = root / "landing.env" + env = _load_env_file(env_path) + + host = env.get("SMTP_HOST", "") + port = int(env.get("SMTP_PORT", "587") or 587) + user = env.get("SMTP_USER", "") + password = env.get("SMTP_PASSWORD", "") + from_name = env.get("SMTP_FROM_NAME", "LinkedIn Post System") + to_email = "team@onyva.de" + + if not host or not user or not password: + logger.error("Contact email SMTP not configured (landing.env)") + return False + + subject = "Neue Kontaktanfrage (Landing Page)" + body = ( + f"Name: {payload.get('name','')}\n" + f"E-Mail: {payload.get('email','')}\n" + f"Mitarbeiter: {payload.get('team_size','')}\n" + f"Posts/Monat: {payload.get('posts_per_month','')}\n" + f"Nachricht:\n{payload.get('message','')}\n" + ) + + msg = MIMEMultipart() + msg["From"] = f"{from_name} <{user}>" + msg["To"] = to_email + msg["Subject"] = subject + msg.attach(MIMEText(body, "plain")) + + try: + with smtplib.SMTP(host, port) as server: + server.starttls() + server.login(user, password) + server.sendmail(user, to_email, msg.as_string()) + return True + except Exception as exc: + logger.error(f"Contact email failed: {exc}") + return False + + +@app.post("/api/contact") +async def contact(request: Request): + data = await request.json() + required = ["name", "email", "team_size", "posts_per_month", "message"] + for key in required: + if not data.get(key): + return JSONResponse({"ok": False, "error": f"Missing {key}"}, status_code=400) + ok = _send_contact_email(data) + if not ok: + return JSONResponse({"ok": False, "error": "Email failed"}, status_code=500) + return {"ok": True} + + @app.get("/sw.js", include_in_schema=False) async def service_worker(): """Serve Service Worker from root scope so it can intercept all page requests.""" diff --git a/src/web/templates/user/base.html b/src/web/templates/user/base.html index f067e2e..282260f 100644 --- a/src/web/templates/user/base.html +++ b/src/web/templates/user/base.html @@ -130,13 +130,6 @@ Post erstellen - -
- - Chat Assistent -
- NEU -
Meine Posts diff --git a/src/web/templates/user/chat_create.html b/src/web/templates/user/chat_create.html index a07f58d..5d18833 100644 --- a/src/web/templates/user/chat_create.html +++ b/src/web/templates/user/chat_create.html @@ -233,7 +233,13 @@ aside.collapsed ~ main .chat-fixed-input {
-
+
+ + + + + Zurück +

💬 Chat Assistent

diff --git a/src/web/templates/user/create_post.html b/src/web/templates/user/create_post.html index a5d3feb..db2d142 100644 --- a/src/web/templates/user/create_post.html +++ b/src/web/templates/user/create_post.html @@ -5,6 +5,14 @@
+

Post erstellen

Generiere einen neuen LinkedIn Post mit AI

diff --git a/src/web/templates/user/create_post_select.html b/src/web/templates/user/create_post_select.html new file mode 100644 index 0000000..fad606b --- /dev/null +++ b/src/web/templates/user/create_post_select.html @@ -0,0 +1,43 @@ +{% extends "base.html" %} +{% block title %}Post erstellen - LinkedIn Posts{% endblock %} + +{% block content %} +
+
+

Post erstellen

+

Wähle, wie du deinen Post erstellen möchtest

+
+ + {% if limit_reached %} +
+ Token-Limit erreicht – keine KI-Aktionen mehr heute möglich. Morgen wird das Limit zurückgesetzt. +
+ {% endif %} + + +
+{% endblock %} diff --git a/src/web/user/routes.py b/src/web/user/routes.py index 5007512..06d5f18 100644 --- a/src/web/user/routes.py +++ b/src/web/user/routes.py @@ -1756,8 +1756,37 @@ async def research_page(request: Request): @user_router.get("/create", response_class=HTMLResponse) +async def create_post_select_page(request: Request): + """Post creation selection page - choose wizard or chat.""" + session = require_user_session(request) + if not session: + return RedirectResponse(url="/login", status_code=302) + + # Check token limit for companies/employees + limit_reached = False + limit_message = "" + if session.account_type in ("company", "employee") and session.company_id: + can_create, error_msg, _, _ = await db.check_company_token_limit(UUID(session.company_id)) + limit_reached = not can_create + limit_message = error_msg + + user_id = UUID(session.user_id) + profile_picture = await get_user_avatar(session, user_id) + + return templates.TemplateResponse("create_post_select.html", { + "request": request, + "page": "create", + "session": session, + "user_id": session.user_id, + "limit_reached": limit_reached, + "limit_message": limit_message, + "profile_picture": profile_picture + }) + + +@user_router.get("/create/wizard", response_class=HTMLResponse) async def create_post_page(request: Request): - """Create post page - with limit check for companies.""" + """Create post wizard page - with limit check for companies.""" session = require_user_session(request) if not session: return RedirectResponse(url="/login", status_code=302) diff --git a/teams-app.zip b/teams-app.zip index 4013123..1a42c0e 100644 Binary files a/teams-app.zip and b/teams-app.zip differ