From 55cc1c82cd40bf2d5813b917e35fa6fe1a936b0e Mon Sep 17 00:00:00 2001 From: David Gomes Date: Mon, 20 Apr 2026 11:36:26 -0700 Subject: [PATCH 1/6] Implement user authentication with JWT, including login and registration features --- backend/api/serializers.py | 25 ++++++ backend/api/urls.py | 5 ++ backend/api/views.py | 15 +++- backend/cheat_sheet/settings.py | 4 + backend/requirements.txt | 1 + frontend/package-lock.json | 70 +++++++++++++++- frontend/package.json | 4 +- frontend/src/App.css | 10 ++- frontend/src/App.jsx | 51 +++++++++--- frontend/src/components/Login.jsx | 52 ++++++++++++ frontend/src/components/SignUp.jsx | 52 ++++++++++++ frontend/src/context/AuthContext.jsx | 119 +++++++++++++++++++++++++++ frontend/src/main.jsx | 8 +- 13 files changed, 399 insertions(+), 17 deletions(-) create mode 100644 frontend/src/components/Login.jsx create mode 100644 frontend/src/components/SignUp.jsx create mode 100644 frontend/src/context/AuthContext.jsx diff --git a/backend/api/serializers.py b/backend/api/serializers.py index 7d8b0f5..6546fae 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -1,8 +1,33 @@ # DRF serializers for the backend API will be added here. +from django.contrib.auth.models import User from rest_framework import serializers +from rest_framework_simplejwt.serializers import TokenObtainPairSerializer from .models import Template, CheatSheet, PracticeProblem +class CustomTokenObtainPairSerializer(TokenObtainPairSerializer): + @classmethod + def get_token(cls, user): + token = super().get_token(user) + # Add custom claims + token['username'] = user.username + return token + + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ('id', 'username', 'password') + extra_kwargs = {'password': {'write_only': True}} + + def create(self, validated_data): + user = User.objects.create_user( + username=validated_data['username'], + password=validated_data['password'] + ) + return user + + class TemplateSerializer(serializers.ModelSerializer): class Meta: model = Template diff --git a/backend/api/urls.py b/backend/api/urls.py index 748584e..1fc76a9 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -1,5 +1,6 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter +from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView from . import views # Create a router and register our viewsets with it. @@ -9,6 +10,10 @@ router.register(r'problems', views.PracticeProblemViewSet, basename='problem') urlpatterns = [ + path('token/', views.CustomTokenObtainPairView.as_view(), name='token_obtain_pair'), + path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), + path('register/', views.RegisterView.as_view(), name='register'), + path("health/", views.health_check, name="health-check"), path("classes/", views.get_classes, name="get-classes"), path("generate-sheet/", views.generate_sheet, name="generate-sheet"), diff --git a/backend/api/views.py b/backend/api/views.py index 23bd3be..498736f 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -2,13 +2,17 @@ from rest_framework.response import Response from rest_framework import status, viewsets from django.http import FileResponse +from django.contrib.auth.models import User +from rest_framework.generics import CreateAPIView +from rest_framework.permissions import AllowAny from django.shortcuts import get_object_or_404 import subprocess import tempfile import os from .models import Template, CheatSheet, PracticeProblem -from .serializers import TemplateSerializer, CheatSheetSerializer, PracticeProblemSerializer +from .serializers import TemplateSerializer, CheatSheetSerializer, PracticeProblemSerializer, UserSerializer, CustomTokenObtainPairSerializer +from rest_framework_simplejwt.views import TokenObtainPairView from .formula_data import get_formula_data, get_classes_with_details, get_special_class_formula, is_special_class from .latex_utils import build_latex_for_formulas, LATEX_HEADER, LATEX_FOOTER @@ -41,6 +45,15 @@ def validate_layout_params(columns, font_size, margins, spacing): # API endpoints # ------------------------------------------------------------------ +class CustomTokenObtainPairView(TokenObtainPairView): + serializer_class = CustomTokenObtainPairSerializer + +class RegisterView(CreateAPIView): + queryset = User.objects.all() + permission_classes = (AllowAny,) + serializer_class = UserSerializer + + @api_view(["GET"]) def health_check(request): return Response({"status": "ok"}) diff --git a/backend/cheat_sheet/settings.py b/backend/cheat_sheet/settings.py index 2e90c30..99ec04c 100644 --- a/backend/cheat_sheet/settings.py +++ b/backend/cheat_sheet/settings.py @@ -44,6 +44,7 @@ "django.contrib.staticfiles", # Third-party "rest_framework", + "rest_framework_simplejwt", "corsheaders", # Local "api", @@ -115,6 +116,9 @@ # DRF REST_FRAMEWORK = { + "DEFAULT_AUTHENTICATION_CLASSES": ( + "rest_framework_simplejwt.authentication.JWTAuthentication", + ), "DEFAULT_PERMISSION_CLASSES": [ "rest_framework.permissions.AllowAny", ], diff --git a/backend/requirements.txt b/backend/requirements.txt index 2589325..e0dd8b6 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,5 +1,6 @@ django>=6.0,<7.0 djangorestframework>=3.15 +djangorestframework-simplejwt>=5.3.1 django-cors-headers>=4.4 python-dotenv>=1.0,<2.0 dj-database-url>=2.1 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6253c04..b3b0814 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,9 +11,11 @@ "@dnd-kit/core": "^6.1.0", "@dnd-kit/sortable": "^8.0.0", "@dnd-kit/utilities": "^3.2.2", + "jwt-decode": "^4.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-pdf": "^10.4.1" + "react-pdf": "^10.4.1", + "react-router-dom": "^7.14.1" }, "devDependencies": { "@eslint/js": "^9.0.0", @@ -2197,6 +2199,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3758,6 +3773,15 @@ "node": ">=4.0" } }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4334,6 +4358,44 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.1.tgz", + "integrity": "sha512-5BCvFskyAAVumqhEKh/iPhLOIkfxcEUz8WqFIARCkMg8hZZzDYX9CtwxXA0e+qT8zAxmMC0x3Ckb9iMONwc5jg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.1.tgz", + "integrity": "sha512-ZkrQuwwhGibjQLqH1eCdyiZyLWglPxzxdl5tgwgKEyCSGC76vmAjleGocRe3J/MLfzMUIKwaFJWpFVJhK3d2xA==", + "license": "MIT", + "dependencies": { + "react-router": "7.14.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -4531,6 +4593,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index f3ff1c7..8a0b9b6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,9 +14,11 @@ "@dnd-kit/core": "^6.1.0", "@dnd-kit/sortable": "^8.0.0", "@dnd-kit/utilities": "^3.2.2", + "jwt-decode": "^4.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-pdf": "^10.4.1" + "react-pdf": "^10.4.1", + "react-router-dom": "^7.14.1" }, "devDependencies": { "@eslint/js": "^9.0.0", diff --git a/frontend/src/App.css b/frontend/src/App.css index 96fd5b6..c7578cf 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -460,16 +460,24 @@ label { justify-content: center; gap: var(--space-xs); padding: 0.625rem 1.25rem; - border: 1px solid transparent; + border: 1px solid var(--border); border-radius: var(--radius-md); + background-color: var(--card-bg); + color: var(--text); cursor: pointer; font-weight: 500; font-size: 0.875rem; letter-spacing: 0.01em; + text-decoration: none; transition: all var(--transition-fast); box-shadow: var(--shadow-sm); } +.btn:hover { + background-color: var(--border-subtle); + border-color: var(--primary); +} + .btn:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3); diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 6061728..752a1ca 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,4 +1,8 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect, useContext } from 'react' +import { Routes, Route, Link } from 'react-router-dom'; +import AuthContext from './context/AuthContext'; +import Login from './components/Login'; +import SignUp from './components/SignUp'; import './App.css' import CreateCheatSheet from './components/CreateCheatSheet'; @@ -39,6 +43,8 @@ function App() { localStorage.setItem('theme', theme); }, [theme]); + const { user, logoutUser } = useContext(AuthContext); + const toggleTheme = () => { setTheme(prev => prev === 'dark' ? 'light' : 'dark'); }; @@ -122,22 +128,43 @@ function App() {
-

Cheat Sheet Generator

+ +

Cheat Sheet Generator

+

Write cheat sheets with LaTeX support

- +
+ {user ? ( + <> + Hi, {user.username || 'User'} + + + ) : ( + <> + Log In + Sign Up + + )} + +
- {}} - /> + + {}} + /> + } /> + } /> + } /> +