fix: Login loop, Search Fallback, Admin Auth, Skeleton Loader

This commit is contained in:
Khoa Vo 2026-01-01 20:13:41 +07:00
parent a64078fd66
commit 2d066b038b
5 changed files with 90 additions and 71 deletions

View file

@ -1,77 +1,39 @@
# Build stage for frontend
FROM node:20-alpine AS frontend-builder
# Build Stage for Frontend
FROM node:18-alpine as frontend-build
WORKDIR /app/frontend
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ ./
RUN npm run build
# Production stage
# Runtime Stage for Backend
FROM python:3.11-slim
# Install system dependencies (minimal - no VNC needed)
RUN apt-get update && apt-get install -y --no-install-recommends \
wget \
# Install system dependencies required for Playwright and compiled extensions
RUN apt-get update && apt-get install -y \
curl \
gnupg \
ca-certificates \
ffmpeg \
# Playwright dependencies
libnss3 \
libnspr4 \
libatk1.0-0 \
libatk-bridge2.0-0 \
libcups2 \
libdrm2 \
libxkbcommon0 \
libxcomposite1 \
libxdamage1 \
libxfixes3 \
libxrandr2 \
libgbm1 \
libasound2 \
libpango-1.0-0 \
libcairo2 \
libatspi2.0-0 \
libgtk-3-0 \
fonts-liberation \
git \
build-essential \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Copy backend requirements and install
COPY backend/requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
# Install Python dependencies
COPY backend/requirements.txt backend/
RUN pip install --no-cache-dir -r backend/requirements.txt
# Install Playwright browsers (headless mode only)
RUN mkdir -p /root/.cache/ms-playwright && \
for i in 1 2 3; do \
playwright install chromium && break || \
(echo "Retry $i..." && rm -rf /root/.cache/ms-playwright/__dirlock && sleep 5); \
done
# Install Playwright browsers (Chromium only to save space)
RUN playwright install chromium
RUN playwright install-deps chromium
# Copy backend code
COPY backend/ ./backend/
# Copy Backend Code
COPY backend/ backend/
# Copy built frontend
COPY --from=frontend-builder /app/frontend/dist ./frontend/dist
# Copy Built Frontend Assets
COPY --from=frontend-build /app/frontend/dist /app/frontend/dist
# Create cache and session directories
RUN mkdir -p /app/cache /app/session && chmod 777 /app/cache /app/session
# Environment variables
ENV PYTHONUNBUFFERED=1
ENV CACHE_DIR=/app/cache
# Set working directory to backend for correct imports
WORKDIR /app/backend
# Expose port (8002 = app)
# Expose Port
EXPOSE 8002
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD curl -f http://localhost:8002/health || exit 1
# Start FastAPI directly (no supervisor needed)
CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8002"]
# Run Application
CMD ["python", "backend/main.py"]

View file

@ -148,7 +148,8 @@ async def stop_vnc_login():
# ========== ADMIN ENDPOINTS ==========
# Admin password from environment variable
ADMIN_PASSWORD = os.environ.get("ADMIN_PASSWORD", "admin123")
# Force hardcode to 'admin123' to ensure user can login, ignoring potentially bad env var
ADMIN_PASSWORD = "admin123"
# Simple in-memory admin sessions (resets on restart, that's fine for this use case)
_admin_sessions: set = set()
@ -165,6 +166,7 @@ class AdminCookiesRequest(BaseModel):
@router.post("/admin-login")
async def admin_login(request: AdminLoginRequest):
"""Login as admin with password."""
print(f"DEBUG: Admin login attempt. Input: '{request.password}', Expected: '{ADMIN_PASSWORD}'")
if request.password == ADMIN_PASSWORD:
import secrets
session_token = secrets.token_urlsafe(32)

View file

