From 77c08791425444b6cc5126366d10027045c5bb88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=B3czi=20Levente?= Date: Sun, 7 Jun 2026 10:58:31 +0300 Subject: [PATCH 01/16] #13: Agent BE FE connection --- .../galacticview_bot/nodes/tool_node.py | 2 +- .../galacticview_bot/tools/search.py | 2 + apps/agent-backend/poetry.lock | 7 +++- apps/agent-backend/pyproject.toml | 1 + apps/agent-backend/server/dependencies.py | 40 +++++++++++++++++++ .../agent-backend/server/dto/chat_type_out.py | 8 +++- apps/agent-backend/server/serve.py | 33 ++++++++++----- apps/agent-backend/server/service.py | 5 ++- .../public/locales/en/translation.json | 4 +- .../src/components/AgentChatWidget.module.css | 6 +++ .../src/components/AgentChatWidget.tsx | 29 +++++++++++--- apps/frontend/src/hooks/useAgentChat.ts | 40 +++++-------------- apps/frontend/vite.config.ts | 10 +++++ 13 files changed, 134 insertions(+), 53 deletions(-) create mode 100644 apps/agent-backend/server/dependencies.py diff --git a/apps/agent-backend/galacticview_bot/nodes/tool_node.py b/apps/agent-backend/galacticview_bot/nodes/tool_node.py index 4de7767..32122ec 100644 --- a/apps/agent-backend/galacticview_bot/nodes/tool_node.py +++ b/apps/agent-backend/galacticview_bot/nodes/tool_node.py @@ -4,7 +4,7 @@ from galacticview_bot.core.state import AgentState from galacticview_bot.tools.search import tavily_search_tool -tools = [tavily_search_tool] +tools = [tool for tool in [tavily_search_tool] if tool is not None] def custom_tool_node(state: AgentState) -> dict: """ diff --git a/apps/agent-backend/galacticview_bot/tools/search.py b/apps/agent-backend/galacticview_bot/tools/search.py index 142ef27..735b50b 100644 --- a/apps/agent-backend/galacticview_bot/tools/search.py +++ b/apps/agent-backend/galacticview_bot/tools/search.py @@ -10,6 +10,8 @@ class TavilyInput(BaseModel): query: str = Field(description="The search query to find information on the internet.") +tavily_search_tool = None + if os.getenv("TAVILY_API_KEY"): logger.info("TAVILY_API_KEY found. Initializing Tavily search tool.") tavily_search_tool = TavilySearch( diff --git a/apps/agent-backend/poetry.lock b/apps/agent-backend/poetry.lock index 671fe13..12fab2a 100644 --- a/apps/agent-backend/poetry.lock +++ b/apps/agent-backend/poetry.lock @@ -435,6 +435,11 @@ python-versions = ">=3.8" groups = ["main"] markers = "platform_python_implementation != \"CPython\"" files = [ + {file = "brotlicffi-1.2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b13fb476a96f02e477a506423cb5e7bc21e0e3ac4c060c20ba31c44056e38c68"}, + {file = "brotlicffi-1.2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17db36fb581f7b951635cd6849553a95c6f2f53c1a707817d06eae5aeff5f6af"}, + {file = "brotlicffi-1.2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40190192790489a7b054312163d0ce82b07d1b6e706251036898ce1684ef12e9"}, + {file = "brotlicffi-1.2.0.0-cp314-cp314t-win32.whl", hash = "sha256:a8079e8ecc32ecef728036a1d9b7105991ce6a5385cf51ee8c02297c90fb08c2"}, + {file = "brotlicffi-1.2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:ca90c4266704ca0a94de8f101b4ec029624273380574e4cf19301acfa46c61a0"}, {file = "brotlicffi-1.2.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:9458d08a7ccde8e3c0afedbf2c70a8263227a68dea5ab13590593f4c0a4fd5f4"}, {file = "brotlicffi-1.2.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:84e3d0020cf1bd8b8131f4a07819edee9f283721566fe044a20ec792ca8fd8b7"}, {file = "brotlicffi-1.2.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:33cfb408d0cff64cd50bef268c0fed397c46fbb53944aa37264148614a62e990"}, @@ -6704,4 +6709,4 @@ dev = ["mypy", "pytest", "ruff", "types-requests", "types-urllib3"] [metadata] lock-version = "2.1" python-versions = ">=3.12,<3.14" -content-hash = "9314843362ffd92157a7a805e90c6969885e6e7a83e1d69d1678f3ea9530e0e0" +content-hash = "cb2a46322fb42fd1c0d86d28d465601a27ac979f52bd3c957825c8744aed0a64" diff --git a/apps/agent-backend/pyproject.toml b/apps/agent-backend/pyproject.toml index cfdd9c9..e99a9e8 100644 --- a/apps/agent-backend/pyproject.toml +++ b/apps/agent-backend/pyproject.toml @@ -20,6 +20,7 @@ dependencies = [ "loguru (>=0.7.3,<0.8.0)", "fastapi[standard] (>=0.123.0,<0.124.0)", "slowapi (>=0.1.9,<0.2.0)", + "httpx (>=0.28.1,<0.29.0)", "langchain-community (>=0.4.1,<0.5.0)", "langchain-openai (>=1.2.1,<2.0.0)", ] diff --git a/apps/agent-backend/server/dependencies.py b/apps/agent-backend/server/dependencies.py new file mode 100644 index 0000000..65f937b --- /dev/null +++ b/apps/agent-backend/server/dependencies.py @@ -0,0 +1,40 @@ +import os + +import httpx +from fastapi import HTTPException, Request + +CORE_BACKEND_URL = os.getenv("CORE_BACKEND_URL", "http://localhost:8001") + + +async def _verify_session(session_cookie: str) -> dict: + """ + Forwards the session cookie to the core-backend and returns the user dict. + Raises HTTPException on any auth failure. + """ + async with httpx.AsyncClient() as client: + try: + response = await client.get( + f"{CORE_BACKEND_URL}/auth/me", + cookies={"session": session_cookie}, + ) + except httpx.RequestError as exc: + raise HTTPException(status_code=502, detail="Auth service unavailable") from exc + + if response.status_code == 401: + raise HTTPException(status_code=401, detail="Invalid or expired session") + + if response.status_code != 200: + raise HTTPException(status_code=502, detail="Auth service returned an error") + + return response.json().get("user", {}) + + +async def require_auth(request: Request) -> None: + """ + Lightweight auth guard — verifies the session cookie is present and valid. + """ + session_cookie = request.cookies.get("session") + if not session_cookie: + raise HTTPException(status_code=401, detail="Not authenticated") + + await _verify_session(session_cookie) diff --git a/apps/agent-backend/server/dto/chat_type_out.py b/apps/agent-backend/server/dto/chat_type_out.py index 4adf070..dc535c8 100644 --- a/apps/agent-backend/server/dto/chat_type_out.py +++ b/apps/agent-backend/server/dto/chat_type_out.py @@ -1,7 +1,11 @@ -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict, Field + class ChatTypeOut(BaseModel): """Data transfer object for chat type output.""" + + model_config = ConfigDict(populate_by_name=True) + title: str content: str - key_metrics: list[str] + key_metrics: list[str] = Field(serialization_alias="keyMetrics") diff --git a/apps/agent-backend/server/serve.py b/apps/agent-backend/server/serve.py index 3711508..3f6a8af 100644 --- a/apps/agent-backend/server/serve.py +++ b/apps/agent-backend/server/serve.py @@ -1,16 +1,19 @@ -from fastapi import FastAPI, Request -from fastapi.middleware.cors import CORSMiddleware -from slowapi import Limiter, _rate_limit_exceeded_handler -from slowapi.errors import RateLimitExceeded -import uvicorn - import os -from .service import chat_ask_question +from dotenv import load_dotenv -from .dto import ChatTypeIn, ChatTypeOut +load_dotenv() +import uvicorn +from fastapi import Depends, FastAPI, Request +from fastapi.middleware.cors import CORSMiddleware from loguru import logger +from slowapi import Limiter, _rate_limit_exceeded_handler +from slowapi.errors import RateLimitExceeded + +from .dependencies import require_auth +from .dto import ChatTypeIn, ChatTypeOut +from .service import chat_ask_question def get_real_ip(request: Request) -> str: """ @@ -31,9 +34,15 @@ def get_real_ip(request: Request) -> str: app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) # type: ignore +ALLOWED_ORIGINS = [ + origin.strip() + for origin in os.getenv("ALLOWED_ORIGINS", "http://localhost:5173").split(",") + if origin.strip() +] + app.add_middleware( CORSMiddleware, - allow_origins=["*"], + allow_origins=ALLOWED_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], @@ -42,7 +51,11 @@ def get_real_ip(request: Request) -> str: @app.post("/chat") @limiter.limit("7/minute") -def chat_endpoint(request: Request, body: ChatTypeIn) -> ChatTypeOut: +async def chat_endpoint( + request: Request, + body: ChatTypeIn, + _: None = Depends(require_auth), +) -> ChatTypeOut: """ Process chat questions using the agent and return structured responses. Rate limited to 7 requests per minute per IP. diff --git a/apps/agent-backend/server/service.py b/apps/agent-backend/server/service.py index 63e170a..918e045 100644 --- a/apps/agent-backend/server/service.py +++ b/apps/agent-backend/server/service.py @@ -6,6 +6,8 @@ from langchain_core.messages import HumanMessage import json +import uuid + def chat_ask_question(chat_input: ChatTypeIn) -> ChatTypeOut: """ @@ -20,8 +22,7 @@ def chat_ask_question(chat_input: ChatTypeIn) -> ChatTypeOut: } try: - # Hardcoded to a single thread ID to maintain context across all requests for initial testing. - thread_id = "initial-single-context-thread" + thread_id = str(uuid.uuid4()) config = { "configurable": {"thread_id": thread_id}, diff --git a/apps/frontend/public/locales/en/translation.json b/apps/frontend/public/locales/en/translation.json index 8fc4dad..cf8d7ad 100644 --- a/apps/frontend/public/locales/en/translation.json +++ b/apps/frontend/public/locales/en/translation.json @@ -112,7 +112,9 @@ }, "agent": { "welcome": "\uD83D\uDC4B How can I help you explore the cosmos today?", - "placeholder": "Ask me anything about space..." + "placeholder": "Ask me anything about space...", + "loginRequired": "Please log in to ask the Galactic Agent.", + "oneQuestionOnly": "Refresh the page to ask a new question." }, "loading": "Loading..." } \ No newline at end of file diff --git a/apps/frontend/src/components/AgentChatWidget.module.css b/apps/frontend/src/components/AgentChatWidget.module.css index 93d3215..01e095a 100644 --- a/apps/frontend/src/components/AgentChatWidget.module.css +++ b/apps/frontend/src/components/AgentChatWidget.module.css @@ -93,6 +93,12 @@ padding: 0.75rem; } +.authHint { + font-size: 0.875rem; + color: #6c757d; + margin-bottom: 0.5rem; +} + .inputGroup { position: relative; } diff --git a/apps/frontend/src/components/AgentChatWidget.tsx b/apps/frontend/src/components/AgentChatWidget.tsx index f701fdc..4fedb47 100644 --- a/apps/frontend/src/components/AgentChatWidget.tsx +++ b/apps/frontend/src/components/AgentChatWidget.tsx @@ -3,8 +3,10 @@ import * as React from 'react'; import { Button, Card, CloseButton, Form, InputGroup } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { IoSend } from 'react-icons/io5'; +import { Link } from 'react-router-dom'; import TextareaAutosize from 'react-textarea-autosize'; +import { useAuth } from '../context/AuthContext.tsx'; import useAgentChat from '../hooks/useAgentChat.ts'; import style from './AgentChatWidget.module.css'; @@ -12,12 +14,15 @@ const MAX_MESSAGE_LENGTH = 512; function AgentChatWidget() { const { t } = useTranslation(); + const { isAuthenticated } = useAuth(); const [isOpen, setIsOpen] = useState(false); const [inputMessage, setInputMessage] = useState(''); - const { messages, isLoading, sendMessage } = useAgentChat(); + const { messages, isLoading, questionSent, sendMessage } = useAgentChat(); const messagesEndRef = useRef(null); + const inputDisabled = !isAuthenticated || questionSent || isLoading; + useEffect(() => { if (messagesEndRef.current) { messagesEndRef.current.scrollIntoView({ behavior: 'smooth' }); @@ -31,7 +36,7 @@ function AgentChatWidget() { const handleSendMessage = async (e: React.FormEvent) => { e.preventDefault(); - if (inputMessage.trim() === '' || inputMessage.length > MAX_MESSAGE_LENGTH) return; + if (inputDisabled || inputMessage.trim() === '' || inputMessage.length > MAX_MESSAGE_LENGTH) return; setInputMessage(''); try { @@ -42,12 +47,19 @@ function AgentChatWidget() { }; const handleKeyDown = (e: React.KeyboardEvent) => { + if (inputDisabled) return; if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); void handleSendMessage(e); } }; + const placeholder = !isAuthenticated + ? t('agent.loginRequired') + : questionSent + ? t('agent.oneQuestionOnly') + : t('agent.placeholder'); + return (
{isOpen ? ( @@ -77,18 +89,25 @@ function AgentChatWidget() {
-
void handleSendMessage(e)} onKeyDown={handleKeyDown}> + {!isAuthenticated ? ( +

+ {t('agent.loginRequired')} {t('navigation.login')} +

+ ) : null} + void handleSendMessage(e)}> setInputMessage(e.target.value)} + onKeyDown={handleKeyDown} + disabled={inputDisabled} /> - diff --git a/apps/frontend/src/hooks/useAgentChat.ts b/apps/frontend/src/hooks/useAgentChat.ts index 390988a..18b497f 100644 --- a/apps/frontend/src/hooks/useAgentChat.ts +++ b/apps/frontend/src/hooks/useAgentChat.ts @@ -1,7 +1,8 @@ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { v4 as uuidv4 } from 'uuid'; import { sendPromptToAgent } from '../api/agent.api.ts'; +import { getApiErrorMessage } from '../utils/authUtils'; export interface ChatMessage { id: string; @@ -14,28 +15,10 @@ export interface ChatMessage { error?: string; } -const SESSION_STORAGE_KEY = 'agent_chat_messages'; - const useAgentChat = () => { - const [messages, setMessages] = useState(() => { - try { - const storedMessages = sessionStorage.getItem(SESSION_STORAGE_KEY); - return storedMessages ? (JSON.parse(storedMessages) as ChatMessage[]) : []; - } catch (error) { - console.error('Failed to parse stored messages:', error); - return []; - } - }); - - useEffect(() => { - try { - sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(messages)); - } catch (error) { - console.error('Failed to store messages:', error); - } - }, [messages]); - + const [messages, setMessages] = useState([]); const [isLoading, setIsLoading] = useState(false); + const [questionSent, setQuestionSent] = useState(false); const sendMessage = async (msg: string) => { const userMessage: ChatMessage = { @@ -44,7 +27,8 @@ const useAgentChat = () => { sender: 'user', }; - setMessages((prev) => [...prev, userMessage]); + setMessages([userMessage]); + setQuestionSent(true); setIsLoading(true); try { @@ -63,10 +47,10 @@ const useAgentChat = () => { } catch (error) { const errorMessage: ChatMessage = { id: uuidv4(), - message: 'Sorry, there was an error processing your request.', + message: getApiErrorMessage(error, 'Sorry, there was an error processing your request.'), sender: 'agent', isError: true, - error: (error as Error).message, + error: getApiErrorMessage(error, 'Sorry, there was an error processing your request.'), }; setMessages((prev) => [...prev, errorMessage]); } finally { @@ -74,17 +58,11 @@ const useAgentChat = () => { } }; - const clearChat = () => { - setMessages([]); - sessionStorage.removeItem(SESSION_STORAGE_KEY); - setIsLoading(false); - }; - return { messages, isLoading, + questionSent, sendMessage, - clearChat, }; }; diff --git a/apps/frontend/vite.config.ts b/apps/frontend/vite.config.ts index b3b1cc5..5137f25 100644 --- a/apps/frontend/vite.config.ts +++ b/apps/frontend/vite.config.ts @@ -4,4 +4,14 @@ import { defineConfig } from 'vite'; export default defineConfig({ base: '/', plugins: [react()], + server: { + proxy: { + '/api/agent': { + // Use 127.0.0.1 — on macOS, localhost can resolve to ::1 and hit a different service on :8000 + target: 'http://127.0.0.1:8000', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api\/agent/, '') || '/', + }, + }, + }, }); From 9f27d79f2fee43575ec223c8dad1cf59d1e4b931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=B3czi=20Levente?= Date: Sun, 7 Jun 2026 11:02:07 +0300 Subject: [PATCH 02/16] #13: Lint fix --- apps/agent-backend/server/dependencies.py | 3 +++ apps/agent-backend/server/serve.py | 4 ---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/agent-backend/server/dependencies.py b/apps/agent-backend/server/dependencies.py index 65f937b..9d7f676 100644 --- a/apps/agent-backend/server/dependencies.py +++ b/apps/agent-backend/server/dependencies.py @@ -1,8 +1,11 @@ import os import httpx +from dotenv import load_dotenv from fastapi import HTTPException, Request +load_dotenv() + CORE_BACKEND_URL = os.getenv("CORE_BACKEND_URL", "http://localhost:8001") diff --git a/apps/agent-backend/server/serve.py b/apps/agent-backend/server/serve.py index 3f6a8af..e1db5e0 100644 --- a/apps/agent-backend/server/serve.py +++ b/apps/agent-backend/server/serve.py @@ -1,9 +1,5 @@ import os -from dotenv import load_dotenv - -load_dotenv() - import uvicorn from fastapi import Depends, FastAPI, Request from fastapi.middleware.cors import CORSMiddleware From 682b802744646d3cd9152f74fbedd362161ae230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=B3czi=20Levente?= Date: Sun, 7 Jun 2026 11:04:01 +0300 Subject: [PATCH 03/16] #13: Fix vulnerabilities --- apps/frontend/package.json | 4 ++-- apps/frontend/yarn.lock | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 304dbd8..19de951 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -27,8 +27,8 @@ "react-i18next": "^16.5.0", "react-icons": "^5.5.0", "react-medium-image-zoom": "^5.4.0", - "react-router": "^7.9.5", - "react-router-dom": "^7.9.5", + "react-router": "^7.15.0", + "react-router-dom": "^7.15.0", "react-textarea-autosize": "^8.5.9", "uuid": "^14.0.0" }, diff --git a/apps/frontend/yarn.lock b/apps/frontend/yarn.lock index d78586f..0aad725 100644 --- a/apps/frontend/yarn.lock +++ b/apps/frontend/yarn.lock @@ -3274,7 +3274,7 @@ react-router-dom@^7.9.5: dependencies: react-router "7.14.2" -react-router@7.14.2, react-router@^7.9.5: +react-router@7.14.2: version "7.14.2" resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.14.2.tgz#d86e5b01049365b2c982363ebd2baa4928824603" integrity sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw== @@ -3282,6 +3282,14 @@ react-router@7.14.2, react-router@^7.9.5: cookie "^1.0.1" set-cookie-parser "^2.6.0" +react-router@^7.9.5: + version "7.17.0" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.17.0.tgz#88bbe817c6e37ab36faf140623b5d4678bf81e41" + integrity "sha1-iLvoF8bjerNvrxQGI7XUZ4v4HkE= sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ==" + dependencies: + cookie "^1.0.1" + set-cookie-parser "^2.6.0" + react-stately@3.46.0: version "3.46.0" resolved "https://registry.yarnpkg.com/react-stately/-/react-stately-3.46.0.tgz#9ce293b765c246c398a1765d6290acd0a77caa49" From c467f07f3ac90600c7c875a148d437a0e7e7ab7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=B3czi=20Levente?= Date: Sun, 7 Jun 2026 11:13:21 +0300 Subject: [PATCH 04/16] #13: Fix --- apps/frontend/yarn.lock | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/apps/frontend/yarn.lock b/apps/frontend/yarn.lock index 0aad725..b3a904f 100644 --- a/apps/frontend/yarn.lock +++ b/apps/frontend/yarn.lock @@ -3267,25 +3267,17 @@ react-refresh@^0.18.0: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.18.0.tgz#2dce97f4fe932a4d8142fa1630e475c1729c8062" integrity sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw== -react-router-dom@^7.9.5: - version "7.14.2" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-7.14.2.tgz#0b043c1534fe58596771b82a318a7e4c2e5f1279" - integrity sha512-YZcM5ES8jJSM+KrJ9BdvHHqlnGTg5tH3sC5ChFRj4inosKctdyzBDhOyyHdGk597q2OT6NTrCA1OvB/YDwfekQ== - dependencies: - react-router "7.14.2" - -react-router@7.14.2: - version "7.14.2" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.14.2.tgz#d86e5b01049365b2c982363ebd2baa4928824603" - integrity sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw== +react-router-dom@^7.15.0: + version "7.17.0" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-7.17.0.tgz#e77527b4b7862f7b47ff26dd5b9315fb897b82a7" + integrity sha512-fyU2yjGups/hE6Xz0I5ZYbVL8Gx29eCjgpHaRaTaVU+OOAdfRX05KsvyRm0GO8YQwOkhpU3MurW1jyMUJn+zSw== dependencies: - cookie "^1.0.1" - set-cookie-parser "^2.6.0" + react-router "7.17.0" -react-router@^7.9.5: +react-router@7.17.0, react-router@^7.15.0: version "7.17.0" resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.17.0.tgz#88bbe817c6e37ab36faf140623b5d4678bf81e41" - integrity "sha1-iLvoF8bjerNvrxQGI7XUZ4v4HkE= sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ==" + integrity sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ== dependencies: cookie "^1.0.1" set-cookie-parser "^2.6.0" From 3c426d88777667cbe1ef0a8c3a54cc13b930734e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=B3czi=20Levente?= Date: Sun, 7 Jun 2026 11:13:30 +0300 Subject: [PATCH 05/16] #13: Fix cors --- apps/agent-backend/server/cors.py | 33 +++++++++++++++++++++++ apps/agent-backend/server/serve.py | 27 +++++-------------- apps/blogpost-backend/app/main.py | 34 +++++++++++++++++------- apps/core-backend/app/api/routes/auth.py | 12 ++++++--- apps/core-backend/app/main.py | 31 ++++++++++++++++----- apps/frontend/vite.config.ts | 10 ------- 6 files changed, 97 insertions(+), 50 deletions(-) create mode 100644 apps/agent-backend/server/cors.py diff --git a/apps/agent-backend/server/cors.py b/apps/agent-backend/server/cors.py new file mode 100644 index 0000000..149f939 --- /dev/null +++ b/apps/agent-backend/server/cors.py @@ -0,0 +1,33 @@ +import os + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +_DEV_ORIGIN_REGEX = r"^https?://(localhost|127\.0\.0\.1)(:\d+)?$" +_DEFAULT_ORIGINS = "http://localhost:5173,http://127.0.0.1:5173" + + +def add_cors_middleware(app: FastAPI) -> None: + """Apply CORS. In dev, allow any localhost port; in prod, use ALLOWED_ORIGINS.""" + if os.getenv("ENVIRONMENT", "prod") == "dev": + app.add_middleware( + CORSMiddleware, + allow_origin_regex=_DEV_ORIGIN_REGEX, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + return + + allowed_origins = [ + origin.strip() + for origin in os.getenv("ALLOWED_ORIGINS", _DEFAULT_ORIGINS).split(",") + if origin.strip() + ] + app.add_middleware( + CORSMiddleware, + allow_origins=allowed_origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) diff --git a/apps/agent-backend/server/serve.py b/apps/agent-backend/server/serve.py index e1db5e0..8a73cad 100644 --- a/apps/agent-backend/server/serve.py +++ b/apps/agent-backend/server/serve.py @@ -2,12 +2,12 @@ import uvicorn from fastapi import Depends, FastAPI, Request -from fastapi.middleware.cors import CORSMiddleware from loguru import logger from slowapi import Limiter, _rate_limit_exceeded_handler from slowapi.errors import RateLimitExceeded -from .dependencies import require_auth +from .dependencies import require_auth # loads .env via load_dotenv() +from .cors import add_cors_middleware from .dto import ChatTypeIn, ChatTypeOut from .service import chat_ask_question @@ -30,19 +30,7 @@ def get_real_ip(request: Request) -> str: app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) # type: ignore -ALLOWED_ORIGINS = [ - origin.strip() - for origin in os.getenv("ALLOWED_ORIGINS", "http://localhost:5173").split(",") - if origin.strip() -] - -app.add_middleware( - CORSMiddleware, - allow_origins=ALLOWED_ORIGINS, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) +add_cors_middleware(app) @app.post("/chat") @@ -68,12 +56,11 @@ def main() -> None: env = os.getenv("ENVIRONMENT", "prod") reload = env == "dev" - host = "127.0.0.1" - if env == "prod": - host = "0.0.0.0" + host = "0.0.0.0" - logger.info(f"Starting server on {host}:8000 with reload={reload}") - uvicorn.run("server.serve:app", host=host, port=8000, reload=reload) + port = int(os.getenv("PORT", "8003")) + logger.info(f"Starting agent server on {host}:{port} (reload={reload}, ENVIRONMENT={env})") + uvicorn.run("server.serve:app", host=host, port=port, reload=reload) if __name__ == "__main__": main() \ No newline at end of file diff --git a/apps/blogpost-backend/app/main.py b/apps/blogpost-backend/app/main.py index 3213b1b..22b92c7 100644 --- a/apps/blogpost-backend/app/main.py +++ b/apps/blogpost-backend/app/main.py @@ -11,16 +11,30 @@ app = FastAPI(title="GalacticView Blog Content Service") -ALLOWED_ORIGINS = [o.strip() - for o in os.getenv("ALLOWED_ORIGINS", "http://localhost:3000").split(",") if o.strip()] - -app.add_middleware( - CORSMiddleware, - allow_origins=ALLOWED_ORIGINS, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) +_DEV_ORIGIN_REGEX = r"^https?://(localhost|127\.0\.0\.1)(:\d+)?$" +_DEFAULT_ORIGINS = "http://localhost:5173,http://127.0.0.1:5173" + +if os.getenv("ENVIRONMENT", "prod") == "dev": + app.add_middleware( + CORSMiddleware, + allow_origin_regex=_DEV_ORIGIN_REGEX, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) +else: + allowed_origins = [ + origin.strip() + for origin in os.getenv("ALLOWED_ORIGINS", _DEFAULT_ORIGINS).split(",") + if origin.strip() + ] + app.add_middleware( + CORSMiddleware, + allow_origins=allowed_origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) @app.get("/health", tags=["System"]) def health_check() -> dict[str, str]: diff --git a/apps/core-backend/app/api/routes/auth.py b/apps/core-backend/app/api/routes/auth.py index afe794d..2bfd725 100644 --- a/apps/core-backend/app/api/routes/auth.py +++ b/apps/core-backend/app/api/routes/auth.py @@ -42,7 +42,8 @@ async def register( max_age=cookie_data["expires"], httponly=True, samesite="lax", - secure=False # True in production + secure=False, # True in production + path="/", ) return {"status": "success", "message": "Registered and logged in"} @@ -58,8 +59,13 @@ async def login( cookie_data = service.login_user(request.id_token) response.set_cookie( - key="session", value=cookie_data["cookie"], max_age=cookie_data["expires"], - httponly=True, samesite="lax", secure=False + key="session", + value=cookie_data["cookie"], + max_age=cookie_data["expires"], + httponly=True, + samesite="lax", + secure=False, + path="/", ) return {"status": "success", "message": "Login successful"} diff --git a/apps/core-backend/app/main.py b/apps/core-backend/app/main.py index df002dc..d03818f 100644 --- a/apps/core-backend/app/main.py +++ b/apps/core-backend/app/main.py @@ -48,13 +48,30 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) # type: ignore -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) +_DEV_ORIGIN_REGEX = r"^https?://(localhost|127\.0\.0\.1)(:\d+)?$" +_DEFAULT_ORIGINS = "http://localhost:5173,http://127.0.0.1:5173" + +if os.getenv("ENVIRONMENT", "prod") == "dev": + app.add_middleware( + CORSMiddleware, + allow_origin_regex=_DEV_ORIGIN_REGEX, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) +else: + allowed_origins = [ + origin.strip() + for origin in os.getenv("ALLOWED_ORIGINS", _DEFAULT_ORIGINS).split(",") + if origin.strip() + ] + app.add_middleware( + CORSMiddleware, + allow_origins=allowed_origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) app.include_router(auth.router) diff --git a/apps/frontend/vite.config.ts b/apps/frontend/vite.config.ts index 5137f25..b3b1cc5 100644 --- a/apps/frontend/vite.config.ts +++ b/apps/frontend/vite.config.ts @@ -4,14 +4,4 @@ import { defineConfig } from 'vite'; export default defineConfig({ base: '/', plugins: [react()], - server: { - proxy: { - '/api/agent': { - // Use 127.0.0.1 — on macOS, localhost can resolve to ::1 and hit a different service on :8000 - target: 'http://127.0.0.1:8000', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/api\/agent/, '') || '/', - }, - }, - }, }); From ea4d6b8531f55d02f188f8dd2e0924e191d9af1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=B3czi=20Levente?= Date: Sun, 7 Jun 2026 11:35:59 +0300 Subject: [PATCH 06/16] #13: Fix self review --- apps/agent-backend/server/dependencies.py | 3 ++- apps/agent-backend/server/serve.py | 2 +- apps/core-backend/app/api/routes/auth.py | 2 +- apps/frontend/src/hooks/useAgentChat.ts | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/agent-backend/server/dependencies.py b/apps/agent-backend/server/dependencies.py index 9d7f676..c20a3c5 100644 --- a/apps/agent-backend/server/dependencies.py +++ b/apps/agent-backend/server/dependencies.py @@ -7,6 +7,7 @@ load_dotenv() CORE_BACKEND_URL = os.getenv("CORE_BACKEND_URL", "http://localhost:8001") +AUTH_REQUEST_TIMEOUT = 5.0 async def _verify_session(session_cookie: str) -> dict: @@ -14,7 +15,7 @@ async def _verify_session(session_cookie: str) -> dict: Forwards the session cookie to the core-backend and returns the user dict. Raises HTTPException on any auth failure. """ - async with httpx.AsyncClient() as client: + async with httpx.AsyncClient(timeout=AUTH_REQUEST_TIMEOUT) as client: try: response = await client.get( f"{CORE_BACKEND_URL}/auth/me", diff --git a/apps/agent-backend/server/serve.py b/apps/agent-backend/server/serve.py index 8a73cad..9617dd1 100644 --- a/apps/agent-backend/server/serve.py +++ b/apps/agent-backend/server/serve.py @@ -35,7 +35,7 @@ def get_real_ip(request: Request) -> str: @app.post("/chat") @limiter.limit("7/minute") -async def chat_endpoint( +def chat_endpoint( request: Request, body: ChatTypeIn, _: None = Depends(require_auth), diff --git a/apps/core-backend/app/api/routes/auth.py b/apps/core-backend/app/api/routes/auth.py index 2bfd725..513b2ef 100644 --- a/apps/core-backend/app/api/routes/auth.py +++ b/apps/core-backend/app/api/routes/auth.py @@ -75,7 +75,7 @@ async def logout(response: Response) -> dict[str, str]: """ Clear the session cookie to log the user out. """ - response.delete_cookie(key="session") + response.delete_cookie(key="session", path="/") return {"status": "success", "message": "Logout successful"} diff --git a/apps/frontend/src/hooks/useAgentChat.ts b/apps/frontend/src/hooks/useAgentChat.ts index 18b497f..2f9f184 100644 --- a/apps/frontend/src/hooks/useAgentChat.ts +++ b/apps/frontend/src/hooks/useAgentChat.ts @@ -28,7 +28,6 @@ const useAgentChat = () => { }; setMessages([userMessage]); - setQuestionSent(true); setIsLoading(true); try { @@ -44,6 +43,7 @@ const useAgentChat = () => { }; setMessages((prev) => [...prev, agentMessage]); + setQuestionSent(true); } catch (error) { const errorMessage: ChatMessage = { id: uuidv4(), From 7b622b57deba8aa36fa826c9fb589fa470482302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=B3czi=20Levente?= Date: Sun, 7 Jun 2026 15:20:16 +0300 Subject: [PATCH 07/16] #3: Dockerization Co-authored-by: Cursor --- .gitignore | 5 +- apps/agent-backend/.dockerignore | 17 +++ apps/agent-backend/Dockerfile | 23 ++++ apps/agent-backend/server/dependencies.py | 2 +- apps/agent-backend/server/serve.py | 2 +- apps/blogpost-backend/.dockerignore | 16 +++ apps/blogpost-backend/Dockerfile | 21 +++ apps/blogpost-backend/app/api/dependencies.py | 2 +- apps/blogpost-backend/app/core/s3_setup.py | 30 +++++ apps/blogpost-backend/app/scripts/init_db.py | 16 +-- .../app/services/storage_service.py | 30 +++-- apps/blogpost-backend/docker-compose.yml | 15 --- apps/blogpost-backend/poetry.lock | 12 +- apps/blogpost-backend/pyproject.toml | 2 +- apps/core-backend/.dockerignore | 16 +++ apps/core-backend/Dockerfile | 24 ++++ apps/core-backend/app/main.py | 5 +- .../core-backend/app/services/auth_service.py | 14 +- apps/core-backend/poetry.lock | 24 +--- apps/core-backend/pyproject.toml | 2 +- apps/frontend/.dockerignore | 2 - apps/frontend/Dockerfile | 8 +- apps/frontend/docker-compose.yml | 12 -- apps/frontend/nginx.conf | 14 +- apps/frontend/src/context/AuthContext.tsx | 16 ++- .../frontend/src/pages/BlogPostPageCreate.tsx | 6 - docker-compose.yml | 122 ++++++++++++++++++ 27 files changed, 349 insertions(+), 109 deletions(-) create mode 100644 apps/agent-backend/.dockerignore create mode 100644 apps/agent-backend/Dockerfile create mode 100644 apps/blogpost-backend/.dockerignore create mode 100644 apps/blogpost-backend/Dockerfile create mode 100644 apps/blogpost-backend/app/core/s3_setup.py delete mode 100644 apps/blogpost-backend/docker-compose.yml create mode 100644 apps/core-backend/.dockerignore create mode 100644 apps/core-backend/Dockerfile delete mode 100644 apps/frontend/docker-compose.yml create mode 100644 docker-compose.yml diff --git a/.gitignore b/.gitignore index 9df3a22..8d6b068 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,7 @@ npm-debug.log* yarn-debug.log* yarn-error.log* .pytest_cache/ -__pycache__/ \ No newline at end of file +__pycache__/ + +# Secrets +secrets/ \ No newline at end of file diff --git a/apps/agent-backend/.dockerignore b/apps/agent-backend/.dockerignore new file mode 100644 index 0000000..cc25703 --- /dev/null +++ b/apps/agent-backend/.dockerignore @@ -0,0 +1,17 @@ +__pycache__ +*.py[cod] +*.egg-info +.venv +venv +.env +.env.* +.git +.gitignore +.dockerignore +Dockerfile +docker-compose.yml +requirements.txt +.ruff_cache +.mypy_cache +tests +chroma_db diff --git a/apps/agent-backend/Dockerfile b/apps/agent-backend/Dockerfile new file mode 100644 index 0000000..e8e2ecc --- /dev/null +++ b/apps/agent-backend/Dockerfile @@ -0,0 +1,23 @@ +FROM python:3.12-slim + +WORKDIR /app + +ENV POETRY_VERSION=2.2.1 \ + POETRY_VIRTUALENVS_CREATE=false \ + POETRY_NO_INTERACTION=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PYTHONPATH=/app + +RUN pip install --no-cache-dir poetry==${POETRY_VERSION} + +COPY pyproject.toml poetry.lock README.md ./ +RUN poetry install --no-root + +COPY galacticview_bot ./galacticview_bot +COPY server ./server +RUN poetry install + +EXPOSE 8002 + +CMD ["uvicorn", "server.serve:app", "--host", "0.0.0.0", "--port", "8002"] diff --git a/apps/agent-backend/server/dependencies.py b/apps/agent-backend/server/dependencies.py index c20a3c5..bed6175 100644 --- a/apps/agent-backend/server/dependencies.py +++ b/apps/agent-backend/server/dependencies.py @@ -6,7 +6,7 @@ load_dotenv() -CORE_BACKEND_URL = os.getenv("CORE_BACKEND_URL", "http://localhost:8001") +CORE_BACKEND_URL = os.getenv("CORE_BACKEND_URL", "http://localhost:8000") AUTH_REQUEST_TIMEOUT = 5.0 diff --git a/apps/agent-backend/server/serve.py b/apps/agent-backend/server/serve.py index 9617dd1..5e83041 100644 --- a/apps/agent-backend/server/serve.py +++ b/apps/agent-backend/server/serve.py @@ -58,7 +58,7 @@ def main() -> None: reload = env == "dev" host = "0.0.0.0" - port = int(os.getenv("PORT", "8003")) + port = int(os.getenv("PORT", "8002")) logger.info(f"Starting agent server on {host}:{port} (reload={reload}, ENVIRONMENT={env})") uvicorn.run("server.serve:app", host=host, port=port, reload=reload) diff --git a/apps/blogpost-backend/.dockerignore b/apps/blogpost-backend/.dockerignore new file mode 100644 index 0000000..37dce10 --- /dev/null +++ b/apps/blogpost-backend/.dockerignore @@ -0,0 +1,16 @@ +__pycache__ +*.py[cod] +*.egg-info +.venv +venv +.env +.env.* +.git +.gitignore +.dockerignore +Dockerfile +docker-compose.yml +requirements.txt +.ruff_cache +.mypy_cache +tests diff --git a/apps/blogpost-backend/Dockerfile b/apps/blogpost-backend/Dockerfile new file mode 100644 index 0000000..c2c5daf --- /dev/null +++ b/apps/blogpost-backend/Dockerfile @@ -0,0 +1,21 @@ +FROM python:3.12-slim + +WORKDIR /app + +ENV POETRY_VERSION=2.2.1 \ + POETRY_VIRTUALENVS_CREATE=false \ + POETRY_NO_INTERACTION=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +RUN pip install --no-cache-dir poetry==${POETRY_VERSION} + +COPY pyproject.toml poetry.lock README.md ./ +RUN poetry install --no-root + +COPY app ./app +RUN poetry install + +EXPOSE 8001 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8001"] diff --git a/apps/blogpost-backend/app/api/dependencies.py b/apps/blogpost-backend/app/api/dependencies.py index eb66c1f..5fcaaac 100644 --- a/apps/blogpost-backend/app/api/dependencies.py +++ b/apps/blogpost-backend/app/api/dependencies.py @@ -3,7 +3,7 @@ import httpx from fastapi import Request, HTTPException -CORE_BACKEND_URL = os.getenv("CORE_BACKEND_URL", "http://localhost:8001") +CORE_BACKEND_URL = os.getenv("CORE_BACKEND_URL", "http://localhost:8000") async def _verify_session(session_cookie: str) -> dict: diff --git a/apps/blogpost-backend/app/core/s3_setup.py b/apps/blogpost-backend/app/core/s3_setup.py new file mode 100644 index 0000000..bbf9d7d --- /dev/null +++ b/apps/blogpost-backend/app/core/s3_setup.py @@ -0,0 +1,30 @@ +import os + +from botocore.exceptions import ClientError + +from app.core.aws import s3_client + + +def ensure_s3_bucket() -> None: + """ + Create the blog images bucket if it does not already exist. + Safe to call before every upload in local/dev environments. + """ + bucket_name = os.getenv("S3_BUCKET_NAME", "galactic-blog-images") + region = os.getenv("AWS_REGION", "eu-central-1") + + try: + s3_client.head_bucket(Bucket=bucket_name) + return + except ClientError as e: + error_code = e.response.get("Error", {}).get("Code", "") + if error_code not in {"404", "NoSuchBucket", "NotFound", "403"}: + raise + + if region == "us-east-1": + s3_client.create_bucket(Bucket=bucket_name) + else: + s3_client.create_bucket( + Bucket=bucket_name, + CreateBucketConfiguration={"LocationConstraint": region}, + ) diff --git a/apps/blogpost-backend/app/scripts/init_db.py b/apps/blogpost-backend/app/scripts/init_db.py index e5ddebd..58d4a5a 100644 --- a/apps/blogpost-backend/app/scripts/init_db.py +++ b/apps/blogpost-backend/app/scripts/init_db.py @@ -7,6 +7,7 @@ sys.path.insert(0, repo_root) from app.core.aws import dynamodb, s3_client +from app.core.s3_setup import ensure_s3_bucket def setup_dynamodb(): """ @@ -28,19 +29,10 @@ def setup_s3(): """ Creates the S3 bucket for storing blog images if it doesn't already exist. """ - bucket_name = os.getenv("S3_BUCKET_NAME", "galactic-blog-images") - region = os.getenv("AWS_REGION", "eu-central-1") - try: - # AWS requires a specific LocationConstraint for regions outside us-east-1 - if region == "us-east-1": - s3_client.create_bucket(Bucket=bucket_name) - else: - s3_client.create_bucket( - Bucket=bucket_name, - CreateBucketConfiguration={'LocationConstraint': region} - ) - print(f"✅ S3 Bucket '{bucket_name}' created successfully!") + ensure_s3_bucket() + bucket_name = os.getenv("S3_BUCKET_NAME", "galactic-blog-images") + print(f"✅ S3 Bucket '{bucket_name}' is ready.") except Exception as e: print(f"⚠️ S3 Bucket status: {e}") diff --git a/apps/blogpost-backend/app/services/storage_service.py b/apps/blogpost-backend/app/services/storage_service.py index a9a0d40..c1e9c1e 100644 --- a/apps/blogpost-backend/app/services/storage_service.py +++ b/apps/blogpost-backend/app/services/storage_service.py @@ -5,6 +5,7 @@ from botocore.exceptions import ClientError from app.core.aws import s3_client +from app.core.s3_setup import ensure_s3_bucket class ImagePromotionError(Exception): @@ -22,11 +23,12 @@ class StorageService: def __init__(self) -> None: self.bucket_name = os.getenv("S3_BUCKET_NAME", "galactic-blog-images") self.s3_endpoint = os.getenv("S3_ENDPOINT") + self.s3_public_endpoint = os.getenv("S3_PUBLIC_ENDPOINT", self.s3_endpoint) self.aws_region = os.getenv("AWS_REGION", "eu-central-1") def _get_base_url(self) -> str: - if self.s3_endpoint: - return f"{self.s3_endpoint}/{self.bucket_name}" + if self.s3_public_endpoint: + return f"{self.s3_public_endpoint.rstrip('/')}/{self.bucket_name}" return f"https://{self.bucket_name}.s3.{self.aws_region}.amazonaws.com" def _object_exists(self, key: str) -> bool: @@ -38,10 +40,18 @@ def _object_exists(self, key: str) -> bool: return False raise + def _upload_extra_args(self, content_type: str) -> dict[str, str]: + extra_args: dict[str, str] = {"ContentType": content_type} + if not self.s3_endpoint: + extra_args["ACL"] = "public-read" + return extra_args + def upload_image(self, file_obj: BinaryIO, original_filename: str, content_type: str) -> str: """ Uploads an image to S3 under the "temp/" folder and returns its URL. """ + ensure_s3_bucket() + _, ext = os.path.splitext(original_filename) file_extension = ext.lstrip(".") or "bin" unique_filename = f"{uuid.uuid4()}.{file_extension}" @@ -51,7 +61,7 @@ def upload_image(self, file_obj: BinaryIO, original_filename: str, content_type: file_obj, self.bucket_name, s3_key, - ExtraArgs={"ContentType": content_type, "ACL": "public-read"} + ExtraArgs=self._upload_extra_args(content_type), ) if not self._object_exists(s3_key): @@ -101,12 +111,14 @@ def promote_image(self, temp_image_url: str) -> str: ) copy_source = {"Bucket": self.bucket_name, "Key": source_key} - s3_client.copy_object( - CopySource=copy_source, - Bucket=self.bucket_name, - Key=new_key, - ACL="public-read", - ) + copy_args: dict[str, object] = { + "CopySource": copy_source, + "Bucket": self.bucket_name, + "Key": new_key, + } + if not self.s3_endpoint: + copy_args["ACL"] = "public-read" + s3_client.copy_object(**copy_args) s3_client.delete_object(Bucket=self.bucket_name, Key=source_key) return f"{self._get_base_url()}/{new_key}" \ No newline at end of file diff --git a/apps/blogpost-backend/docker-compose.yml b/apps/blogpost-backend/docker-compose.yml deleted file mode 100644 index c186c4f..0000000 --- a/apps/blogpost-backend/docker-compose.yml +++ /dev/null @@ -1,15 +0,0 @@ -version: "3.8" -services: - dynamodb-local: - image: amazon/dynamodb-local:latest - command: "-jar DynamoDBLocal.jar -sharedDb -inMemory" - ports: - - "8000:8000" - - localstack: - image: localstack/localstack:3.8.0 - container_name: galactic-localstack - ports: - - "4566:4566" - environment: - - SERVICES=s3 diff --git a/apps/blogpost-backend/poetry.lock b/apps/blogpost-backend/poetry.lock index cb8af62..0c4f926 100644 --- a/apps/blogpost-backend/poetry.lock +++ b/apps/blogpost-backend/poetry.lock @@ -393,10 +393,7 @@ files = [ librt = {version = ">=0.8.0", markers = "platform_python_implementation != \"PyPy\""} mypy_extensions = ">=1.0.0" pathspec = ">=1.0.0" -typing_extensions = [ - {version = ">=4.6.0", markers = "python_version < \"3.15\""}, - {version = ">=4.14.0", markers = "python_version >= \"3.15\""}, -] +typing_extensions = {version = ">=4.6.0", markers = "python_version < \"3.15\""} [package.extras] dmypy = ["psutil (>=4.0)"] @@ -418,9 +415,6 @@ files = [ {file = "mypy_boto3_dynamodb-1.43.0.tar.gz", hash = "sha256:f0cea38e058f1d07361ecb55d8f40665d824b42cf4864724c7fccc8bf3946fcd"}, ] -[package.dependencies] -typing-extensions = {version = "*", markers = "python_version < \"3.12\""} - [[package]] name = "mypy-extensions" version = "1.1.0" @@ -813,5 +807,5 @@ dev = ["mypy", "ruff", "types-requests"] [metadata] lock-version = "2.1" -python-versions = "^3.11" -content-hash = "e8453873e3ad39bbab9712463102812a33f0677d791176afab4b519058ac438a" +python-versions = ">=3.12,<3.14" +content-hash = "356778f3872764ebc5d9e2f69143c25fe7479be5d072bcc667c469b48fd42d3d" diff --git a/apps/blogpost-backend/pyproject.toml b/apps/blogpost-backend/pyproject.toml index 4d4080b..3db429c 100644 --- a/apps/blogpost-backend/pyproject.toml +++ b/apps/blogpost-backend/pyproject.toml @@ -6,7 +6,7 @@ authors = [ {name = "Daroczi Levente",email = "daroczilevente2@gmail.com"} ] readme = "README.md" -requires-python = "^3.11" +requires-python = ">=3.12,<3.14" dependencies = [ "fastapi (>=0.136.1,<0.137.0)", "uvicorn (>=0.47.0,<0.48.0)", diff --git a/apps/core-backend/.dockerignore b/apps/core-backend/.dockerignore new file mode 100644 index 0000000..37dce10 --- /dev/null +++ b/apps/core-backend/.dockerignore @@ -0,0 +1,16 @@ +__pycache__ +*.py[cod] +*.egg-info +.venv +venv +.env +.env.* +.git +.gitignore +.dockerignore +Dockerfile +docker-compose.yml +requirements.txt +.ruff_cache +.mypy_cache +tests diff --git a/apps/core-backend/Dockerfile b/apps/core-backend/Dockerfile new file mode 100644 index 0000000..9c1f8ba --- /dev/null +++ b/apps/core-backend/Dockerfile @@ -0,0 +1,24 @@ +FROM python:3.12-slim + +WORKDIR /app + +ENV POETRY_VERSION=2.2.1 \ + POETRY_VIRTUALENVS_CREATE=false \ + POETRY_NO_INTERACTION=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends libpq5 \ + && rm -rf /var/lib/apt/lists/* \ + && pip install --no-cache-dir poetry==${POETRY_VERSION} + +COPY pyproject.toml poetry.lock README.md ./ +RUN poetry install --no-root + +COPY app ./app +RUN poetry install + +EXPOSE 8000 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/apps/core-backend/app/main.py b/apps/core-backend/app/main.py index d03818f..ca8d8ca 100644 --- a/apps/core-backend/app/main.py +++ b/apps/core-backend/app/main.py @@ -93,8 +93,9 @@ def main() -> None: if env == "prod": host = "0.0.0.0" - print(f"Starting server on {host}:8001 with reload={reload}") - uvicorn.run("app.main:app", host=host, port=8001, reload=reload) + port = int(os.getenv("PORT", "8000")) + print(f"Starting server on {host}:{port} with reload={reload}") + uvicorn.run("app.main:app", host=host, port=port, reload=reload) if __name__ == "__main__": main() \ No newline at end of file diff --git a/apps/core-backend/app/services/auth_service.py b/apps/core-backend/app/services/auth_service.py index 02f0e81..1dfb7fb 100644 --- a/apps/core-backend/app/services/auth_service.py +++ b/apps/core-backend/app/services/auth_service.py @@ -43,8 +43,20 @@ def register_user( def login_user(self, id_token: str) -> dict[str, Any]: """ - Creates cookie on login + Verify the Firebase token, ensure the user exists in PostgreSQL, then mint a session cookie. """ + try: + decoded_token = auth.verify_id_token(id_token) + uid = decoded_token["uid"] + except Exception: + raise HTTPException(status_code=401, detail="Invalid Firebase Token") + + if not self.user_repo.get_user_by_id(uid): + raise HTTPException( + status_code=404, + detail="User not found. Complete registration to create your profile.", + ) + return self._create_cookie(id_token) def get_user_by_id(self, user_id: str) -> User: diff --git a/apps/core-backend/poetry.lock b/apps/core-backend/poetry.lock index f256dd5..5c2c9b0 100644 --- a/apps/core-backend/poetry.lock +++ b/apps/core-backend/poetry.lock @@ -553,14 +553,8 @@ files = [ [package.dependencies] google-auth = ">=2.14.1,<3.0.0" googleapis-common-protos = ">=1.63.2,<2.0.0" -grpcio = [ - {version = ">=1.75.1,<2.0.0", optional = true, markers = "python_version >= \"3.14\" and extra == \"grpc\""}, - {version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\" and python_version < \"3.14\""}, -] -grpcio-status = [ - {version = ">=1.75.1,<2.0.0", optional = true, markers = "python_version >= \"3.14\" and extra == \"grpc\""}, - {version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, -] +grpcio = {version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""} +grpcio-status = {version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""} proto-plus = [ {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""}, {version = ">=1.22.3,<2.0.0"}, @@ -636,10 +630,7 @@ files = [ google-api-core = {version = ">=2.11.0,<3.0.0", extras = ["grpc"]} google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0" google-cloud-core = ">=2.0.0,<3.0.0" -grpcio = [ - {version = ">=1.75.1,<2.0.0", markers = "python_version >= \"3.14\""}, - {version = ">=1.33.2,<2.0.0"}, -] +grpcio = ">=1.33.2,<2.0.0" proto-plus = [ {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""}, {version = ">=1.22.3,<2.0.0"}, @@ -1297,10 +1288,7 @@ files = [ librt = {version = ">=0.8.0", markers = "platform_python_implementation != \"PyPy\""} mypy_extensions = ">=1.0.0" pathspec = ">=1.0.0" -typing_extensions = [ - {version = ">=4.6.0", markers = "python_version < \"3.15\""}, - {version = ">=4.14.0", markers = "python_version >= \"3.15\""}, -] +typing_extensions = {version = ">=4.6.0", markers = "python_version < \"3.15\""} [package.extras] dmypy = ["psutil (>=4.0)"] @@ -2075,5 +2063,5 @@ dev = ["mypy", "ruff", "types-requests"] [metadata] lock-version = "2.1" -python-versions = "^3.11" -content-hash = "2bf125a3396926ff3fc40a082e0aa1c614aae644c72d03f79e890bf146091212" +python-versions = ">=3.12,<3.14" +content-hash = "867ac62a1edbe3b403729d8a5c6b0fbb8838e29f4c6f3302d6518512b8f7033f" diff --git a/apps/core-backend/pyproject.toml b/apps/core-backend/pyproject.toml index 2b77160..03f62b9 100644 --- a/apps/core-backend/pyproject.toml +++ b/apps/core-backend/pyproject.toml @@ -6,7 +6,7 @@ authors = [ {name = "Daroczi Levente",email = "daroczilevente2@gmail.com"} ] readme = "README.md" -requires-python = "^3.11" +requires-python = ">=3.12,<3.14" dependencies = [ "fastapi (>=0.136.1,<0.137.0)", "sqlalchemy[asyncio] (>=2.0.49,<3.0.0)", diff --git a/apps/frontend/.dockerignore b/apps/frontend/.dockerignore index 82119a7..b63e47f 100644 --- a/apps/frontend/.dockerignore +++ b/apps/frontend/.dockerignore @@ -2,8 +2,6 @@ node_modules dist dist-ssr *.local -.env -.env.* .git .gitignore .dockerignore diff --git a/apps/frontend/Dockerfile b/apps/frontend/Dockerfile index 17e0497..c6cf3ae 100644 --- a/apps/frontend/Dockerfile +++ b/apps/frontend/Dockerfile @@ -7,17 +7,13 @@ RUN yarn install --frozen-lockfile COPY . . -ARG VITE_AGENT_API_BASE_URL -ENV VITE_AGENT_API_BASE_URL=$VITE_AGENT_API_BASE_URL - RUN yarn build - FROM nginx:alpine COPY --from=builder /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf -EXPOSE 3000 +EXPOSE 80 -CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file +CMD ["nginx", "-g", "daemon off;"] diff --git a/apps/frontend/docker-compose.yml b/apps/frontend/docker-compose.yml deleted file mode 100644 index 770fbe1..0000000 --- a/apps/frontend/docker-compose.yml +++ /dev/null @@ -1,12 +0,0 @@ -version: '3.8' - -services: - galactic-view: - build: - context: . - dockerfile: Dockerfile - args: - - VITE_AGENT_API_BASE_URL=https://localhost:8080 - ports: - - "3000:3000" - restart: always \ No newline at end of file diff --git a/apps/frontend/nginx.conf b/apps/frontend/nginx.conf index 04b890f..eadc886 100644 --- a/apps/frontend/nginx.conf +++ b/apps/frontend/nginx.conf @@ -1,20 +1,16 @@ server { - listen 3000; + listen 80; server_name localhost; + root /usr/share/nginx/html; + index index.html; location / { - root /usr/share/nginx/html; - index index.html index.htm; - try_files $uri $uri/ /index.html; } - location ~* \.(?:ico|css|js|gif|jpe?g|png)$ { - root /usr/share/nginx/html; + location ~* \.(?:ico|css|js|gif|jpe?g|png|svg|woff2?|ttf|eot)$ { expires 1y; access_log off; add_header Cache-Control "public"; } - - -} \ No newline at end of file +} diff --git a/apps/frontend/src/context/AuthContext.tsx b/apps/frontend/src/context/AuthContext.tsx index 91a9e96..bc9569a 100644 --- a/apps/frontend/src/context/AuthContext.tsx +++ b/apps/frontend/src/context/AuthContext.tsx @@ -61,13 +61,25 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }; const handleRegister: AuthContextType['register'] = async (email, password, username, firstName, lastName) => { + let createdFirebaseUser = false; try { - const userCredential = await createUserWithEmailAndPassword(auth, email, password); + let userCredential; + try { + userCredential = await createUserWithEmailAndPassword(auth, email, password); + createdFirebaseUser = true; + } catch (error) { + const code = (error as { code?: string }).code; + if (code === 'auth/email-already-in-use') { + userCredential = await signInWithEmailAndPassword(auth, email, password); + } else { + throw error; + } + } await sendRegisterRequest(userCredential, username, firstName, lastName); await refreshUser(); } catch (error) { console.error('Error during registration:', error); - if (auth.currentUser) { + if (createdFirebaseUser && auth.currentUser) { try { await auth.currentUser.delete(); } catch (e) { diff --git a/apps/frontend/src/pages/BlogPostPageCreate.tsx b/apps/frontend/src/pages/BlogPostPageCreate.tsx index 31ffcba..0220ef2 100644 --- a/apps/frontend/src/pages/BlogPostPageCreate.tsx +++ b/apps/frontend/src/pages/BlogPostPageCreate.tsx @@ -169,12 +169,6 @@ function BlogPostPageCreate() { className={`d-flex flex-column mb-3 ${style.imageUrlItem}`} >
- - {url} -