AuthorBot commited on
Commit
0cadd33
Β·
1 Parent(s): 2bbde84

Fix: datetime naive/aware crash across embed-token, access-repo, superadmin, expiry-task + friendly no-subscription UI

Browse files
app/admin/router.py CHANGED
@@ -662,24 +662,27 @@ async def get_embed_token(
662
  from app.models.client_access import ClientAccess
663
  from app.core.access.token_crypto import create_subscription_token
664
 
665
- now = datetime.now(timezone.utc) # R-051: timezone-aware UTC
 
 
666
  result = await db.execute(
667
  select(ClientAccess)
668
  .where(
669
  ClientAccess.author_id == current_user.id,
670
  ClientAccess.is_revoked == False,
671
- ClientAccess.expires_at > now,
672
  )
673
  .order_by(ClientAccess.expires_at.desc())
674
  .limit(1)
675
  )
676
  access = result.scalar_one_or_none()
677
  if not access:
678
- from fastapi import HTTPException
679
- raise HTTPException(
680
- status_code=403,
681
- detail="No active subscription. Contact your administrator to get access."
682
- )
 
683
 
684
  token = create_subscription_token(
685
  author_id=current_user.id,
@@ -688,15 +691,16 @@ async def get_embed_token(
688
  expires_at=access.expires_at,
689
  )
690
 
691
- # Calculate days remaining safely (both naive)
692
  exp = access.expires_at
693
  if exp and exp.tzinfo is not None:
694
  exp = exp.replace(tzinfo=None)
695
- days_remaining = max(0, (exp - now).days) if exp else 0
696
 
697
  from app.services.token_budget import tokens_remaining
698
 
699
  return {
 
700
  "token": token,
701
  "grant_id": access.id,
702
  "plan": access.plan,
 
662
  from app.models.client_access import ClientAccess
663
  from app.core.access.token_crypto import create_subscription_token
664
 
665
+ now = datetime.now(timezone.utc)
666
+ now_naive = now.replace(tzinfo=None) # For comparisons with naive DB datetimes
667
+
668
  result = await db.execute(
669
  select(ClientAccess)
670
  .where(
671
  ClientAccess.author_id == current_user.id,
672
  ClientAccess.is_revoked == False,
673
+ ClientAccess.expires_at > now_naive,
674
  )
675
  .order_by(ClientAccess.expires_at.desc())
676
  .limit(1)
677
  )
678
  access = result.scalar_one_or_none()
679
  if not access:
680
+ # Return clean 200 with active=False instead of 403 so UI can show a proper message
681
+ return {
682
+ "active": False,
683
+ "token": None,
684
+ "message": "No active subscription. Contact your administrator to activate your chatbot.",
685
+ }
686
 
687
  token = create_subscription_token(
688
  author_id=current_user.id,
 
691
  expires_at=access.expires_at,
692
  )
693
 
694
+ # Normalize expiry to naive UTC for subtraction
695
  exp = access.expires_at
696
  if exp and exp.tzinfo is not None:
697
  exp = exp.replace(tzinfo=None)
698
+ days_remaining = max(0, (exp - now_naive).days) if exp else 0
699
 
700
  from app.services.token_budget import tokens_remaining
701
 
702
  return {
703
+ "active": True,
704
  "token": token,
705
  "grant_id": access.id,
706
  "plan": access.plan,
app/admin/templates/admin.html CHANGED
@@ -1277,6 +1277,20 @@ async function generateEmbedCode() {
1277
  if (codeEl) codeEl.textContent = '⏳ Preparing your embed code...';
1278
  try {
1279
  const data = await apiGet(`/admin/${authorSlug}/embed-token`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1280
  _cachedEmbedToken = data.token;
1281
  if (statusEl) {
1282
  const d = data.days_remaining || 0;
@@ -1284,14 +1298,15 @@ async function generateEmbedCode() {
1284
  statusEl.innerHTML = `<span style="color:var(--green);font-weight:600;">βœ… Your chatbot is active</span> β€” ${d} days remaining, ${t} tokens left`;
1285
  }
1286
  } catch(e) {
1287
- if (codeEl) codeEl.textContent = '❌ ' + (e.message || 'Your account is not active yet. Please contact support.');
1288
- if (statusEl) statusEl.innerHTML = `<span style="color:var(--red);">❌ Not active yet β€” please contact your administrator to activate your chatbot</span>`;
1289
- if (liveStatus) liveStatus.innerHTML = `<span style="color:var(--red);">❌ Cannot preview β€” your account needs to be activated first</span>`;
1290
  return;
1291
  }
1292
  }
1293
 
1294
  const tok = _cachedEmbedToken;
 
1295
  if (codeEl) codeEl.textContent = buildEmbedSnippet(origin, tok, authorSlug, theme, pos);
1296
  mountLiveWidgetPreview(origin, tok, theme, pos);
1297
  if (!document.getElementById('platform-instructions')?.innerHTML) showPlatform('wordpress');
 
1277
  if (codeEl) codeEl.textContent = '⏳ Preparing your embed code...';
1278
  try {
1279
  const data = await apiGet(`/admin/${authorSlug}/embed-token`);
1280
+
1281
+ // No active subscription β€” show friendly UI, not an error
1282
+ if (!data.active || !data.token) {
1283
+ if (statusEl) statusEl.innerHTML = `
1284
+ <div style="padding:14px;background:var(--yellow-bg);border:1px solid var(--yellow);border-radius:8px;">
1285
+ <strong style="color:var(--yellow-dark);">⚠️ No active subscription</strong><br>
1286
+ <span style="font-size:12px;color:var(--text2);">Your chatbot is ready but needs a subscription plan to go live.
1287
+ Contact your administrator to activate your account.</span>
1288
+ </div>`;
1289
+ if (codeEl) codeEl.textContent = '⚠️ Chatbot not active yet β€” contact your administrator to get a subscription plan.';
1290
+ if (liveStatus) liveStatus.innerHTML = `<span style="color:var(--yellow-dark);">⚠️ Chatbot preview unavailable β€” no active subscription</span>`;
1291
+ return;
1292
+ }
1293
+
1294
  _cachedEmbedToken = data.token;
1295
  if (statusEl) {
1296
  const d = data.days_remaining || 0;
 
1298
  statusEl.innerHTML = `<span style="color:var(--green);font-weight:600;">βœ… Your chatbot is active</span> β€” ${d} days remaining, ${t} tokens left`;
1299
  }
1300
  } catch(e) {
1301
+ if (codeEl) codeEl.textContent = '❌ Failed to load embed code: ' + (e.message || 'Unknown error');
1302
+ if (statusEl) statusEl.innerHTML = `<span style="color:var(--red);">❌ ${e.message || 'Could not load subscription info'}</span>`;
1303
+ if (liveStatus) liveStatus.innerHTML = `<span style="color:var(--red);">❌ Preview unavailable</span>`;
1304
  return;
1305
  }
1306
  }
1307
 
1308
  const tok = _cachedEmbedToken;
1309
+ if (!tok) return;
1310
  if (codeEl) codeEl.textContent = buildEmbedSnippet(origin, tok, authorSlug, theme, pos);
1311
  mountLiveWidgetPreview(origin, tok, theme, pos);
1312
  if (!document.getElementById('platform-instructions')?.innerHTML) showPlatform('wordpress');
app/repositories/access_repo.py CHANGED
@@ -24,7 +24,7 @@ class AccessRepository(BaseRepository[ClientAccess]):
24
  Returns:
25
  Active ClientAccess record or None.
26
  """
27
- now = datetime.now(timezone.utc)
28
  result = await self.session.execute(
29
  select(ClientAccess).where(
30
  ClientAccess.author_id == author_id,
 
24
  Returns:
25
  Active ClientAccess record or None.
26
  """
27
+ now = datetime.now(timezone.utc).replace(tzinfo=None) # SQLite stores naive datetimes
28
  result = await self.session.execute(
29
  select(ClientAccess).where(
30
  ClientAccess.author_id == author_id,
app/superadmin/router.py CHANGED
@@ -603,8 +603,8 @@ async def get_author_embed_token(
603
  if not user:
604
  raise HTTPException(404, "Author not found")
605
 
606
- # R-051: Use timezone-aware UTC datetime
607
- now = datetime.now(timezone.utc)
608
  result = await db.execute(
609
  select(ClientAccess)
610
  .where(
 
603
  if not user:
604
  raise HTTPException(404, "Author not found")
605
 
606
+ # R-051: Use naive UTC β€” SQLite stores naive datetimes
607
+ now = datetime.now(timezone.utc).replace(tzinfo=None)
608
  result = await db.execute(
609
  select(ClientAccess)
610
  .where(
app/tasks/expiry_check_task.py CHANGED
@@ -27,7 +27,7 @@ async def _run():
27
  from app.services.email_service import EmailService
28
 
29
  email = EmailService()
30
- now = datetime.now(timezone.utc)
31
  warning_days = [7, 1]
32
 
33
  async with _get_session_factory()() as db:
@@ -44,7 +44,8 @@ async def _run():
44
 
45
  sent = 0
46
  for access, user in records:
47
- days_left = (access.expires_at - now).days
 
48
  if days_left in warning_days:
49
  try:
50
  email.send_subscription_expiry_warning(
 
27
  from app.services.email_service import EmailService
28
 
29
  email = EmailService()
30
+ now = datetime.now(timezone.utc).replace(tzinfo=None) # SQLite stores naive datetimes
31
  warning_days = [7, 1]
32
 
33
  async with _get_session_factory()() as db:
 
44
 
45
  sent = 0
46
  for access, user in records:
47
+ expires = access.expires_at.replace(tzinfo=None) if access.expires_at.tzinfo else access.expires_at
48
+ days_left = (expires - now).days
49
  if days_left in warning_days:
50
  try:
51
  email.send_subscription_expiry_warning(