Build AI-Powered Flashcard Study App#3
Conversation
Core Features: ✅ AI-powered flashcard generation from multiple sources ✅ Spaced repetition system (SM-2 simplified) ✅ User authentication with JWT ✅ Full deck management (CRUD) ✅ Interactive practice mode with flip animations ✅ Support for PDF, PPTX, text, and YouTube Tech Stack: Backend: - FastAPI + PostgreSQL + SQLAlchemy - AI integration (OpenAI & Anthropic APIs) - File parsing (PyPDF2, python-pptx, yt-dlp) - JWT authentication - Spaced repetition algorithm Frontend: - React 18 + TypeScript + Vite - TailwindCSS + shadcn/ui components - React Router for navigation - Axios for API calls - Responsive design Infrastructure: - Docker Compose setup - PostgreSQL database - Nginx reverse proxy - Environment configuration Project Structure: - backend/: FastAPI application with models, routes, services - frontend/: React app with pages, components, hooks - docker-compose.yml: Complete deployment setup - README.md: Comprehensive documentation See README.md for setup and usage instructions.
WalkthroughIntroduces a complete AI-powered spaced repetition flashcard application. Includes a FastAPI backend with user authentication, deck/flashcard management, spaced repetition practice, and AI-driven card generation; a React 18/TypeScript frontend with dashboard, practice, and upload interfaces; Docker/Docker Compose deployment setup; and comprehensive documentation. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Frontend as React Frontend
participant Backend as FastAPI Backend
participant DB as PostgreSQL
participant AI as OpenAI/Anthropic
Note over User,AI: User Authentication & Deck Creation Flow
User->>Frontend: Sign up with email/password
Frontend->>Backend: POST /api/auth/signup
Backend->>DB: Create User (hashed password)
Backend->>Frontend: Return User + Token
Frontend->>Frontend: Store token in localStorage
Note over User,AI: Upload Content & Generate Flashcards
User->>Frontend: Upload file/text/YouTube URL
Frontend->>Backend: POST /api/upload/process (multipart/form-data)
Backend->>Backend: Parse content (FileParser)
Backend->>AI: Generate flashcards from content
AI-->>Backend: Return list of Q&A pairs
Backend->>DB: Create Deck + Flashcards
Backend->>Frontend: Return Deck with card_count
Frontend->>Frontend: Navigate to deck editor
Note over User,AI: Spaced Repetition Practice Flow
User->>Frontend: Click "Practice" on deck
Frontend->>Backend: GET /api/practice/deck/{id}/due
Backend->>DB: Query Flashcards filtered by next_review <= now
Backend->>Frontend: Return due Flashcards + Review metadata
Frontend->>Frontend: Display flashcard (question side)
User->>Frontend: Click "Remembered" or "Forgot"
Frontend->>Backend: POST /api/practice/review (remembered: bool)
Backend->>Backend: Calculate next_review via SM-2 algorithm
Backend->>DB: Insert Review record
Backend->>Frontend: Return Review with updated interval
Frontend->>Frontend: Show next flashcard or completion
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 20
🧹 Nitpick comments (12)
README.md (2)
38-38: Add language specifier to code fence.The project structure code block needs a language identifier for proper markdown formatting.
-``` +```plaintext flashcard-app/ ├── backend/
105-107: Wrap bare URLs in markdown link syntax.Five bare URLs should be wrapped in markdown brackets for consistent formatting per markdownlint standards (MD034).
- - Frontend: http://localhost:3000 - - Backend API: http://localhost:8000 - - API Documentation: http://localhost:8000/docs + - Frontend: [`http://localhost:3000`](http://localhost:3000) + - Backend API: [`http://localhost:8000`](http://localhost:8000) + - API Documentation: [`http://localhost:8000/docs`](http://localhost:8000/docs)- The API will be available at http://localhost:8000 + The API will be available at [`http://localhost:8000`](http://localhost:8000)- The app will be available at http://localhost:3000 + The app will be available at [`http://localhost:3000`](http://localhost:3000)Also applies to: 160-160, 179-179
backend/Dockerfile (1)
1-25: Consider running the application as a non-root user.The Dockerfile runs the application as root (default user in the container), which is a security risk if the container is compromised. Industry best practice is to create and switch to a non-root user.
Apply this diff to add a non-root user:
# Create uploads directory RUN mkdir -p uploads +# Create non-root user +RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app +USER appuser + # Expose port EXPOSE 8000frontend/Dockerfile (1)
18-28: Consider adding a non-root user for nginx.Running nginx as root in production is a security concern. While nginx itself drops privileges, the container process starts as root.
Add a non-root user before the CMD:
FROM nginx:alpine # Copy built assets from build stage COPY --from=build /app/dist /usr/share/nginx/html # Copy nginx configuration COPY nginx.conf /etc/nginx/conf.d/default.conf +# Run as non-root user +RUN chown -R nginx:nginx /usr/share/nginx/html +USER nginx + EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]Note: This may require adjusting nginx.conf to bind to port 8080 instead of 80, and updating EXPOSE accordingly.
backend/app/database.py (1)
8-8: Consider adding connection pool settings for production.The engine is created with minimal configuration. For production deployments, consider adding resilience and pool management settings.
Apply this diff to add production-grade settings:
-engine = create_engine(settings.DATABASE_URL) +engine = create_engine( + settings.DATABASE_URL, + pool_pre_ping=True, # Verify connections before using + pool_size=5, + max_overflow=10, + pool_recycle=3600, # Recycle connections after 1 hour +)
pool_pre_ping=Truedetects and discards stale connections- Pool sizing prevents connection exhaustion
pool_recyclehelps with databases that close idle connectionsfrontend/src/App.tsx (1)
74-89: Consider consolidating the Practice routes.Both
/practiceand/practice/:deckIdrender the samePracticecomponent. While this works, you could simplify by using a single route with an optional parameter:/practice/:deckId?.Apply this diff to consolidate:
- <Route - path="/practice" - element={ - <PrivateRoute> - <Practice /> - </PrivateRoute> - } - /> - <Route - path="/practice/:deckId" - element={ - <PrivateRoute> - <Practice /> - </PrivateRoute> - } - /> + <Route + path="/practice/:deckId?" + element={ + <PrivateRoute> + <Practice /> + </PrivateRoute> + } + />backend/app/schemas/__init__.py (1)
7-20: Consider sorting__all__for consistency.The static analysis tool suggests applying isort-style sorting to the
__all__list. This is a minor style improvement that enhances consistency.Apply this diff:
__all__ = [ - "UserCreate", - "UserLogin", - "UserResponse", - "Token", "DeckCreate", + "DeckResponse", "DeckUpdate", - "DeckResponse", "FlashcardCreate", + "FlashcardResponse", "FlashcardUpdate", - "FlashcardResponse", "ReviewCreate", "ReviewResponse", + "Token", + "UserCreate", + "UserLogin", + "UserResponse", ]frontend/src/pages/Signup.tsx (1)
22-29: Consider adding password strength validation.Currently, only password matching is validated client-side. Adding minimum length or strength requirements would improve user experience by providing immediate feedback before the backend rejects weak passwords.
Example addition:
if (password !== confirmPassword) { toast({ title: 'Error', description: 'Passwords do not match', variant: 'destructive', }); return; } + + if (password.length < 8) { + toast({ + title: 'Error', + description: 'Password must be at least 8 characters', + variant: 'destructive', + }); + return; + }frontend/src/pages/DeckEditor.tsx (2)
108-108: Consider using a custom confirmation dialog.Native
confirm()dialogs work but don't match the app's design system and can't be styled. Consider using a custom dialog component from your UI library for better UX consistency.Also applies to: 128-128
50-75: Consider adding loading state to the add card form.The form submission doesn't disable the submit button or show a loading state during the API call. This could allow users to double-submit the form. Consider adding a loading state.
Example implementation:
export default function DeckEditor() { // ... existing state ... + const [addingCard, setAddingCard] = useState(false); const handleAddCard = async (e: React.FormEvent) => { e.preventDefault(); if (!deckId || !newQuestion || !newAnswer) return; + setAddingCard(true); try { const newCard = await flashcardsAPI.create({ question: newQuestion, answer: newAnswer, deck_id: parseInt(deckId), }); setFlashcards([...flashcards, newCard]); setNewQuestion(''); setNewAnswer(''); toast({ title: 'Success', description: 'Flashcard added', }); } catch (error) { toast({ title: 'Error', description: 'Failed to add flashcard', variant: 'destructive', }); + } finally { + setAddingCard(false); } };Then update the button:
- <Button type="submit"> + <Button type="submit" disabled={addingCard}> <Plus className="h-4 w-4 mr-2" /> - Add Flashcard + {addingCard ? 'Adding...' : 'Add Flashcard'} </Button>backend/app/utils/auth.py (1)
32-42: Consider using timezone-aware datetime for future compatibility.Lines 36 and 38 use
datetime.utcnow(), which is deprecated in Python 3.12+ in favor ofdatetime.now(timezone.utc). While not critical, updating would ensure future compatibility.Apply this diff to use timezone-aware datetime:
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: """Create a JWT access token""" to_encode = data.copy() if expires_delta: - expire = datetime.utcnow() + expires_delta + expire = datetime.now(timezone.utc) + expires_delta else: - expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) + expire = datetime.now(timezone.utc) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM) return encoded_jwtNote: You'll need to add
from datetime import timezoneto the imports.backend/app/utils/file_parser.py (1)
14-47: Add timeout to prevent hanging on network operations.While the PDF, PPTX, and text parsers don't have the same security issues as the YouTube parser, consider adding timeouts if any of these libraries make network calls internally. For the current implementation, the exception handling could be improved by chaining exceptions and being more specific.
Example improvement for exception handling:
def parse_pdf(file_path: str) -> str: """Extract text from a PDF file""" try: reader = PdfReader(file_path) text = "" for page in reader.pages: text += page.extract_text() + "\n" return text.strip() - except Exception as e: - raise ValueError(f"Error parsing PDF: {str(e)}") + except Exception as e: + raise ValueError(f"Error parsing PDF: {str(e)}") from e
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (64)
.env.example(1 hunks)README.md(1 hunks)backend/.env.example(1 hunks)backend/.gitignore(1 hunks)backend/Dockerfile(1 hunks)backend/app/__init__.py(1 hunks)backend/app/config.py(1 hunks)backend/app/database.py(1 hunks)backend/app/main.py(1 hunks)backend/app/models/__init__.py(1 hunks)backend/app/models/deck.py(1 hunks)backend/app/models/flashcard.py(1 hunks)backend/app/models/review.py(1 hunks)backend/app/models/user.py(1 hunks)backend/app/routes/__init__.py(1 hunks)backend/app/routes/auth.py(1 hunks)backend/app/routes/decks.py(1 hunks)backend/app/routes/flashcards.py(1 hunks)backend/app/routes/practice.py(1 hunks)backend/app/routes/upload.py(1 hunks)backend/app/schemas/__init__.py(1 hunks)backend/app/schemas/deck.py(1 hunks)backend/app/schemas/flashcard.py(1 hunks)backend/app/schemas/review.py(1 hunks)backend/app/schemas/user.py(1 hunks)backend/app/services/__init__.py(1 hunks)backend/app/services/ai_service.py(1 hunks)backend/app/utils/__init__.py(1 hunks)backend/app/utils/auth.py(1 hunks)backend/app/utils/file_parser.py(1 hunks)backend/app/utils/spaced_repetition.py(1 hunks)backend/requirements.txt(1 hunks)backend/run.py(1 hunks)docker-compose.yml(1 hunks)frontend/.gitignore(1 hunks)frontend/Dockerfile(1 hunks)frontend/index.html(1 hunks)frontend/nginx.conf(1 hunks)frontend/package.json(1 hunks)frontend/postcss.config.js(1 hunks)frontend/src/App.tsx(1 hunks)frontend/src/components/ui/button.tsx(1 hunks)frontend/src/components/ui/card.tsx(1 hunks)frontend/src/components/ui/input.tsx(1 hunks)frontend/src/components/ui/label.tsx(1 hunks)frontend/src/components/ui/toast.tsx(1 hunks)frontend/src/components/ui/toaster.tsx(1 hunks)frontend/src/contexts/AuthContext.tsx(1 hunks)frontend/src/hooks/use-toast.ts(1 hunks)frontend/src/index.css(1 hunks)frontend/src/lib/api.ts(1 hunks)frontend/src/lib/utils.ts(1 hunks)frontend/src/main.tsx(1 hunks)frontend/src/pages/Dashboard.tsx(1 hunks)frontend/src/pages/DeckEditor.tsx(1 hunks)frontend/src/pages/Login.tsx(1 hunks)frontend/src/pages/Practice.tsx(1 hunks)frontend/src/pages/Signup.tsx(1 hunks)frontend/src/pages/Upload.tsx(1 hunks)frontend/src/types/index.ts(1 hunks)frontend/tailwind.config.js(1 hunks)frontend/tsconfig.json(1 hunks)frontend/tsconfig.node.json(1 hunks)frontend/vite.config.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (33)
frontend/src/pages/Login.tsx (2)
frontend/src/contexts/AuthContext.tsx (1)
useAuth(74-80)backend/app/routes/auth.py (1)
login(44-63)
frontend/src/pages/DeckEditor.tsx (2)
frontend/src/types/index.ts (2)
Deck(7-16)Flashcard(18-27)frontend/src/lib/api.ts (2)
decksAPI(47-71)flashcardsAPI(74-105)
frontend/src/components/ui/label.tsx (1)
frontend/src/lib/utils.ts (1)
cn(4-6)
backend/app/schemas/__init__.py (4)
backend/app/schemas/user.py (4)
UserCreate(7-11)UserLogin(14-18)UserResponse(21-29)Token(32-36)backend/app/schemas/deck.py (3)
DeckCreate(7-11)DeckUpdate(14-18)DeckResponse(21-34)backend/app/schemas/flashcard.py (3)
FlashcardCreate(7-12)FlashcardUpdate(15-19)FlashcardResponse(22-35)backend/app/schemas/review.py (2)
ReviewCreate(6-10)ReviewResponse(13-25)
frontend/src/contexts/AuthContext.tsx (4)
backend/app/models/user.py (1)
User(8-20)frontend/src/types/index.ts (3)
User(1-5)LoginCredentials(44-47)SignupCredentials(49-52)frontend/src/lib/api.ts (1)
authAPI(29-44)backend/app/routes/auth.py (2)
login(44-63)signup(22-40)
frontend/src/pages/Practice.tsx (2)
frontend/src/types/index.ts (2)
Deck(7-16)Flashcard(18-27)frontend/src/lib/api.ts (2)
decksAPI(47-71)practiceAPI(108-123)
frontend/src/components/ui/input.tsx (1)
frontend/src/lib/utils.ts (1)
cn(4-6)
frontend/src/pages/Upload.tsx (1)
frontend/src/lib/api.ts (1)
uploadAPI(126-144)
backend/app/schemas/deck.py (3)
backend/app/schemas/flashcard.py (1)
Config(34-35)backend/app/schemas/review.py (1)
Config(24-25)backend/app/schemas/user.py (1)
Config(28-29)
backend/app/schemas/review.py (3)
backend/app/schemas/deck.py (1)
Config(33-34)backend/app/schemas/flashcard.py (1)
Config(34-35)backend/app/schemas/user.py (1)
Config(28-29)
backend/app/utils/auth.py (3)
backend/app/database.py (1)
get_db(17-23)backend/app/models/user.py (1)
User(8-20)backend/app/schemas/user.py (1)
TokenData(39-42)
backend/app/models/flashcard.py (1)
frontend/src/types/index.ts (1)
Flashcard(18-27)
frontend/src/components/ui/toaster.tsx (2)
frontend/src/hooks/use-toast.ts (1)
useToast(186-186)frontend/src/components/ui/toast.tsx (6)
ToastProvider(120-120)Toast(122-122)ToastTitle(123-123)ToastDescription(124-124)ToastClose(125-125)ToastViewport(121-121)
frontend/src/App.tsx (2)
frontend/src/contexts/AuthContext.tsx (2)
useAuth(74-80)AuthProvider(16-72)frontend/src/components/ui/toaster.tsx (1)
Toaster(11-33)
frontend/src/pages/Signup.tsx (2)
frontend/src/contexts/AuthContext.tsx (1)
useAuth(74-80)backend/app/routes/auth.py (1)
signup(22-40)
frontend/src/components/ui/toast.tsx (1)
frontend/src/lib/utils.ts (1)
cn(4-6)
frontend/src/components/ui/button.tsx (1)
frontend/src/lib/utils.ts (1)
cn(4-6)
backend/app/schemas/flashcard.py (3)
backend/app/schemas/deck.py (1)
Config(33-34)backend/app/schemas/review.py (1)
Config(24-25)backend/app/schemas/user.py (1)
Config(28-29)
frontend/src/hooks/use-toast.ts (1)
frontend/src/components/ui/toast.tsx (3)
ToastProps(118-118)ToastActionElement(119-119)Toast(122-122)
frontend/src/types/index.ts (4)
backend/app/models/user.py (1)
User(8-20)backend/app/models/deck.py (1)
Deck(8-22)backend/app/models/flashcard.py (1)
Flashcard(8-22)backend/app/models/review.py (1)
Review(8-25)
backend/app/routes/decks.py (7)
backend/app/database.py (1)
get_db(17-23)backend/app/models/user.py (1)
User(8-20)backend/app/models/deck.py (1)
Deck(8-22)backend/app/models/flashcard.py (1)
Flashcard(8-22)backend/app/models/review.py (1)
Review(8-25)backend/app/schemas/deck.py (3)
DeckCreate(7-11)DeckUpdate(14-18)DeckResponse(21-34)backend/app/utils/auth.py (1)
get_current_user(45-68)
frontend/src/components/ui/card.tsx (1)
frontend/src/lib/utils.ts (1)
cn(4-6)
backend/app/routes/practice.py (9)
backend/app/database.py (1)
get_db(17-23)backend/app/models/user.py (1)
User(8-20)backend/app/models/deck.py (1)
Deck(8-22)backend/app/models/flashcard.py (1)
Flashcard(8-22)backend/app/models/review.py (1)
Review(8-25)backend/app/schemas/flashcard.py (1)
FlashcardResponse(22-35)backend/app/schemas/review.py (2)
ReviewCreate(6-10)ReviewResponse(13-25)backend/app/utils/auth.py (1)
get_current_user(45-68)backend/app/utils/spaced_repetition.py (2)
SpacedRepetition(6-45)calculate_next_review(10-33)
frontend/src/pages/Dashboard.tsx (3)
frontend/src/types/index.ts (2)
Deck(7-16)Flashcard(18-27)frontend/src/contexts/AuthContext.tsx (1)
useAuth(74-80)frontend/src/lib/api.ts (2)
decksAPI(47-71)practiceAPI(108-123)
backend/app/models/deck.py (1)
frontend/src/types/index.ts (1)
Deck(7-16)
backend/app/models/review.py (1)
frontend/src/types/index.ts (1)
Review(29-37)
backend/app/models/user.py (1)
frontend/src/types/index.ts (1)
User(1-5)
backend/app/routes/upload.py (9)
backend/app/database.py (1)
get_db(17-23)backend/app/models/user.py (1)
User(8-20)backend/app/models/deck.py (1)
Deck(8-22)backend/app/models/flashcard.py (1)
Flashcard(8-22)backend/app/schemas/deck.py (1)
DeckResponse(21-34)backend/app/schemas/flashcard.py (1)
FlashcardResponse(22-35)backend/app/utils/auth.py (1)
get_current_user(45-68)backend/app/utils/file_parser.py (3)
FileParser(11-114)parse_file(81-114)parse_youtube(50-78)backend/app/services/ai_service.py (2)
AIService(9-199)generate_flashcards(16-45)
backend/app/routes/flashcards.py (7)
backend/app/database.py (1)
get_db(17-23)backend/app/models/user.py (1)
User(8-20)backend/app/models/deck.py (1)
Deck(8-22)backend/app/models/flashcard.py (1)
Flashcard(8-22)backend/app/models/review.py (1)
Review(8-25)backend/app/schemas/flashcard.py (3)
FlashcardCreate(7-12)FlashcardUpdate(15-19)FlashcardResponse(22-35)backend/app/utils/auth.py (1)
get_current_user(45-68)
backend/app/routes/auth.py (4)
backend/app/database.py (1)
get_db(17-23)backend/app/models/user.py (1)
User(8-20)backend/app/schemas/user.py (4)
UserCreate(7-11)UserLogin(14-18)UserResponse(21-29)Token(32-36)backend/app/utils/auth.py (4)
verify_password(22-24)get_password_hash(27-29)create_access_token(32-42)get_current_user(45-68)
backend/app/models/__init__.py (5)
backend/app/models/user.py (1)
User(8-20)frontend/src/types/index.ts (4)
User(1-5)Deck(7-16)Flashcard(18-27)Review(29-37)backend/app/models/deck.py (1)
Deck(8-22)backend/app/models/flashcard.py (1)
Flashcard(8-22)backend/app/models/review.py (1)
Review(8-25)
backend/app/schemas/user.py (3)
backend/app/schemas/deck.py (1)
Config(33-34)backend/app/schemas/flashcard.py (1)
Config(34-35)backend/app/schemas/review.py (1)
Config(24-25)
frontend/src/lib/api.ts (1)
frontend/src/types/index.ts (7)
SignupCredentials(49-52)User(1-5)LoginCredentials(44-47)AuthTokens(39-42)Deck(7-16)Flashcard(18-27)Review(29-37)
🪛 ast-grep (0.39.7)
backend/app/utils/file_parser.py
[warning] 57-57: The function mktemp is deprecated. When using this function, it is possible for an attacker to modify the created file before the filename is returned. Use NamedTemporaryFile() instead and pass it the delete=False parameter.
Context: tempfile.mktemp()
Note: [CWE-377]: Insecure Temporary File [OWASP A01:2021]: Broken Access Control [REFERENCES]
https://docs.python.org/3/library/tempfile.html#tempfile.mktemp
https://owasp.org/Top10/A01_2021-Broken_Access_Control
(avoid-mktemp-python)
🪛 Checkov (3.2.334)
docker-compose.yml
[medium] 29-30: Basic Auth Credentials
(CKV_SECRET_4)
🪛 dotenv-linter (4.0.0)
backend/.env.example
[warning] 6-6: [UnorderedKey] The ALGORITHM key should go before the SECRET_KEY key
(UnorderedKey)
[warning] 7-7: [UnorderedKey] The ACCESS_TOKEN_EXPIRE_MINUTES key should go before the ALGORITHM key
(UnorderedKey)
[warning] 11-11: [UnorderedKey] The ANTHROPIC_API_KEY key should go before the OPENAI_API_KEY key
(UnorderedKey)
[warning] 15-15: [UnorderedKey] The MAX_UPLOAD_SIZE key should go before the UPLOAD_DIR key
(UnorderedKey)
[warning] 15-15: [ValueWithoutQuotes] This value needs to be surrounded in quotes
(ValueWithoutQuotes)
.env.example
[warning] 9-9: [UnorderedKey] The ANTHROPIC_API_KEY key should go before the OPENAI_API_KEY key
(UnorderedKey)
[warning] 13-13: [UnorderedKey] The MAX_UPLOAD_SIZE key should go before the UPLOAD_DIR key
(UnorderedKey)
🪛 markdownlint-cli2 (0.18.1)
README.md
38-38: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
105-105: Bare URL used
(MD034, no-bare-urls)
106-106: Bare URL used
(MD034, no-bare-urls)
107-107: Bare URL used
(MD034, no-bare-urls)
160-160: Bare URL used
(MD034, no-bare-urls)
179-179: Bare URL used
(MD034, no-bare-urls)
🪛 OSV Scanner (2.2.4)
backend/requirements.txt
[HIGH] 1-1: fastapi 0.109.0: undefined
(PYSEC-2024-38)
[CRITICAL] 1-1: python-jose 3.3.0: undefined
(PYSEC-2024-232)
[CRITICAL] 1-1: python-jose 3.3.0: undefined
(PYSEC-2024-233)
[CRITICAL] 1-1: python-jose 3.3.0: python-jose algorithm confusion with OpenSSH ECDSA keys
[CRITICAL] 1-1: python-jose 3.3.0: python-jose denial of service via compressed JWE content
[HIGH] 1-1: python-multipart 0.0.6: python-multipart vulnerable to Content-Type Header ReDoS
[HIGH] 1-1: python-multipart 0.0.6: Denial of service (DoS) via deformation multipart/form-data boundary
[HIGH] 1-1: yt-dlp 2024.1.4: yt-dlp has dependency on potentially malicious third-party code in Douyu extractors
[HIGH] 1-1: yt-dlp 2024.1.4: yt-dlp File system modification and RCE through improper file-extension sanitization
[HIGH] 1-1: yt-dlp 2024.1.4: yt-dlp: --exec command injection when using %q in yt-dlp on Windows (Bypass of CVE-2023-40581)
🪛 Ruff (0.14.3)
backend/app/schemas/__init__.py
7-20: __all__ is not sorted
Apply an isort-style sorting to __all__
(RUF022)
backend/app/utils/auth.py
46-46: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
62-62: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
backend/app/services/ai_service.py
37-37: Avoid specifying long messages outside the exception class
(TRY003)
45-45: Avoid specifying long messages outside the exception class
(TRY003)
50-50: Avoid specifying long messages outside the exception class
(TRY003)
89-89: Prefer TypeError exception for invalid type
(TRY004)
89-89: Abstract raise to an inner function
(TRY301)
89-89: Avoid specifying long messages outside the exception class
(TRY003)
93-93: Abstract raise to an inner function
(TRY301)
93-93: Avoid specifying long messages outside the exception class
(TRY003)
98-98: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
98-98: Avoid specifying long messages outside the exception class
(TRY003)
98-98: Use explicit conversion flag
Replace with conversion flag
(RUF010)
99-99: Do not catch blind exception: Exception
(BLE001)
100-100: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
100-100: Avoid specifying long messages outside the exception class
(TRY003)
100-100: Use explicit conversion flag
Replace with conversion flag
(RUF010)
105-105: Avoid specifying long messages outside the exception class
(TRY003)
144-144: Prefer TypeError exception for invalid type
(TRY004)
144-144: Abstract raise to an inner function
(TRY301)
144-144: Avoid specifying long messages outside the exception class
(TRY003)
148-148: Abstract raise to an inner function
(TRY301)
148-148: Avoid specifying long messages outside the exception class
(TRY003)
153-153: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
153-153: Avoid specifying long messages outside the exception class
(TRY003)
153-153: Use explicit conversion flag
Replace with conversion flag
(RUF010)
154-154: Do not catch blind exception: Exception
(BLE001)
155-155: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
155-155: Avoid specifying long messages outside the exception class
(TRY003)
155-155: Use explicit conversion flag
Replace with conversion flag
(RUF010)
175-175: Avoid specifying long messages outside the exception class
(TRY003)
backend/run.py
7-7: Possible binding to all interfaces
(S104)
backend/app/main.py
54-54: Possible binding to all interfaces
(S104)
backend/app/routes/decks.py
21-21: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
21-21: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
37-37: Comparison to None should be cond is None
Replace with cond is None
(E711)
61-61: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
62-62: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
80-80: Comparison to None should be cond is None
Replace with cond is None
(E711)
102-102: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
103-103: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
132-132: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
133-133: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
159-159: Comparison to None should be cond is None
Replace with cond is None
(E711)
179-179: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
180-180: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
backend/app/config.py
13-13: Possible hardcoded password assigned to: "SECRET_KEY"
(S105)
backend/app/utils/file_parser.py
23-23: Do not catch blind exception: Exception
(BLE001)
24-24: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
24-24: Avoid specifying long messages outside the exception class
(TRY003)
24-24: Use explicit conversion flag
Replace with conversion flag
(RUF010)
37-37: Do not catch blind exception: Exception
(BLE001)
38-38: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
38-38: Avoid specifying long messages outside the exception class
(TRY003)
38-38: Use explicit conversion flag
Replace with conversion flag
(RUF010)
46-46: Do not catch blind exception: Exception
(BLE001)
47-47: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
47-47: Avoid specifying long messages outside the exception class
(TRY003)
47-47: Use explicit conversion flag
Replace with conversion flag
(RUF010)
58-58: Use of insecure and deprecated function (mktemp)
(S306)
67-67: Probable use of requests call without timeout
(S113)
71-71: Probable use of requests call without timeout
(S113)
77-77: Do not catch blind exception: Exception
(BLE001)
78-78: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
78-78: Avoid specifying long messages outside the exception class
(TRY003)
78-78: Use explicit conversion flag
Replace with conversion flag
(RUF010)
backend/app/routes/practice.py
24-24: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
25-25: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
74-74: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
75-75: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
126-126: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
127-127: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
backend/app/routes/upload.py
24-24: Do not perform function call File in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
30-30: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
31-31: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
71-74: Abstract raise to an inner function
(TRY301)
77-80: Abstract raise to an inner function
(TRY301)
121-124: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
125-125: Do not catch blind exception: Exception
(BLE001)
126-129: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
128-128: Use explicit conversion flag
Replace with conversion flag
(RUF010)
137-137: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
138-138: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
179-179: Consider moving this statement to an else block
(TRY300)
182-185: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
186-186: Do not catch blind exception: Exception
(BLE001)
187-190: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
189-189: Use explicit conversion flag
Replace with conversion flag
(RUF010)
backend/app/routes/flashcards.py
20-20: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
21-21: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
68-68: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
69-69: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
110-110: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
111-111: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
148-148: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
149-149: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
199-199: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
200-200: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
backend/app/routes/auth.py
22-22: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
44-44: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
68-68: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
68-68: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
89-89: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable
(B008)
backend/app/models/__init__.py
7-7: __all__ is not sorted
Apply an isort-style sorting to __all__
(RUF022)
🔇 Additional comments (24)
backend/app/__init__.py (1)
1-1: Module initializer looks good.A minimal
__init__.pywith a docstring is a reasonable approach. If your package needs to expose a public API (models, services, configuration), consider adding strategic imports/exports here as the backend grows.README.md (1)
1-397: Comprehensive documentation with clear setup and usage guidance.The README provides excellent coverage of features, tech stack, project structure, setup procedures (both Docker and local), API endpoints, configuration, and troubleshooting. The spaced repetition algorithm explanation is helpful, and the production deployment section adds operational value. Clear structure makes it accessible to both new contributors and users.
frontend/.gitignore (1)
1-24: LGTM!Comprehensive gitignore patterns covering logs, dependencies, build artifacts, and editor files appropriate for a Vite/React project.
backend/app/services/__init__.py (1)
1-1: LGTM!Standard package initializer.
backend/app/utils/__init__.py (1)
1-1: LGTM!Standard package initializer.
backend/.env.example (1)
1-15: LGTM!Appropriate environment variable templates for backend configuration. The static analysis warnings about key ordering are stylistic preferences that don't affect functionality. The
ValueWithoutQuoteswarning forMAX_UPLOAD_SIZEis a false positive—numeric values don't require quotes in .env files.Note: Configuration duplication with root
.env.examplewas already flagged in the root file review.frontend/src/components/ui/label.tsx (1)
1-24: LGTM!Clean implementation following shadcn/ui patterns. Proper use of
forwardRef, TypeScript typing, and integration with Radix UI primitives. The peer-disabled styling ensures good accessibility.backend/.gitignore (1)
1-47: LGTM!Comprehensive gitignore covering Python artifacts, virtual environments, sensitive configs, uploads, and database files. Well-organized and appropriate for the backend service.
backend/run.py (1)
4-10: Binding to 0.0.0.0 is acceptable for development.Static analysis flags the
host="0.0.0.0"binding as a potential security risk. However, this is a development script (as indicated by the docstring andreload=True), and binding to all interfaces is standard practice to allow access when running in Docker containers.Ensure that production deployments use a more restrictive host binding or rely on proper network configuration.
frontend/src/main.tsx (1)
1-10: LGTM!Standard React 18 bootstrap pattern with StrictMode enabled. The non-null assertion on line 6 is safe given that the root element is guaranteed to exist in
index.html.frontend/vite.config.ts (1)
1-22: LGTM! Standard Vite configuration.The configuration correctly sets up React, path aliases, and development server with API proxying to the backend.
frontend/src/index.css (1)
1-59: LGTM! Standard Tailwind and shadcn/ui theming setup.The CSS properly integrates Tailwind directives and defines comprehensive theme variables for light and dark modes.
frontend/src/components/ui/toaster.tsx (1)
11-33: LGTM! Clean toast notification implementation.The component correctly integrates with the toast system, handles optional fields, and follows React best practices.
backend/app/database.py (1)
17-23: LGTM! Proper session lifecycle management.The dependency correctly yields a session and ensures cleanup in the finally block.
backend/app/models/__init__.py (1)
1-7: LGTM! Model exports are correctly defined.The module properly re-exports the core ORM models. The current order (User, then domain entities) is logical, though the linter suggests alphabetical sorting which you can optionally apply.
backend/app/config.py (1)
17-23: LGTM! AI and upload configuration looks reasonable.Optional API keys and explicit upload limits are well-structured. The 50MB upload limit is clearly expressed.
docker-compose.yml (1)
30-30: Ensure SECRET_KEY is documented as required for production.The default SECRET_KEY value includes a warning, but relying on defaults for security-critical values is risky. Ensure your README or deployment documentation emphasizes that SECRET_KEY must be set to a strong, random value in production.
frontend/src/contexts/AuthContext.tsx (1)
1-80: LGTM! Clean authentication context implementation.The authentication flow is well-structured with proper loading state management and token persistence. The auto-login after signup (lines 47-51) provides good UX, and error handling is appropriately delegated to consuming components.
backend/app/schemas/flashcard.py (1)
1-35: LGTM! Well-structured Pydantic schemas.The flashcard schemas are properly defined with appropriate field types and ORM integration via
from_attributes = True.backend/app/models/review.py (1)
1-25: LGTM! Appropriate model for spaced repetition tracking.The Review model correctly captures the essential spaced repetition fields. The required
next_reviewfield (line 20) ensures explicit scheduling of reviews, which is appropriate for the SM-2 algorithm.backend/app/utils/auth.py (1)
45-68: LGTM! Dependency injection is correctly implemented.The
Depends()calls in argument defaults (line 46) are the correct FastAPI pattern for dependency injection. The static analysis warnings are false positives and can be safely ignored.backend/app/routes/auth.py (1)
1-91: LGTM! Well-implemented authentication endpoints.The auth routes follow FastAPI best practices with appropriate error handling, status codes, and response models. The duplicate login endpoints (lines 43-63 for JSON, lines 66-85 for OAuth2 form) provide good API flexibility.
Note: The static analysis warnings about
Depends()in argument defaults are false positives—this is the correct FastAPI dependency injection pattern.frontend/nginx.conf (1)
1-36: LGTM! Well-configured nginx setup with appropriate optimizations.The configuration properly handles:
- SPA routing with fallback to index.html (lines 14-16)
- API proxying with necessary headers (lines 19-29)
- Static asset caching for performance (lines 32-35)
- Gzip compression for reduced bandwidth (lines 8-12)
Ensure the backend FastAPI app mounts routers with the
/apiprefix to match the proxy configuration at line 19.frontend/src/pages/Dashboard.tsx (1)
1-174: LGTM! Well-structured dashboard with good UX patterns.The implementation demonstrates good practices:
- Concurrent data loading with
Promise.all(lines 25-28) for better performance- Proper error handling with user feedback via toast (lines 31-36)
- Loading state management (lines 37-39)
- Event propagation handling for nested clickable elements (lines 148-149, 158-159)
- Responsive grid layout and conditional rendering
| # Database | ||
| DATABASE_URL=postgresql://flashcard_user:flashcard_password@localhost:5432/flashcards_db | ||
|
|
||
| # JWT Secret (generate a strong random key for production) | ||
| SECRET_KEY=your-super-secret-key-change-in-production | ||
|
|
||
| # AI API Keys (at least one is required) | ||
| OPENAI_API_KEY=sk-... | ||
| ANTHROPIC_API_KEY=sk-ant-... | ||
|
|
||
| # Application Settings | ||
| UPLOAD_DIR=./uploads | ||
| MAX_UPLOAD_SIZE=50000000 |
There was a problem hiding this comment.
Configuration duplication with backend/.env.example.
Both root .env.example and backend/.env.example define overlapping environment variables (DATABASE_URL, SECRET_KEY, API keys, upload settings). This creates ambiguity about which file should be used and maintained.
Consider one of these approaches:
- Option 1 (recommended): Keep only
backend/.env.examplesince the backend is the primary consumer of these variables, and remove the root-level file. - Option 2: If multiple services need separate configs, document clearly in README which file each service uses and ensure
backend/app/config.pyexplicitly references the correct path.
🧰 Tools
🪛 dotenv-linter (4.0.0)
[warning] 9-9: [UnorderedKey] The ANTHROPIC_API_KEY key should go before the OPENAI_API_KEY key
(UnorderedKey)
[warning] 13-13: [UnorderedKey] The MAX_UPLOAD_SIZE key should go before the UPLOAD_DIR key
(UnorderedKey)
🤖 Prompt for AI Agents
In .env.example around lines 1-13, these environment variables duplicate
backend/.env.example causing ambiguity; choose the recommended Option 1: remove
the root-level .env.example file and ensure backend/.env.example contains the
canonical DATABASE_URL, SECRET_KEY, API keys and upload settings; update any
project scripts/CI/docs that referenced the root .env to point to backend/.env
and run a quick grep to replace references to the root file so only
backend/.env.example is maintained.
| """Application settings""" | ||
|
|
||
| # Database | ||
| DATABASE_URL: str = "postgresql://user:password@localhost:5432/flashcards_db" |
There was a problem hiding this comment.
Remove hardcoded credentials from default DATABASE_URL.
The default contains user:password, which is a security risk if the environment variable is not set. This could lead to accidental exposure or use in development/staging.
Apply this diff:
- DATABASE_URL: str = "postgresql://user:password@localhost:5432/flashcards_db"
+ DATABASE_URL: strThis forces the environment variable to be set explicitly. Update the README or provide a .env.example to document the expected format.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| DATABASE_URL: str = "postgresql://user:password@localhost:5432/flashcards_db" | |
| DATABASE_URL: str |
🤖 Prompt for AI Agents
In backend/app/config.py around line 10, remove the hardcoded DATABASE_URL value
containing "user:password" and instead require the DATABASE_URL to come from the
environment; if the env var is missing or empty, raise an explicit error (or
fail fast) so the app cannot start with default credentials. Update the code to
read the value from the environment and validate presence, and then document the
required variable format in README or add a .env.example as suggested.
| SECRET_KEY: str = "your-secret-key-change-in-production" | ||
| ALGORITHM: str = "HS256" | ||
| ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 |
There was a problem hiding this comment.
Remove insecure default SECRET_KEY and enforce explicit configuration.
The default "your-secret-key-change-in-production" is a critical security vulnerability. If .env is missing or misconfigured, JWT tokens will be signed with this weak, publicly known key, compromising all authentication.
Apply this diff:
- SECRET_KEY: str = "your-secret-key-change-in-production"
+ SECRET_KEY: str
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30This forces explicit configuration. Document in README that SECRET_KEY must be a cryptographically random string (e.g., generated via openssl rand -hex 32).
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| SECRET_KEY: str = "your-secret-key-change-in-production" | |
| ALGORITHM: str = "HS256" | |
| ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 | |
| SECRET_KEY: str | |
| ALGORITHM: str = "HS256" | |
| ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 |
🧰 Tools
🪛 Ruff (0.14.3)
13-13: Possible hardcoded password assigned to: "SECRET_KEY"
(S105)
🤖 Prompt for AI Agents
In backend/app/config.py around lines 13 to 15, remove the insecure hardcoded
default SECRET_KEY and require an explicit environment value; change the code to
read SECRET_KEY from the environment (no fallback) and raise a clear startup
error if it's missing or too short/weak (e.g., enforce minimum byte/hex length),
and update README to document that SECRET_KEY must be a cryptographically random
string (example: openssl rand -hex 32) and how to set it in .env or env vars.
| due_count = ( | ||
| db.query(Flashcard) | ||
| .outerjoin(Review) | ||
| .filter(Flashcard.deck_id == deck.id) | ||
| .filter( | ||
| (Review.next_review <= datetime.utcnow()) | (Review.next_review == None) | ||
| ) | ||
| .distinct() | ||
| .count() | ||
| ) |
There was a problem hiding this comment.
Scope due_count to the current user’s latest review
The due_count query joins every review row, so it (a) includes other users’ reviews and (b) marks a card as due if any historical review is overdue—even when the current user’s latest review is scheduled in the future. That makes the dashboard show inflated due counts. Reuse the same “latest review per user” logic you apply in the practice routes and check due-ness against that single row.
Apply this diff (replicate the pattern in each due_count block):
- due_count = (
- db.query(Flashcard)
- .outerjoin(Review)
- .filter(Flashcard.deck_id == deck.id)
- .filter(
- (Review.next_review <= datetime.utcnow()) | (Review.next_review == None)
- )
- .distinct()
- .count()
- )
+ due_count = 0
+ cards = db.query(Flashcard).filter(Flashcard.deck_id == deck.id).all()
+ for card in cards:
+ latest_review = (
+ db.query(Review)
+ .filter(
+ Review.flashcard_id == card.id,
+ Review.user_id == current_user.id,
+ )
+ .order_by(Review.reviewed_at.desc())
+ .first()
+ )
+ if not latest_review or SpacedRepetition.is_due(latest_review.next_review):
+ due_count += 1Remember to import SpacedRepetition at the top and apply the same fix in get_deck and update_deck.
Also applies to: 77-83, 156-162
🧰 Tools
🪛 Ruff (0.14.3)
37-37: Comparison to None should be cond is None
Replace with cond is None
(E711)
🤖 Prompt for AI Agents
In backend/app/routes/decks.py around lines 32-41, the due_count query currently
joins all Review rows causing counts to include other users' reviews and
historical reviews; replace it with the "latest review per user" pattern used in
practice routes: join/Subquery to select only the latest Review per
(flashcard_id, user_id) (using SpacedRepetition.latest_review logic), then join
that subquery to Flashcard and filter due-ness against that single
latest_review.next_review (next_review <= datetime.utcnow() or next_review is
NULL). Import SpacedRepetition at the top of the file and apply the same
replacement for the other due_count blocks at lines 77-83 and 156-162, and
likewise update get_deck and update_deck to use the latest-review-scoped query
when computing due_count.
| if not latest_review or latest_review.next_review <= datetime.utcnow(): | ||
| card_dict = { | ||
| "id": card.id, | ||
| "deck_id": card.deck_id, | ||
| "question": card.question, | ||
| "answer": card.answer, | ||
| "created_at": card.created_at, | ||
| "updated_at": card.updated_at, | ||
| "next_review": latest_review.next_review if latest_review else None, | ||
| "interval_days": latest_review.interval_days if latest_review else None, | ||
| } |
There was a problem hiding this comment.
Fix timezone-aware due checks
Review.next_review is stored with timezone information (SQLAlchemy DateTime(timezone=True)), so the ORM returns offset-aware datetimes. Comparing them with datetime.utcnow() creates a naïve-versus-aware comparison, which raises a TypeError: can't compare offset-naive and offset-aware datetimes as soon as a reviewed card is fetched. This breaks both /practice/deck/{deck_id}/due and /practice/today. Switch to a timezone-aware “now” before comparing.
Apply this diff:
-from datetime import datetime
+from datetime import datetime, timezone
@@
- if not latest_review or latest_review.next_review <= datetime.utcnow():
+ if not latest_review or latest_review.next_review <= datetime.now(timezone.utc):
@@
- if not latest_review or latest_review.next_review <= datetime.utcnow():
+ if not latest_review or latest_review.next_review <= datetime.now(timezone.utc):Also applies to: 154-164
🤖 Prompt for AI Agents
In backend/app/routes/practice.py around lines 55-65 (and also apply the same
change at lines 154-164), you are comparing offset-aware Review.next_review
datetimes to a naïve datetime.utcnow(), causing a TypeError; update the code to
use a timezone-aware "now" (for example datetime.now(timezone.utc) or
datetime.utcnow().replace(tzinfo=timezone.utc)) wherever datetime.utcnow() is
used for these comparisons so the left and right operands are both offset-aware;
replace each datetime.utcnow() in these blocks with datetime.now(timezone.utc)
(and add "from datetime import timezone" if not already imported).
| COPY package*.json ./ | ||
|
|
||
| # Install dependencies | ||
| RUN npm install |
There was a problem hiding this comment.
Use npm ci for reproducible builds.
Using npm install without enforcing the lockfile can lead to inconsistent builds across environments. This could introduce subtle bugs or dependency conflicts.
Apply this diff:
-RUN npm install
+RUN npm ciThis ensures the exact dependency tree from package-lock.json is installed.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| RUN npm install | |
| RUN npm ci |
🤖 Prompt for AI Agents
In frontend/Dockerfile around line 9, the Dockerfile uses "RUN npm install"
which can produce non-reproducible installs; replace it with "RUN npm ci" so the
image installs exact versions from package-lock.json, ensuring reproducible
builds and failing if lockfile is out of sync.
| "dependencies": { | ||
| "react": "^18.2.0", | ||
| "react-dom": "^18.2.0", | ||
| "react-router-dom": "^6.21.1", | ||
| "axios": "^1.6.5", | ||
| "@radix-ui/react-alert-dialog": "^1.0.5", | ||
| "@radix-ui/react-dialog": "^1.0.5", | ||
| "@radix-ui/react-dropdown-menu": "^2.0.6", | ||
| "@radix-ui/react-label": "^2.0.2", | ||
| "@radix-ui/react-select": "^2.0.0", | ||
| "@radix-ui/react-slot": "^1.0.2", | ||
| "@radix-ui/react-toast": "^1.1.5", | ||
| "class-variance-authority": "^0.7.0", | ||
| "clsx": "^2.1.0", | ||
| "tailwind-merge": "^2.2.0", | ||
| "lucide-react": "^0.309.0" |
There was a problem hiding this comment.
🧩 Analysis chain
Verify dependency versions for security updates.
The specified versions are from late 2023/early 2024. Given the current date (November 2025), these packages may have security patches or important updates available.
Run the following script to check for outdated packages and security advisories:
🏁 Script executed:
#!/bin/bash
# Check for latest versions and security advisories for key frontend dependencies
echo "=== Checking React ==="
curl -s https://registry.npmjs.org/react | jq -r '.["dist-tags"].latest'
echo "=== Checking Axios ==="
curl -s https://registry.npmjs.org/axios | jq -r '.["dist-tags"].latest'
echo "=== Checking React Router ==="
curl -s https://registry.npmjs.org/react-router-dom | jq -r '.["dist-tags"].latest'
echo "=== Security advisories for axios ==="
gh api graphql -f query='
{
securityVulnerabilities(first: 5, ecosystem: NPM, package: "axios") {
nodes {
advisory {
summary
severity
publishedAt
}
vulnerableVersionRange
firstPatchedVersion {
identifier
}
}
}
}'Length of output: 1396
Update axios to resolve critical security vulnerabilities.
The specified version (^1.6.5) includes axios 1.6.5, which has HIGH-severity vulnerabilities:
- Denial-of-service attack through lack of data size check (patched in 1.12.0)
- SSRF and credential leakage via absolute URL (patched in 1.8.2)
Upgrade to at least ^1.12.0 or higher (latest: 1.13.2) to resolve these issues.
🤖 Prompt for AI Agents
In frontend/package.json around lines 12 to 27 the axios dependency is pinned to
^1.6.5 which contains known high-severity vulnerabilities; update the axios
entry to at least ^1.12.0 (preferably the latest 1.13.2), then run your package
manager to reinstall and update the lockfile (npm install / yarn install / pnpm
install), run the test suite and any vulnerability audits (npm audit / yarn
audit) and commit the updated package.json and lockfile so the fixed version is
pinned in CI.
| const CardTitle = React.forwardRef< | ||
| HTMLParagraphElement, | ||
| React.HTMLAttributes<HTMLHeadingElement> |
There was a problem hiding this comment.
Fix the ref type mismatch.
The ref is typed as HTMLParagraphElement but the component renders an h3 element, which is HTMLHeadingElement. This type inconsistency could cause type errors when consumers try to access heading-specific properties on the ref.
Apply this diff to fix the type:
const CardTitle = React.forwardRef<
- HTMLParagraphElement,
+ HTMLHeadingElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const CardTitle = React.forwardRef< | |
| HTMLParagraphElement, | |
| React.HTMLAttributes<HTMLHeadingElement> | |
| const CardTitle = React.forwardRef< | |
| HTMLHeadingElement, | |
| React.HTMLAttributes<HTMLHeadingElement> | |
| >(({ className, ...props }, ref) => ( |
🤖 Prompt for AI Agents
In frontend/src/components/ui/card.tsx around lines 32 to 34, the forwardRef
generics use HTMLParagraphElement for the ref while the component renders an h3
(an HTMLHeadingElement); change the first generic type from HTMLParagraphElement
to HTMLHeadingElement so the ref type matches the rendered element, keeping the
existing React.HTMLAttributes<HTMLHeadingElement> for props.
| setDeck(deckData); | ||
| setCards(cardsData); | ||
| } else { | ||
| // Practice all due cards | ||
| const cardsData = await practiceAPI.getTodaysReviews(); | ||
| setCards(cardsData); | ||
| } |
There was a problem hiding this comment.
Reset practice state when reloading cards
Switching from /practice/:deckId to another deck reuses this component, so currentIndex and showAnswer retain their previous values. If the previous deck left you at, say, index 5 but the new deck only returns one due card, cards[currentIndex] is undefined and the render attempts to read .question, throwing a runtime TypeError. Reset the practice state each time a fresh card list arrives.
@@
const [deckData, cardsData] = await Promise.all([
decksAPI.getById(parseInt(deckId)),
practiceAPI.getDueCards(parseInt(deckId)),
]);
setDeck(deckData);
setCards(cardsData);
+ setCurrentIndex(0);
+ setShowAnswer(false);
} else {
// Practice all due cards
const cardsData = await practiceAPI.getTodaysReviews();
setCards(cardsData);
+ setCurrentIndex(0);
+ setShowAnswer(false);
}Also applies to: 35-37
🤖 Prompt for AI Agents
In frontend/src/pages/Practice.tsx around lines 31 to 37, when replacing the
card list (either by setting deck/cards for a specific deck or loading today's
reviews) the component reuses prior state so currentIndex and showAnswer can
point past the new list and cause cards[currentIndex] to be undefined; reset the
practice state whenever you set new cards (and when setting deck) by setting
currentIndex back to 0 (or -1 if you prefer starting state) and
setShowAnswer(false) so renders won't access out-of-range items; apply the same
reset in both branches (lines 31-37 and 35-37) wherever cards are replaced.
| <Label htmlFor="numCards">Number of Flashcards</Label> | ||
| <Input | ||
| id="numCards" | ||
| type="number" | ||
| min="1" | ||
| max="50" | ||
| value={numCards} | ||
| onChange={(e) => setNumCards(parseInt(e.target.value))} | ||
| /> |
There was a problem hiding this comment.
Guard numCards against NaN.
parseInt on the number input emits NaN whenever the field is cleared or an intermediate invalid value is typed. React then complains about receiving NaN for a controlled input, and the form submits "NaN" to the backend, which breaks validation for upload requests. Please sanitize the value before updating state (and clamp it to the allowed range) so we never store NaN.
Apply this diff to harden the handler:
- <Input
+ <Input
id="numCards"
type="number"
min="1"
max="50"
value={numCards}
- onChange={(e) => setNumCards(parseInt(e.target.value))}
+ onChange={(e) => {
+ const value = Number.parseInt(e.target.value, 10);
+ if (Number.isNaN(value)) {
+ return;
+ }
+ setNumCards(Math.min(Math.max(value, 1), 50));
+ }}
/>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <Label htmlFor="numCards">Number of Flashcards</Label> | |
| <Input | |
| id="numCards" | |
| type="number" | |
| min="1" | |
| max="50" | |
| value={numCards} | |
| onChange={(e) => setNumCards(parseInt(e.target.value))} | |
| /> | |
| <Label htmlFor="numCards">Number of Flashcards</Label> | |
| <Input | |
| id="numCards" | |
| type="number" | |
| min="1" | |
| max="50" | |
| value={numCards} | |
| onChange={(e) => { | |
| const value = Number.parseInt(e.target.value, 10); | |
| if (Number.isNaN(value)) { | |
| return; | |
| } | |
| setNumCards(Math.min(Math.max(value, 1), 50)); | |
| }} | |
| /> |
🤖 Prompt for AI Agents
In frontend/src/pages/Upload.tsx around lines 215 to 223, the onChange handler
for numCards uses parseInt and can set state to NaN when the input is cleared or
temporarily invalid; change the handler to parse the value, test
Number.isNaN(parsed), and if NaN set a safe fallback (e.g., 1), otherwise clamp
the parsed value into the allowed range using Math.max(1, Math.min(50, parsed))
before calling setNumCards so the state never holds NaN and always respects the
1–50 bounds.
Core Features:
✅ AI-powered flashcard generation from multiple sources ✅ Spaced repetition system (SM-2 simplified)
✅ User authentication with JWT
✅ Full deck management (CRUD)
✅ Interactive practice mode with flip animations
✅ Support for PDF, PPTX, text, and YouTube
Tech Stack:
Backend:
Frontend:
Infrastructure:
Project Structure:
See README.md for setup and usage instructions.
Summary by CodeRabbit
New Features
Documentation
Chores