Spaces:
Running
Running
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 +13 -9
- app/admin/templates/admin.html +18 -3
- app/repositories/access_repo.py +1 -1
- app/superadmin/router.py +2 -2
- app/tasks/expiry_check_task.py +3 -2
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)
|
|
|
|
|
|
|
| 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 >
|
| 672 |
)
|
| 673 |
.order_by(ClientAccess.expires_at.desc())
|
| 674 |
.limit(1)
|
| 675 |
)
|
| 676 |
access = result.scalar_one_or_none()
|
| 677 |
if not access:
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 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 |
-
#
|
| 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 -
|
| 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 || '
|
| 1288 |
-
if (statusEl) statusEl.innerHTML = `<span style="color:var(--red);">β
|
| 1289 |
-
if (liveStatus) liveStatus.innerHTML = `<span style="color:var(--red);">β
|
| 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
|
| 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 |
-
|
|
|
|
| 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(
|