Skip to content

Commit b6e7acb

Browse files
committed
chore: Various backend improvements and cleanup
- Updated backend routes for improved error handling - Enhanced authentication service - Database and post history improvements - User data cleanup service updates - Dashboard page refinements
1 parent a601aa9 commit b6e7acb

File tree

10 files changed

+197
-185
lines changed

10 files changed

+197
-185
lines changed

backend/app.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -630,7 +630,7 @@ async def linkedin_callback(code: str = None, state: str = None, redirect_uri: s
630630
if user_id and get_user_settings and exchange_code_for_token_with_user:
631631
settings = await get_user_settings(user_id)
632632
if settings and settings.get('linkedin_client_id') and settings.get('linkedin_client_secret'):
633-
result = exchange_code_for_token_with_user(
633+
result = await exchange_code_for_token_with_user(
634634
settings['linkedin_client_id'],
635635
settings['linkedin_client_secret'],
636636
code,
@@ -649,7 +649,7 @@ async def linkedin_callback(code: str = None, state: str = None, redirect_uri: s
649649

650650
# Pass user_id for multi-tenant token storage
651651
# CRITICAL: We pass backend_callback_uri as 'redirect_uri' for the exchange
652-
result = exchange_code_for_token(code, backend_callback_uri, user_id)
652+
result = await exchange_code_for_token(code, backend_callback_uri, user_id)
653653

654654
linkedin_urn = result.get("linkedin_user_urn", "")
655655
return RedirectResponse(f"{frontend_redirect}?linkedin_success=true&linkedin_urn={linkedin_urn}")
@@ -888,9 +888,7 @@ async def get_connection_status_endpoint(user_id: str):
888888
- github_oauth_connected: Has GitHub OAuth token (for private repos)
889889
"""
890890
try:
891-
# Import get_connection_status from token_store
892-
from services.token_store import get_connection_status, get_token_by_user_id
893-
891+
# Use top-level imports (get_connection_status, get_token_by_user_id already imported)
894892
status = await get_connection_status(user_id)
895893

896894
# Get github_username from settings

backend/routes/auth.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ class AuthRefreshRequest(BaseModel):
6464
# LINKEDIN OAUTH ENDPOINTS
6565
# =============================================================================
6666
@router.get('/linkedin/start')
67-
def linkedin_start(redirect_uri: str, user_id: str = None):
67+
async def linkedin_start(redirect_uri: str, user_id: str = None):
6868
"""
6969
Redirects the user to LinkedIn's authorization page.
7070
@@ -87,7 +87,7 @@ def linkedin_start(redirect_uri: str, user_id: str = None):
8787
# Try to use per-user credentials if user_id provided
8888
if user_id and get_user_settings:
8989
try:
90-
settings = get_user_settings(user_id)
90+
settings = await get_user_settings(user_id)
9191
if settings and settings.get('linkedin_client_id') and get_authorize_url_for_user:
9292
url = get_authorize_url_for_user(
9393
settings['linkedin_client_id'],
@@ -107,7 +107,7 @@ def linkedin_start(redirect_uri: str, user_id: str = None):
107107

108108

109109
@router.get('/linkedin/callback')
110-
def linkedin_callback(code: str = None, state: str = None, redirect_uri: str = None):
110+
async def linkedin_callback(code: str = None, state: str = None, redirect_uri: str = None):
111111
"""
112112
Exchange code for token and redirect back to frontend.
113113
@@ -155,9 +155,9 @@ def linkedin_callback(code: str = None, state: str = None, redirect_uri: str = N
155155

156156
# Use per-user credentials if we have a user_id
157157
if user_id and get_user_settings and exchange_code_for_token_with_user:
158-
settings = get_user_settings(user_id)
158+
settings = await get_user_settings(user_id)
159159
if settings and settings.get('linkedin_client_id') and settings.get('linkedin_client_secret'):
160-
result = exchange_code_for_token_with_user(
160+
result = await exchange_code_for_token_with_user(
161161
settings['linkedin_client_id'],
162162
settings['linkedin_client_secret'],
163163
code,
@@ -166,14 +166,14 @@ def linkedin_callback(code: str = None, state: str = None, redirect_uri: str = N
166166
)
167167
if save_user_settings:
168168
settings['linkedin_user_urn'] = result.get('linkedin_user_urn')
169-
save_user_settings(user_id, settings)
169+
await save_user_settings(user_id, settings)
170170

171171
# Fallback to global credentials
172172
if not result:
173173
if not exchange_code_for_token:
174174
return RedirectResponse(f"{frontend_redirect}?linkedin_success=false&error=oauth_not_available")
175175

176-
result = exchange_code_for_token(code, backend_callback_uri, user_id)
176+
result = await exchange_code_for_token(code, backend_callback_uri, user_id)
177177

178178
linkedin_urn = result.get("linkedin_user_urn", "")
179179
return RedirectResponse(f"{frontend_redirect}?linkedin_success=true&linkedin_urn={linkedin_urn}")
@@ -215,7 +215,7 @@ def github_oauth_start(redirect_uri: str, user_id: str):
215215

216216

217217
@router.get('/github/callback')
218-
def github_oauth_callback(code: str = None, state: str = None, redirect_uri: str = None):
218+
async def github_oauth_callback(code: str = None, state: str = None, redirect_uri: str = None):
219219
"""
220220
Handle GitHub OAuth callback.
221221
@@ -276,13 +276,13 @@ def github_oauth_callback(code: str = None, state: str = None, redirect_uri: str
276276

277277
# Store the token encrypted
278278
from services.token_store import save_github_token
279-
save_github_token(user_id, github_username, access_token)
279+
await save_github_token(user_id, github_username, access_token)
280280

281281
# Also update user settings with username
282282
if save_user_settings and get_user_settings:
283-
settings = get_user_settings(user_id) or {}
283+
settings = await get_user_settings(user_id) or {}
284284
settings['github_username'] = github_username
285-
save_user_settings(user_id, settings)
285+
await save_user_settings(user_id, settings)
286286

287287
return {
288288
"status": "success",
@@ -300,12 +300,12 @@ def github_oauth_callback(code: str = None, state: str = None, redirect_uri: str
300300
# AUTH UTILITY ENDPOINTS
301301
# =============================================================================
302302
@router.post("/refresh")
303-
def refresh_auth(req: AuthRefreshRequest):
303+
async def refresh_auth(req: AuthRefreshRequest):
304304
"""Check if user has valid LinkedIn connection"""
305305
if not get_user_settings:
306306
return {"error": "Settings service not available"}
307307
try:
308-
settings = get_user_settings(req.user_id)
308+
settings = await get_user_settings(req.user_id)
309309
if settings and settings.get("linkedin_user_urn"):
310310
return {
311311
"access_token": "valid",

backend/routes/posts.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ async def generate_preview(
121121
groq_api_key = None
122122
if user_id and get_user_settings:
123123
try:
124-
settings = get_user_settings(user_id)
124+
settings = await get_user_settings(user_id)
125125
if settings:
126126
groq_api_key = settings.get('groq_api_key')
127127
except Exception as e:
@@ -132,7 +132,7 @@ async def generate_preview(
132132

133133

134134
@router.post("/publish")
135-
def publish(req: PostRequest):
135+
async def publish(req: PostRequest):
136136
"""Publish a post to LinkedIn."""
137137
if not generate_post_with_ai:
138138
return {"error": "generate_post_with_ai not available (import failed)"}
@@ -142,7 +142,7 @@ def publish(req: PostRequest):
142142
user_settings = None
143143
if req.user_id and get_user_settings:
144144
try:
145-
user_settings = get_user_settings(req.user_id)
145+
user_settings = await get_user_settings(req.user_id)
146146
if user_settings:
147147
groq_api_key = user_settings.get('groq_api_key')
148148
except Exception as e:
@@ -162,7 +162,7 @@ def publish(req: PostRequest):
162162
# First try: use user's specific token
163163
if req.user_id and get_token_by_user_id:
164164
try:
165-
user_token = get_token_by_user_id(req.user_id)
165+
user_token = await get_token_by_user_id(req.user_id)
166166
if user_token:
167167
linkedin_urn = user_token.get('linkedin_user_urn')
168168
token = user_token.get('access_token')
@@ -180,15 +180,15 @@ def publish(req: PostRequest):
180180
# Fallback: use first stored account or environment-based service
181181
accounts = []
182182
try:
183-
accounts = get_all_tokens() if get_all_tokens else []
183+
accounts = await get_all_tokens() if get_all_tokens else []
184184
except Exception:
185185
accounts = []
186186

187187
if accounts:
188188
account = accounts[0]
189189
linkedin_urn = account.get('linkedin_user_urn')
190190
try:
191-
token = get_access_token_for_urn(linkedin_urn)
191+
token = await get_access_token_for_urn(linkedin_urn)
192192
except Exception:
193193
token = None
194194

backend/routes/webhooks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ async def handle_clerk_webhook(request: Request):
140140
# Import and run cleanup
141141
try:
142142
from services.user_data_cleanup import delete_all_user_data
143-
result = delete_all_user_data(user_id)
143+
result = await delete_all_user_data(user_id)
144144

145145
return JSONResponse(
146146
status_code=200,

backend_tokens.db

0 Bytes
Binary file not shown.

services/auth_service.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def get_authorize_url(redirect_uri: str, state: str) -> str:
7676
return f"https://www.linkedin.com/oauth/v2/authorization?{q}"
7777

7878

79-
def exchange_code_for_token(code: str, redirect_uri: str, user_id: str = None) -> dict:
79+
async def exchange_code_for_token(code: str, redirect_uri: str, user_id: str = None) -> dict:
8080
"""
8181
Exchange authorization code for access token (OAuth Step 2).
8282
@@ -146,7 +146,7 @@ def exchange_code_for_token(code: str, redirect_uri: str, user_id: str = None) -
146146

147147
# Store token securely WITH user_id for multi-tenant isolation
148148
# SECURITY: Token is stored in SQLite; access is via parameterized queries
149-
save_token(linkedin_user_urn, access_token, refresh_token=None, expires_at=expires_at, user_id=user_id)
149+
await save_token(linkedin_user_urn, access_token, refresh_token=None, expires_at=expires_at, user_id=user_id)
150150

151151
except Exception as e:
152152
import traceback
@@ -197,7 +197,7 @@ def get_authorize_url_for_user(client_id: str, redirect_uri: str, state: str) ->
197197
return f"https://www.linkedin.com/oauth/v2/authorization?{q}"
198198

199199

200-
def exchange_code_for_token_with_user(
200+
async def exchange_code_for_token_with_user(
201201
client_id: str,
202202
client_secret: str,
203203
code: str,
@@ -262,7 +262,7 @@ def exchange_code_for_token_with_user(
262262
expires_at = int(time.time()) + int(expires_in) if expires_in else None
263263

264264
# Save token with user_id association for multi-tenant isolation
265-
save_token(
265+
await save_token(
266266
linkedin_user_urn,
267267
access_token,
268268
refresh_token=None,
@@ -324,7 +324,7 @@ def refresh_access_token(refresh_token: str) -> dict:
324324
}
325325

326326

327-
def get_access_token_for_urn(linkedin_user_urn: str, refresh_buffer: int = 60) -> str:
327+
async def get_access_token_for_urn(linkedin_user_urn: str, refresh_buffer: int = 60) -> str:
328328
"""
329329
Get a valid access token for a LinkedIn user, refreshing if needed.
330330
@@ -344,7 +344,7 @@ def get_access_token_for_urn(linkedin_user_urn: str, refresh_buffer: int = 60) -
344344
SECURITY: Tokens are refreshed proactively to avoid failed API calls.
345345
The refresh buffer ensures continuous availability.
346346
"""
347-
token_row = get_token_by_urn(linkedin_user_urn)
347+
token_row = await get_token_by_urn(linkedin_user_urn)
348348
if not token_row:
349349
raise RuntimeError('No token found for this linkedin_user_urn')
350350

@@ -362,7 +362,7 @@ def get_access_token_for_urn(linkedin_user_urn: str, refresh_buffer: int = 60) -
362362
refreshed = refresh_access_token(refresh_token)
363363

364364
# Update stored token with new values
365-
save_token(
365+
await save_token(
366366
linkedin_user_urn,
367367
refreshed['access_token'],
368368
refreshed.get('refresh_token'),

services/db.py

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,91 @@
4646
if DATABASE_URL and DATABASE_URL.startswith("postgres://"):
4747
DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql://", 1)
4848

49+
# Detect if we're using SQLite
50+
IS_SQLITE = DATABASE_URL and DATABASE_URL.startswith("sqlite")
51+
4952
# Lazy import to avoid issues if databases package not installed
5053
database = None
54+
_wrapper = None
55+
56+
57+
def _convert_query_for_sqlite(query: str, params: list) -> tuple:
58+
"""
59+
Convert PostgreSQL-style $1, $2 placeholders to SQLite-compatible :p1, :p2 named params.
60+
61+
Args:
62+
query: SQL query with $1, $2, ... placeholders
63+
params: List of parameter values
64+
65+
Returns:
66+
Tuple of (converted_query, params_dict)
67+
"""
68+
import re
69+
70+
if not params:
71+
return query, {}
72+
73+
# Convert $1, $2, etc. to :p1, :p2, etc.
74+
converted_query = query
75+
params_dict = {}
76+
77+
# Find all $N placeholders and replace them
78+
for i, value in enumerate(params, 1):
79+
placeholder = f"${i}"
80+
named_param = f":p{i}"
81+
converted_query = converted_query.replace(placeholder, named_param)
82+
params_dict[f"p{i}"] = value
83+
84+
return converted_query, params_dict
85+
86+
87+
class DatabaseWrapper:
88+
"""
89+
Wrapper around the databases.Database class that handles
90+
PostgreSQL/SQLite parameter compatibility.
91+
92+
All services use PostgreSQL-style $1 placeholders, but when running
93+
locally with SQLite, this wrapper converts them to named params.
94+
"""
95+
96+
def __init__(self, db):
97+
self._db = db
98+
self._is_sqlite = IS_SQLITE
99+
100+
@property
101+
def is_connected(self):
102+
return self._db.is_connected
103+
104+
async def connect(self):
105+
return await self._db.connect()
106+
107+
async def disconnect(self):
108+
return await self._db.disconnect()
109+
110+
async def execute(self, query: str, values: list = None):
111+
if self._is_sqlite and values:
112+
query, values = _convert_query_for_sqlite(query, values)
113+
return await self._db.execute(query=query, values=values)
114+
115+
async def fetch_one(self, query: str, values: list = None):
116+
if self._is_sqlite and values:
117+
query, values = _convert_query_for_sqlite(query, values)
118+
return await self._db.fetch_one(query=query, values=values)
119+
120+
async def fetch_all(self, query: str, values: list = None):
121+
if self._is_sqlite and values:
122+
query, values = _convert_query_for_sqlite(query, values)
123+
return await self._db.fetch_all(query=query, values=values)
51124

52125

53126
def get_database():
54127
"""Get the database instance, initializing if needed."""
55-
global database
128+
global database, _wrapper
56129
if database is None:
57130
from databases import Database
58131
database = Database(DATABASE_URL)
59-
return database
132+
_wrapper = DatabaseWrapper(database)
133+
return _wrapper
60134

61135

62136
async def connect_db():

services/post_history.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -200,10 +200,14 @@ async def get_daily_post_count(user_id: str, user_timezone: str = "UTC") -> int:
200200
import datetime
201201
try:
202202
from zoneinfo import ZoneInfo
203-
user_tz = ZoneInfo(user_timezone)
203+
# Handle common UTC variants
204+
tz_key = user_timezone
205+
if tz_key.upper() == "UTC":
206+
tz_key = "Etc/UTC"
207+
user_tz = ZoneInfo(tz_key)
204208
except Exception:
205-
from zoneinfo import ZoneInfo
206-
user_tz = ZoneInfo("UTC")
209+
# Fallback to UTC using datetime.timezone
210+
user_tz = datetime.timezone.utc
207211

208212
now_local = datetime.datetime.now(user_tz)
209213
today_start_local = now_local.replace(hour=0, minute=0, second=0, microsecond=0)
@@ -246,10 +250,14 @@ async def get_user_usage(user_id: str, tier: str = "free", user_timezone: str =
246250

247251
try:
248252
from zoneinfo import ZoneInfo
249-
user_tz = ZoneInfo(user_timezone)
253+
# Handle common UTC variants
254+
tz_key = user_timezone
255+
if tz_key.upper() == "UTC":
256+
tz_key = "Etc/UTC"
257+
user_tz = ZoneInfo(tz_key)
250258
except Exception:
251-
from zoneinfo import ZoneInfo
252-
user_tz = ZoneInfo("UTC")
259+
# Fallback to UTC using datetime.timezone
260+
user_tz = datetime.timezone.utc
253261

254262
now_local = datetime.datetime.now(user_tz)
255263
tomorrow_midnight = (now_local + datetime.timedelta(days=1)).replace(

0 commit comments

Comments
 (0)