Skip to content

Commit 76f2799

Browse files
committed
Add a better dev mode
1 parent 1ef3fa7 commit 76f2799

File tree

7 files changed

+83
-15
lines changed

7 files changed

+83
-15
lines changed

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,6 @@ VITE_SUBPATH=""
3434

3535
# Configure max size of a message in review
3636
VITE_MSG_MAX_LEN=4096
37+
38+
# Configure the port the vite server uses in dev mode
39+
VITE_DEV_PORT=8080

backend/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
VITE_SUBPATH = os.environ.get("VITE_SUBPATH", "")
2222
VITE_MSG_MAX_LEN = int(os.environ["VITE_MSG_MAX_LEN"])
23+
VITE_DEV_PORT = int(os.environ["VITE_DEV_PORT"])
2324

2425
mongo_client = AsyncIOMotorClient(
2526
f"{BACKEND_MONGO_URI}/{BACKEND_MONGO_DATABASE}?retryWrites=true&w=majority"

backend/main.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
from fastapi.staticfiles import StaticFiles
66
import uvicorn
77

8-
from config import HOST_PORT, HOST_PRIVATE, VITE_SUBPATH
8+
from utils import ProxyFiles
9+
from config import HOST_PORT, HOST_PRIVATE, VITE_DEV_PORT, VITE_SUBPATH
910
from routes.auth import router as auth_router
1011
from routes.courses import router as course_router
1112
from routes.members import router as members_router
@@ -59,7 +60,13 @@ async def diagnostics(request: Request):
5960

6061

6162
# serve frontend
62-
app.mount("/", StaticFiles(directory=FRONTEND_PATH, html=True), "frontend")
63+
try:
64+
app.mount("/", StaticFiles(directory=FRONTEND_PATH, html=True), "frontend")
65+
except RuntimeError:
66+
# we don't have a production build of the frontend, so assume that the
67+
# frontend is running on its own server, and proxy to it
68+
app.mount("/", ProxyFiles(VITE_DEV_PORT), "frontend")
69+
6370

6471
if __name__ == "__main__":
6572
# forwarded_allow_ips set to * so that the nginx headers are trusted

backend/requirements.txt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# To regen this file: run the following in a fresh venv
2-
# pip install fastapi email-validator uvicorn[standard] pyjwt cryptography motor python-cas
2+
# pip install fastapi email-validator uvicorn[standard] pyjwt cryptography motor python-cas httpx
33
# pip freeze
44
annotated-types==0.7.0
55
anyio==4.7.0
@@ -12,13 +12,15 @@ dnspython==2.7.0
1212
email_validator==2.2.0
1313
fastapi==0.115.6
1414
h11==0.14.0
15+
httpcore==1.0.7
1516
httptools==0.6.4
17+
httpx==0.28.1
1618
idna==3.10
1719
lxml==5.3.0
1820
motor==3.6.0
1921
pycparser==2.22
20-
pydantic==2.10.3
21-
pydantic_core==2.27.1
22+
pydantic==2.10.4
23+
pydantic_core==2.27.2
2224
PyJWT==2.10.1
2325
pymongo==4.9.2
2426
python-cas==1.6.0

backend/utils.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import base64
22
from cryptography.fernet import Fernet, InvalidToken
33
from fastapi import HTTPException, Request
4-
from fastapi.responses import RedirectResponse
4+
from fastapi.responses import JSONResponse, RedirectResponse, StreamingResponse
5+
import httpx
56
import jwt
7+
from starlette.types import Scope, Receive, Send
68

79
from config import BACKEND_ADMIN_UIDS, BACKEND_JWT_SECRET
810

@@ -91,3 +93,44 @@ def hash_decrypt(reviewer_id: str):
9193
return base64.b64encode(Fernet(secure_key).decrypt(reviewer_id)).decode()
9294
except InvalidToken:
9395
return None
96+
97+
98+
class ProxyFiles:
99+
"""
100+
This is custom API that serves as a replacement for StaticFiles function
101+
when a production build is not available and the frontend is running on its
102+
own server.
103+
Essentially this implements a basic HTTP proxy so that the backend can serve
104+
the frontend even in dev mode, like it does in production mode.
105+
"""
106+
107+
def __init__(self, port: int, host: str = "127.0.0.1", http_scheme: str = "http"):
108+
self.http_scheme = http_scheme
109+
self.host = host
110+
self.port = port
111+
112+
async def handle_http(self, scope: Scope, receive: Receive, send: Send):
113+
request = Request(scope, receive, send)
114+
async with httpx.AsyncClient() as client:
115+
request_response = await client.request(
116+
request.method.lower(),
117+
f"{self.http_scheme}://{self.host}:{self.port}{request.url.path}",
118+
headers=dict(request.headers),
119+
params=request.query_params,
120+
content=await request.body(),
121+
)
122+
return StreamingResponse(
123+
request_response.aiter_bytes(),
124+
status_code=request_response.status_code,
125+
headers=dict(request_response.headers),
126+
)
127+
128+
async def __call__(self, scope: Scope, receive: Receive, send: Send):
129+
if scope["type"] == "http":
130+
response = await self.handle_http(scope, receive, send)
131+
else:
132+
response = JSONResponse(
133+
{"error": f"{scope['type']} connections are not supported"},
134+
status_code=400,
135+
)
136+
await response(scope, receive, send)

frontend/react-app-env.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
interface ImportMetaEnv {
22
readonly VITE_SUBPATH: string;
33
readonly VITE_MSG_MAX_LEN: string;
4+
readonly VITE_DEV_PORT: string;
45
}
56

67
interface ImportMeta {

frontend/vite.config.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,29 @@
1-
import { defineConfig } from 'vite';
1+
import { defineConfig, ViteDevServer } from 'vite';
22
import react from '@vitejs/plugin-react';
33

4+
const logger = () => {
5+
return {
6+
name: 'log-paths',
7+
configureServer(server: ViteDevServer) {
8+
server.middlewares.use((req, res, next) => {
9+
console.log(`[vite] ${req.method} ${req.url}`);
10+
next();
11+
});
12+
},
13+
};
14+
};
15+
416
// https://vitejs.dev/config/
517
export default defineConfig({
618
base: process.env.VITE_BASE,
7-
plugins: [react()],
8-
preview: {
9-
port: 80,
10-
strictPort: true,
11-
},
19+
plugins: [react(), logger()],
1220
server: {
13-
port: 80,
21+
port: Number(process.env.VITE_DEV_PORT),
1422
strictPort: true,
15-
host: true,
16-
origin: 'http://0.0.0.0:80',
23+
// Force a direct HMR connection instead of proxying it via the backend
24+
// in the dev environment
25+
hmr: {
26+
port: Number(process.env.VITE_DEV_PORT),
27+
},
1728
},
1829
});

0 commit comments

Comments
 (0)