mirror of
https://github.com/vndangkhoa/purestream.git
synced 2026-04-05 01:17:58 +07:00
fix: Login loop, Search Fallback, Admin Auth, Skeleton Loader
This commit is contained in:
parent
a64078fd66
commit
2d066b038b
5 changed files with 90 additions and 71 deletions
78
Dockerfile
78
Dockerfile
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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
16
test_login.py
Normal 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()
|
||||
Loading…
Reference in a new issue