Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b276d91
chore: add logger for auth and web apps
A-lexisL Oct 7, 2025
6d5f8f1
refactor: coursedetail, courselist, review related to class based vie…
A-lexisL Oct 8, 2025
54a81d4
refactor: two hierarchy for reviews, course-relevant or user-releavan…
A-lexisL Oct 9, 2025
6bf01e8
refactor: combine review_form to ReviewSerializer and calculate revie…
A-lexisL Oct 13, 2025
b2a4d24
docs: add api doc for useful endpoint in apps.web
A-lexisL Oct 14, 2025
460b828
chore: update config and lock dep version
A-lexisL Nov 9, 2025
a237322
refactor: rearrange urls under their own apps
A-lexisL Nov 9, 2025
f4a7e42
fix: rebase conflict
A-lexisL Nov 20, 2025
047c2cf
fix: N+1 issue for review votes
A-lexisL Nov 30, 2025
3734879
style: queryset_raw to raw_queryset
A-lexisL Nov 30, 2025
9f877cd
fix: add csrf check for logout signup reset
A-lexisL Dec 4, 2025
79633ba
fix: use annotation for course scores to fix race condition and N+1
A-lexisL Dec 4, 2025
3414439
fix: ReviewManager.with_votes request_user param renamed to vote_user
A-lexisL Dec 4, 2025
92cffb2
chore: add celery dep
A-lexisL Dec 23, 2025
875949a
fix: use serializer for examining input of vote apis
A-lexisL Dec 23, 2025
53ec588
fix: N+1 issue in course offering
A-lexisL Dec 23, 2025
9e5d3fa
chore: rm celery dep
A-lexisL Dec 23, 2025
1d45ff2
chore: change pre-commit to prek
A-lexisL Dec 23, 2025
f3cf237
docs: update env example and docstrings for unused api
A-lexisL Dec 23, 2025
415af54
fix(auth)!: Login user after signup
PACHAKUTlQ Dec 23, 2025
54e3342
fix(auth): Make sure length_step is non-zero
PACHAKUTlQ Dec 23, 2025
68b3ed0
chore: Add ruff as dev dependency
PACHAKUTlQ Dec 23, 2025
e729301
fix(auth): Add CSRF protection for logout api
PACHAKUTlQ Dec 23, 2025
8f851fc
fix(auth): Add CSRF protection for login api
PACHAKUTlQ Dec 23, 2025
cd09a01
feat(auth): Add explicit `@permission_classes([AllowAny])` for clarity
PACHAKUTlQ Dec 23, 2025
20949e2
fix(auth): Use `user.has_usable_password()` to account for users with…
PACHAKUTlQ Dec 23, 2025
79f9c42
docs(auth): Make auth initiate API /api/auth/init, which is consisten…
PACHAKUTlQ Dec 23, 2025
4ba49c3
Revert "chore: Add ruff as dev dependency"
PACHAKUTlQ Dec 23, 2025
e9effe1
fix: rm csrf check for password login
A-lexisL Dec 26, 2025
2b31769
refactor: change reset to reset_password in action list
A-lexisL Dec 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,17 @@ QUEST__LOGIN__API_KEY=dummy2
# QUEST__LOGIN__URL=
# QUEST__LOGIN__QUESTIONID=

QUEST__RESET__API_KEY=dummy3
# QUEST__RESET__URL=
# QUEST__RESET__QUESTIONID=
QUEST__RESET_PASSWORD__API_KEY=dummy3
# QUEST__RESET_PASSWORD__URL=
# QUEST__RESET_PASSWORD__QUESTIONID=

# --- Other Overrides (Optional) ---
# Example of overriding a nested value in the AUTH dictionary
# AUTH__OTP_TIMEOUT=60
# Example of overridng web size constraints
# WEB__COURSE__PAGE_SIZE=5
# WEB__REVIEW__PAGE_SIZE=10
# WEB__REVIEW__COMMENT_MIN_LENGTH=30

# Example of overriding a list with a comma-separated string
# ALLOWED_HOSTS=localhost,127.0.0.1,dev.my-app.com
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ help:
@echo " collect - Collects Django static files"
@echo " install-frontend - Installs frontend dependencies using bun"
@echo " format - Formats both backend (Python) and frontend (JS/TS/CSS) code"
@echo " format-backend - Formats Python code using isort and black"
@echo " format-backend - Formats Python code using ruff check and format"
@echo " format-frontend - Formats frontend code using prettier"
@echo " lint - Lints both backend (Python) and frontend (JS/TS/CSS) code"
@echo " lint-backend - Lints Python code using ruff"
Expand Down Expand Up @@ -45,8 +45,8 @@ format: format-backend format-frontend
@echo "All code formatted successfully!"

format-backend:
@echo "Formatting backend (Python) code with isort and black..."
uv run ruff check --select I . && \
@echo "Formatting backend (Python) code with ruff check and format..."
uv run ruff check --select I . --fix && \
uv run ruff format