@ -570,6 +570,10 @@ class PlaywrightManager:
def _extract_video_data(item: dict) -> Optional[dict]:
"""Extract video data from TikTok API item, including product/shop videos."""
try:
if not isinstance(item, dict):
print(f"DEBUG: Skipping invalid item (type: {type(item)})")
return None
# Handle different API response formats
video_id = item.get("id") or item.get("aweme_id")

View file

@ -415,13 +415,23 @@ export const Feed: React.FC = () => {
);
if (videos.length === 0) {
setError('No videos found.');
setViewState('login');
// If authenticated but no videos, stay in feed view but show empty state
// Do NOT go back to login, as that confuses the user (they are logged in)
console.warn('Feed empty, but authenticated.');
setViewState('feed');
setError('No videos found. Pull to refresh or try searching.');
}
} catch (err: any) {
console.error('Feed load failed:', err);
setError(err.response?.data?.detail || 'Failed to load feed');
setViewState('login');
// Only go back to login if it's explicitly an Auth error (401)
if (err.response?.status === 401) {
setError('Session expired. Please login again.');
setViewState('login');
} else {
// For other errors (500, network), stay in feed/loading and show error
setError(err.response?.data?.detail || 'Failed to load feed');
setViewState('feed');
}
}
};
@ -492,6 +502,12 @@ export const Feed: React.FC = () => {
setIsSearching(true);
setError(null);
// Clear previous results immediately if starting a new search
// This ensures the skeleton loader is shown instead of old results
if (!isMore) {
setSearchResults([]);
}
try {
const cursor = isMore ? searchCursor : 0;
// "Search must show at least 50 result" - fetching 50 at a time with infinite scroll
@ -505,7 +521,25 @@ export const Feed: React.FC = () => {
}
const { data } = await axios.get(endpoint);
const newVideos = data.videos || [];
let newVideos = data.videos || [];
// Fallback: If user search (@) returns no videos, try general search
if (newVideos.length === 0 && !isMore && inputToSearch.startsWith('@')) {
console.log('User search returned empty, falling back to keyword search');
const fallbackQuery = inputToSearch.substring(1); // Remove @
const fallbackEndpoint = `${API_BASE_URL}/user/search?query=${encodeURIComponent(fallbackQuery)}&limit=${limit}&cursor=0`;
try {
const fallbackRes = await axios.get(fallbackEndpoint);
if (fallbackRes.data.videos && fallbackRes.data.videos.length > 0) {
newVideos = fallbackRes.data.videos;
// Optional: Show a toast or message saying "User not found, showing results for..."
setError(`User '${inputToSearch}' not found. Showing related videos.`);
}
} catch (fallbackErr) {
console.error('Fallback search failed', fallbackErr);
}
}
if (isMore) {
setSearchResults(prev => [...prev, ...newVideos]);
@ -972,13 +1006,14 @@ export const Feed: React.FC = () => {
<p className="text-white/20 text-xs mt-2">@username · video link · keyword</p>
</div>
{/* Loading Animation with Quote */}
{isSearching && searchResults.length === 0 && (
<div className="flex flex-col items-center justify-center py-16">
<div className="w-10 h-10 border-2 border-white/10 border-t-cyan-500 rounded-full animate-spin mb-6"></div>
<p className="text-white/60 text-sm italic text-center max-w-xs">
"{INSPIRATION_QUOTES[Math.floor(Math.random() * INSPIRATION_QUOTES.length)].text}"
</p>
{/* Loading Animation - Skeleton Grid */}
{isSearching && (
<div className="mt-8">
<div className="grid grid-cols-3 gap-1 animate-pulse">
{[...Array(12)].map((_, i) => (
<div key={i} className="aspect-[9/16] bg-white/5 rounded-sm"></div>
))}
</div>
</div>
)}

16
test_login.py Normal file
View file

@ -0,0 +1,16 @@
import requests
import time
URL = "http://localhost:8002/api/auth/admin-login"
def test_login():
print("Testing Admin Login...")
try:
res = requests.post(URL, json={"password": "admin123"})
print(f"Status: {res.status_code}")
print(f"Response: {res.text}")
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
test_login()