Skip to content

Commit bbf4b87

Browse files
authored
General cleanup (#35)
* Format Code and improve formatting - Formatted the code - Fixed the `pyproject.toml` on the backend so that it actually reformats code when we run a command like `uv run ruff format` * Updated package.json when running dev and preview Updated package.json when running dev and preview. Now it explicitly runs it with a host and port. The main part of this was running it with host `127.0.0.1`, which aligns with our configurations of detecting when we're running something in Development server.' * Removed remaining H2S script files Had some remaining H2S related files that were related to how we tried to do client side calculations for H2S. We've moved those calculations to the server side so we don't need these files anymore. * Prevented archived users from being authenticated - Fixed makefile as well
1 parent fe6c83b commit bbf4b87

28 files changed

Lines changed: 1759 additions & 3331 deletions

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ TAG=latest
1111
up:
1212
@echo "Building the project..."
1313
@cd frontend && npm install && npm run build
14+
@cd backend && uv run ruff format
1415
@echo "Starting up the project with docker compose..."
1516
docker compose -f $(COMPOSE_FILE) -p $(PROJECT_NAME) up --build -d
1617

@@ -38,6 +39,7 @@ frontend:
3839

3940
build:
4041
@echo "Building the project..."
42+
@cd backend && uv run ruff format
4143
@cd frontend && npm install && npm run build
4244
docker build -t $(IMAGE_NAME):$(TAG) .
4345

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ make down # Shutsdown all of the Docker Compose containers
1616
make logs # Get the logs from the Docker Compose deployment
1717
```
1818

19+
**Note:** Building the project with `make up` or `make build` will format the frontend and backend. The backend is done through a command, whilst the frontend is done automatically since we're using RS as a build tool with `biome.json`.
20+
1921
### Simulating a prod environment
2022
This one is mainly useful locally if you're wanting to see if a healthcheck is failing or some other broader *deployment* issue is happening. You should be testing your application with this (w/ IU's VPN for the database connection) before making a PR.
2123
```sh

backend/app/__init__.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,24 @@
1717

1818
# Setup middleware layer; for prod we should probably just let the only the client be the allowed origin
1919
app.add_middleware(
20-
CORSMiddleware,
21-
allow_origins=["*"],
22-
allow_credentials=True,
23-
allow_methods=["*"],
24-
allow_headers=["*"],
20+
CORSMiddleware,
21+
allow_origins=["*"],
22+
allow_credentials=True,
23+
allow_methods=["*"],
24+
allow_headers=["*"],
2525
)
2626

27+
2728
# The motivation is to catch all HTTPExceptions and return a consistent JSON response.
2829
# NOTE: If changed, please reflect those changes on frontend as well. It's simple just go to the fetch
2930
# functions for each calc.
3031
@app.exception_handler(HTTPException)
3132
async def custom_http_exception_handler(request: Request, exc: HTTPException):
32-
err_content = {"status_code": exc.status_code, "message": exc.detail}
33-
return JSONResponse(
34-
status_code=exc.status_code,
35-
content=err_content,
36-
)
33+
err_content = {"status_code": exc.status_code, "message": exc.detail}
34+
return JSONResponse(
35+
status_code=exc.status_code,
36+
content=err_content,
37+
)
3738

3839

3940
app.include_router(minerals.router, tags=["minerals"])
@@ -48,10 +49,11 @@ async def custom_http_exception_handler(request: Request, exc: HTTPException):
4849
# Serve all static files (JS, CSS, images, etc.)
4950
app.mount("/static", StaticFiles(directory="/app/dist/static"), name="static")
5051

52+
5153
# Serve index.html for root and any unmatched frontend routes
5254
@app.get("/{full_path:path}", include_in_schema=False)
5355
async def serve_spa(full_path: str):
54-
index_path = os.path.join("/app/dist", "index.html")
55-
if os.path.exists(index_path):
56-
return FileResponse(index_path)
57-
return {"error": "index.html not found"}
56+
index_path = os.path.join("/app/dist", "index.html")
57+
if os.path.exists(index_path):
58+
return FileResponse(index_path)
59+
return {"error": "index.html not found"}

backend/app/config.py

Lines changed: 35 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,38 +4,38 @@
44

55

66
class Settings(BaseSettings):
7-
"""Application settings loaded from environment variables."""
8-
9-
# Database settings
10-
DB_HOST: str = os.getenv("DB_HOST", "")
11-
DB_PORT: str = os.getenv("DB_PORT", "3306")
12-
DB_NAME: str = os.getenv("DB_NAME", "")
13-
DB_USER: str = os.getenv("DB_USER", "")
14-
DB_PASS: str = os.getenv("DB_PASS", "")
15-
16-
# Authentication settings
17-
CILOGON_CLIENT_ID: str = os.getenv("CILOGON_CLIENT_ID", "")
18-
CILOGON_CLIENT_SECRET: str = os.getenv("CILOGON_CLIENT_SECRET", "")
19-
CILOGON_REDIRECT_URI: str = os.getenv("CILOGON_REDIRECT_URI", "")
20-
21-
# Email settings - Mailtrap
22-
SMTP_HOST: str = os.getenv("SMTP_HOST", "smtp.mailtrap.io")
23-
SMTP_PORT: int = int(os.getenv("SMTP_PORT", "2525"))
24-
SMTP_USER: str = os.getenv("SMTP_USER", "")
25-
SMTP_PASSWORD: str = os.getenv("SMTP_PASSWORD", "")
26-
EMAIL_FROM: str = os.getenv("EMAIL_FROM", "noreply@js2-gateway.example.com")
27-
EMAIL_FROM_NAME: str = os.getenv("EMAIL_FROM_NAME", "JS2 Geochemical Gateway")
28-
29-
# Admin notification email (temporary hardcoded value)
30-
ADMIN_NOTIFICATION_EMAIL: str = "callsutt@iu.edu"
31-
32-
# Secret key for JWT token generation
33-
SECRET_KEY: str = os.getenv("SECRET_KEY", "supersecretkey123changemelater")
34-
ALGORITHM: str = "HS256"
35-
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 # 1 week
36-
37-
# Frontend URL for email links
38-
FRONTEND_URL: str = os.getenv("FRONTEND_URL", "http://localhost:4000")
39-
40-
41-
settings = Settings()
7+
"""Application settings loaded from environment variables."""
8+
9+
# Database settings
10+
DB_HOST: str = os.getenv("DB_HOST", "")
11+
DB_PORT: str = os.getenv("DB_PORT", "3306")
12+
DB_NAME: str = os.getenv("DB_NAME", "")
13+
DB_USER: str = os.getenv("DB_USER", "")
14+
DB_PASS: str = os.getenv("DB_PASS", "")
15+
16+
# Authentication settings
17+
CILOGON_CLIENT_ID: str = os.getenv("CILOGON_CLIENT_ID", "")
18+
CILOGON_CLIENT_SECRET: str = os.getenv("CILOGON_CLIENT_SECRET", "")
19+
CILOGON_REDIRECT_URI: str = os.getenv("CILOGON_REDIRECT_URI", "")
20+
21+
# Email settings - Mailtrap
22+
SMTP_HOST: str = os.getenv("SMTP_HOST", "smtp.mailtrap.io")
23+
SMTP_PORT: int = int(os.getenv("SMTP_PORT", "2525"))
24+
SMTP_USER: str = os.getenv("SMTP_USER", "")
25+
SMTP_PASSWORD: str = os.getenv("SMTP_PASSWORD", "")
26+
EMAIL_FROM: str = os.getenv("EMAIL_FROM", "noreply@js2-gateway.example.com")
27+
EMAIL_FROM_NAME: str = os.getenv("EMAIL_FROM_NAME", "JS2 Geochemical Gateway")
28+
29+
# Admin notification email (temporary hardcoded value)
30+
ADMIN_NOTIFICATION_EMAIL: str = "callsutt@iu.edu"
31+
32+
# Secret key for JWT token generation
33+
SECRET_KEY: str = os.getenv("SECRET_KEY", "supersecretkey123changemelater")
34+
ALGORITHM: str = "HS256"
35+
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 # 1 week
36+
37+
# Frontend URL for email links
38+
FRONTEND_URL: str = os.getenv("FRONTEND_URL", "http://localhost:4000")
39+
40+
41+
settings = Settings()

backend/app/db/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
from app.db.database import get_session, engine
33

44
# This makes these imports available from the db package
5-
__all__ = ["User", "get_session", "engine"]
5+
__all__ = ["User", "get_session", "engine"]

backend/app/db/database.py

Lines changed: 108 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -19,125 +19,128 @@
1919

2020

2121
def test_raw_connection():
22-
"""Test the raw MySQL connection without SQLAlchemy to diagnose issues."""
23-
logger.info(
24-
f"Testing direct MySQL connection to {DB_HOST}:{DB_PORT}/{DB_NAME} as {DB_USER}"
22+
"""Test the raw MySQL connection without SQLAlchemy to diagnose issues."""
23+
logger.info(
24+
f"Testing direct MySQL connection to {DB_HOST}:{DB_PORT}/{DB_NAME} as {DB_USER}"
25+
)
26+
try:
27+
conn = pymysql.connect(
28+
host=DB_HOST,
29+
port=int(DB_PORT),
30+
user=DB_USER,
31+
password=DB_PASS,
32+
database=DB_NAME,
33+
connect_timeout=5,
2534
)
26-
try:
27-
conn = pymysql.connect(
28-
host=DB_HOST,
29-
port=int(DB_PORT),
30-
user=DB_USER,
31-
password=DB_PASS,
32-
database=DB_NAME,
33-
connect_timeout=5,
34-
)
35-
with conn.cursor() as cursor:
36-
cursor.execute("SELECT 1")
37-
result = cursor.fetchone()
38-
logger.info(f"Direct MySQL connection successful: {result}")
39-
conn.close()
40-
return True
41-
except pymysql.Error as e:
42-
error_code = e.args[0] if len(e.args) > 0 else "unknown"
43-
if error_code == 1045: # Access denied error
44-
logger.error(f"MySQL access denied. Check credentials for user {DB_USER}.")
45-
else:
46-
logger.error(f"MySQL connection error (code {error_code}): {e}")
47-
return False
35+
with conn.cursor() as cursor:
36+
cursor.execute("SELECT 1")
37+
result = cursor.fetchone()
38+
logger.info(f"Direct MySQL connection successful: {result}")
39+
conn.close()
40+
return True
41+
except pymysql.Error as e:
42+
error_code = e.args[0] if len(e.args) > 0 else "unknown"
43+
if error_code == 1045: # Access denied error
44+
logger.error(f"MySQL access denied. Check credentials for user {DB_USER}.")
45+
else:
46+
logger.error(f"MySQL connection error (code {error_code}): {e}")
47+
return False
4848

4949

5050
# Log connection details for debugging (without password)
5151
logger.info(
52-
f"Database connection info - Host: {DB_HOST}, Port: {DB_PORT}, DB: {DB_NAME}, User: {DB_USER}"
52+
f"Database connection info - Host: {DB_HOST}, Port: {DB_PORT}, DB: {DB_NAME}, User: {DB_USER}"
5353
)
5454

5555
# Initialize engine as None
5656
engine = None
5757

5858
# Check if all required environment variables are set
5959
if not all([DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASS]):
60-
logger.warning(
61-
"Missing database environment variables. Database features will be disabled."
62-
)
60+
logger.warning(
61+
"Missing database environment variables. Database features will be disabled."
62+
)
6363
else:
64-
# Test raw connection first
65-
raw_connection_ok = test_raw_connection()
66-
67-
if raw_connection_ok:
68-
# If raw connection works, try SQLAlchemy
69-
try:
70-
# Create database connection URL with proper escaping and formatting
71-
# Make sure we're properly URL encoding special characters in the password
72-
encoded_password = urllib.parse.quote_plus(DB_PASS)
73-
74-
# Create the connection URL - use proper format to avoid the @ symbol issue
75-
DATABASE_URL = f"mysql+pymysql://{DB_USER}:{encoded_password}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
76-
77-
logger.info(
78-
f"Creating SQLAlchemy engine with host: {DB_HOST}, port: {DB_PORT}, db: {DB_NAME}"
79-
)
80-
81-
# Create SQLModel engine with minimal options
82-
engine = create_engine(
83-
DATABASE_URL,
84-
echo=False,
85-
pool_recycle=3600, # Recycle connections after an hour
86-
pool_pre_ping=False, # Disable connection testing
87-
)
88-
89-
# Basic connection test using proper SQLAlchemy 2.0 syntax
90-
try:
91-
with engine.connect() as conn:
92-
# Using text() to create a SQL expression
93-
result = conn.execute(text("SELECT 1"))
94-
logger.info(f"SQLAlchemy database connection test successful: {result.fetchone()}")
95-
except Exception as e:
96-
logger.error(f"SQLAlchemy database connection test failed: {e}")
97-
# Keep the engine but mark the error
98-
engine._connection_error = str(e)
99-
100-
except Exception as e:
101-
logger.error(f"Error creating database engine: {e}")
102-
engine = None
103-
else:
104-
logger.error("Skipping SQLAlchemy setup due to raw connection failure.")
64+
# Test raw connection first
65+
raw_connection_ok = test_raw_connection()
66+
67+
if raw_connection_ok:
68+
# If raw connection works, try SQLAlchemy
69+
try:
70+
# Create database connection URL with proper escaping and formatting
71+
# Make sure we're properly URL encoding special characters in the password
72+
encoded_password = urllib.parse.quote_plus(DB_PASS)
73+
74+
# Create the connection URL - use proper format to avoid the @ symbol issue
75+
DATABASE_URL = (
76+
f"mysql+pymysql://{DB_USER}:{encoded_password}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
77+
)
78+
79+
logger.info(
80+
f"Creating SQLAlchemy engine with host: {DB_HOST}, port: {DB_PORT}, db: {DB_NAME}"
81+
)
82+
83+
# Create SQLModel engine with minimal options
84+
engine = create_engine(
85+
DATABASE_URL,
86+
echo=False,
87+
pool_recycle=3600, # Recycle connections after an hour
88+
pool_pre_ping=False, # Disable connection testing
89+
)
90+
91+
# Basic connection test using proper SQLAlchemy 2.0 syntax
92+
try:
93+
with engine.connect() as conn:
94+
# Using text() to create a SQL expression
95+
result = conn.execute(text("SELECT 1"))
96+
logger.info(
97+
f"SQLAlchemy database connection test successful: {result.fetchone()}"
98+
)
99+
except Exception as e:
100+
logger.error(f"SQLAlchemy database connection test failed: {e}")
101+
# Keep the engine but mark the error
102+
engine._connection_error = str(e)
103+
104+
except Exception as e:
105+
logger.error(f"Error creating database engine: {e}")
106+
engine = None
107+
else:
108+
logger.error("Skipping SQLAlchemy setup due to raw connection failure.")
109+
105110

106111
# Context manager for database sessions with better error handling
107112
@contextmanager
108113
def get_session():
109-
"""Get a database session if possible, otherwise return None."""
110-
if engine is None:
111-
logger.warning("Database engine is not configured. Returning None session.")
112-
yield None
113-
return
114-
115-
# Check if we have a known connection error
116-
if hasattr(engine, "_connection_error"):
117-
logger.warning(
118-
f"Skipping database session due to known connection error: {engine._connection_error}"
119-
)
120-
yield None
121-
return
122-
123-
session = None
124-
try:
125-
session = Session(engine)
126-
logger.debug("Database session created")
127-
yield session
128-
except pymysql.err.OperationalError as e:
129-
error_code = e.args[0] if len(e.args) > 0 else "unknown"
130-
if error_code == 1045: # Access denied error
131-
logger.error(
132-
f"Database access denied. Check credentials for user {DB_USER}."
133-
)
134-
else:
135-
logger.error(f"Database operational error: {e}")
136-
yield None
137-
except Exception as e:
138-
logger.error(f"Database error: {e}")
139-
yield None
140-
finally:
141-
if session:
142-
logger.debug("Database session closed")
143-
session.close()
114+
"""Get a database session if possible, otherwise return None."""
115+
if engine is None:
116+
logger.warning("Database engine is not configured. Returning None session.")
117+
yield None
118+
return
119+
120+
# Check if we have a known connection error
121+
if hasattr(engine, "_connection_error"):
122+
logger.warning(
123+
f"Skipping database session due to known connection error: {engine._connection_error}"
124+
)
125+
yield None
126+
return
127+
128+
session = None
129+
try:
130+
session = Session(engine)
131+
logger.debug("Database session created")
132+
yield session
133+
except pymysql.err.OperationalError as e:
134+
error_code = e.args[0] if len(e.args) > 0 else "unknown"
135+
if error_code == 1045: # Access denied error
136+
logger.error(f"Database access denied. Check credentials for user {DB_USER}.")
137+
else:
138+
logger.error(f"Database operational error: {e}")
139+
yield None
140+
except Exception as e:
141+
logger.error(f"Database error: {e}")
142+
yield None
143+
finally:
144+
if session:
145+
logger.debug("Database session closed")
146+
session.close()

0 commit comments

Comments
 (0)