format-frontend:
Expand Down
16 changes: 16 additions & 0 deletions apps/auth/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from django.urls import re_path

from apps.auth import views as auth_views

urlpatterns = [
re_path(r"^init/$", auth_views.auth_initiate_api, name="auth_initiate_api"),
re_path(r"^verify/$", auth_views.verify_callback_api, name="verify_callback_api"),
re_path(
r"^password/$",
auth_views.auth_reset_password_api,
name="auth_reset_password_api",
),
re_path(r"^signup/$", auth_views.auth_signup_api, name="auth_signup_api"),
re_path(r"^login/$", auth_views.auth_login_api, name="auth_login_api"),
re_path(r"^logout/?$", auth_views.auth_logout_api, name="auth_logout_api"),
]
38 changes: 24 additions & 14 deletions apps/auth/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError
from rest_framework.authentication import SessionAuthentication
from rest_framework.response import Response

from apps.web.models import Student

logger = logging.getLogger(__name__)

AUTH_SETTINGS = settings.AUTH
PASSWORD_LENGTH_MIN = AUTH_SETTINGS["PASSWORD_LENGTH_MIN"]
PASSWORD_LENGTH_MAX = AUTH_SETTINGS["PASSWORD_LENGTH_MAX"]
Expand All @@ -23,22 +26,28 @@
QUEST_BASE_URL = QUEST_SETTINGS["BASE_URL"]


class CSRFCheckSessionAuthentication(SessionAuthentication):
def authenticate(self, request):
super().enforce_csrf(request)
return super().authenticate(request)


def get_survey_details(action: str) -> dict[str, Any] | None:
"""
A single, clean function to get all survey details for a given action.
Valid actions: "signup", "login", "reset".
Valid actions: "signup", "login", "reset_password".
"""

action_details = QUEST_SETTINGS.get(action.upper())

if not action_details:
logging.error("Invalid quest action requested: %s", action)
logger.error("Invalid quest action requested: %s", action)
return None

try:
question_id = int(action_details.get("QUESTIONID"))
except (ValueError, TypeError):
logging.error(
logger.error(
"Could not parse 'QUESTIONID' for action '%s'. Check your settings.", action
)
return None
Expand Down Expand Up @@ -66,18 +75,18 @@ async def verify_turnstile_token(
},
)
if not response.json().get("success"):
logging.warning("Turnstile verification failed: %s", response.json())
logger.warning("Turnstile verification failed: %s", response.json())
return False, Response(
{"error": "Turnstile verification failed"}, status=403
)
return True, None
except httpx.TimeoutException:
logging.error("Turnstile verification timed out")
logger.error("Turnstile verification timed out")
return False, Response(
{"error": "Turnstile verification timed out"}, status=504
)
except Exception as e:
logging.error(f"Error verifying Turnstile token: {e}")
except Exception:
logger.error("Turnstile verification error")
return False, Response({"error": "Turnstile verification error"}, status=500)


Expand Down Expand Up @@ -132,19 +141,19 @@ async def get_latest_answer(
response.raise_for_status() # Raise an exception for bad status codes
full_data = response.json()
except httpx.TimeoutException:
logging.exception("Questionnaire API query timed out")
logger.error("Questionnaire API query timed out")
return None, Response(
{"error": "Questionnaire API query timed out"},
status=504,
)
except httpx.RequestError as e:
logging.exception(f"Error querying questionnaire API: {e}")
except httpx.RequestError:
logger.error("Error querying questionnaire API")
return None, Response(
{"error": "Failed to query questionnaire API"},
status=500,
)
except Exception as e:
logging.exception(f"An unexpected error occurred: {e}")
except Exception:
logger.error("An unexpected error occurred")
return None, Response({"error": "An unexpected error occurred"}, status=500)

# Filter and return only the required fields from the first row
Expand Down Expand Up @@ -180,7 +189,7 @@ async def get_latest_answer(
key in filtered_data and filtered_data[key] is not None
for key in ["id", "submitted_at", "account", "otp"]
):
logging.warning("Missing required field(s) in questionnaire response")
logger.warning("Missing required field(s) in questionnaire response")
return None, Response(
{"error": "Missing required field(s) in questionnaire response"},
status=400,
Expand Down Expand Up @@ -211,7 +220,8 @@ def rate_password_strength(password: str) -> int:
if re.search(r"[^a-zA-Z0-9\s]", password):
score += 1

length_step = (PASSWORD_LENGTH_MAX - PASSWORD_LENGTH_MIN) // 10
length_range = max(1, PASSWORD_LENGTH_MAX - PASSWORD_LENGTH_MIN)
length_step = max(1, length_range // 10)

score += (len(password) - PASSWORD_LENGTH_MIN) // length_step

Expand Down
Loading
Loading