From 005059be84a10ba93145be3e9840a9a1be69108c Mon Sep 17 00:00:00 2001 From: Ruben Fischer Date: Mon, 23 Feb 2026 16:13:14 +0100 Subject: [PATCH] merged create and chat create in one tab --- src/config.py | 1 + src/services/teams_service.py | 11 ++- src/web/app.py | 80 +++++++++++++++++- src/web/templates/user/base.html | 7 -- src/web/templates/user/chat_create.html | 8 +- src/web/templates/user/create_post.html | 8 ++ .../templates/user/create_post_select.html | 43 ++++++++++ src/web/user/routes.py | 31 ++++++- teams-app.zip | Bin 1018 -> 1144 bytes 9 files changed, 175 insertions(+), 14 deletions(-) create mode 100644 src/web/templates/user/create_post_select.html 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 401312339d0166df58a48eecdc8899d6a874ae7f..1a42c0e7aa88dba5785447c77e9a612bae15633a 100644 GIT binary patch delta 997 zcmeyx{)3}Fz?+#xgn@y9gF&VvBG9^_@{^(22brXEb zw`0+snW>vo>+VjjXEW%YqH|$#x#HwUQrZgT+|w>J+a+H(no!K=w>i^gBhP_dLRV{8 zL$%CQ{lzy+`u|MY=NbMwQ*qgA%RTo?^qil@mAz-=&s?W4Bj@YNhq|Ym%L4?gc5M8# zC%^L-gX*g@EP2n)R@K+#ZhZLYiD#eg z2_M#q#$|pxeZyJxZ0!71R$f1q8f#7Rw&*st9KCfk(!TtE;)$j4IuH897esc7@%1fC zd+RlS`LCSsF;)rAr__tqX`e3lk7?uXtb4P<`sFm%cZ*G4Ep}CabAuCCeBPQf_2%}1C5L}%DSXoAIlYT5^vA5p z2NygPI_>XPXj5MCkel`BlZS$+X~qAD(YoWnG}1oVf=R#r)8~>xo|Q8`e|zQVr{>Pc z=6>;+?d|8R^X_E7y8HTp(u03VqRx@k$+xyYjo*K_V9%t8qXy2c4~4o9>#*@Bw)v>B z#73?5PN1!fOd`y6GLT{l>_^;LgIpzzY=3P0Y(oOD!(Z%PP*#n;Lw&@3Mi& z-S66K4op(X4PEy3%>{-_9BrEt+E|kwT9}_*TIaGa&+Y1p|9!I0FFGy|w0?c3_+0I; z%wrLsX1VZosbAMwIXC--)p_p>$60JS%K{>tmle%4&|;q}r}^L2ZF!)}PUdwQ`QkSp zY3-Ljai3|1tC!e|ld~sy7WRrxsAlkf;W*#$Mbd`7EYEM+EQ(-D2=2_<*`_6Wd(!gN zN1K+{Y`wGW_1QfFpU(>2%d4Kgu}HuAQ$xE+xPVXDuPdMWo*s19;E=r&U3qi6OKpQt zX&RIH=QQU18*da9R!yA${fOi916sF2zkexOwdb)~T=~1Gm#^RLUT`ZyoX4*GQ1sb- z($y=LRL&7mov`7?4vS;L#RP+B#YE0diWcOI3^9@lCCQn%ug%v|||?brU@bahwOW63Btei0w5yFTpwwT~`bkDdDD=LVkB&^l=`52UZp{31=tZ0N{cHc`e`MD= z$a7JzV&2!4PK;cdf0nH@`ak)P%Z1+QD;rL%H_&;_7xFOM&#ifyx6Xa(qaQCl)ZeeH zk@oz407?SM*%!X69GEZym>3v1fpl_yPJWSIL0z4vfrc3M& zGn#ol*zTT8OixG<`d(7VvvS7gZ<`h>G&db+AI!PT?m1~dekvqmSGlwhPowa>q!qyExrd~-mWL0m_Y(04S>iN{U!A`;q zsIF@>sl5;gbX5`%a|3aHX-Q6IUMhC?aVeZR$fnZ0deuG#O<#ZU;}g0p^UrG7icphlL#~JtOK+j3>tvh z2`wX`Yef%Kh(-p621Z3-FvEiwT^qV@5!&_xwITUEz?+o~B*_eftAVs7Gl&NO(i57U