From 3912feaed29a59750ebdb9cd0a9fbf145210bb8c Mon Sep 17 00:00:00 2001 From: spkap Date: Fri, 17 Jan 2025 21:47:25 +0530 Subject: [PATCH 1/7] Implemented mock routes --- .gitignore | 1 + backend/Dockerfile | 2 +- backend/atlas_backend/models.py | 43 +- backend/atlas_backend/serializers.py | 71 ++- backend/atlas_backend/urls.py | 18 +- backend/atlas_backend/views.py | 610 +++++++++++++-------- frontend/src/api/auth.js | 19 +- frontend/src/api/challenges.js | 39 +- frontend/src/api/config.js | 54 +- frontend/src/api/scoreboard.js | 22 +- frontend/src/api/teams.js | 62 ++- frontend/src/api/user.js | 72 +-- frontend/src/components/ChallengeCard.jsx | 104 ++-- frontend/src/components/Navbar.jsx | 5 +- frontend/src/components/ProtectedRoute.jsx | 12 +- frontend/src/components/TeamCard.jsx | 16 +- frontend/src/context/AuthContext.jsx | 102 +--- 17 files changed, 694 insertions(+), 558 deletions(-) diff --git a/.gitignore b/.gitignore index 8f7bd4f..7fdaa38 100644 --- a/.gitignore +++ b/.gitignore @@ -82,3 +82,4 @@ test-report.xml Thumbs.db ehthumbs.db Desktop.ini +codebase_review.txt diff --git a/backend/Dockerfile b/backend/Dockerfile index f353394..ddc227d 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,5 +1,5 @@ # Use Python base image -FROM python:3.12-slim +FROM python:3.12 # Set environment variables ENV PYTHONDONTWRITEBYTECODE=1 diff --git a/backend/atlas_backend/models.py b/backend/atlas_backend/models.py index 2aa0c5f..6a5c8bb 100644 --- a/backend/atlas_backend/models.py +++ b/backend/atlas_backend/models.py @@ -2,7 +2,6 @@ from django.contrib.auth.models import AbstractUser, BaseUserManager, Group from django.core.validators import MinValueValidator - class CustomUserManager(BaseUserManager): def create_user(self, email, password=None, **extra_fields): if not email: @@ -30,7 +29,6 @@ def get_by_natural_key(self, email): """ return self.get(email=email) - class Team(models.Model): name = models.CharField(max_length=100, unique=True) description = models.TextField(blank=True) @@ -42,21 +40,13 @@ class Team(models.Model): def __str__(self): return self.name - class User(AbstractUser): email = models.EmailField(unique=True) - team = models.ForeignKey( - Team, - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name="members", - ) + team = models.ForeignKey(Team, on_delete=models.SET_NULL, null=True, blank=True, related_name="members") created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) objects = CustomUserManager() - USERNAME_FIELD = "email" REQUIRED_FIELDS = [] @@ -66,11 +56,10 @@ def __str__(self): def has_role(self, role_name): return self.groups.filter(name=role_name).exists() - class Challenge(models.Model): CATEGORY_CHOICES = [ ("web", "Web"), - ("crypto", "Cryptography"), + ("crypto", "Cryptography"), ("pwn", "Binary Exploitation"), ("reverse", "Reverse Engineering"), ("forensics", "Forensics"), @@ -90,46 +79,40 @@ class Challenge(models.Model): def __str__(self): return self.title - class Container(models.Model): STATUS_CHOICES = [ ("running", "Running"), - ("exited", "Exit"), + ("exited", "Exited"), ("error", "Error"), ] - team = models.ForeignKey(Team, on_delete=models.CASCADE) - challenge = models.ForeignKey(Challenge, on_delete=models.CASCADE) + team = models.ForeignKey(Team, on_delete=models.CASCADE, related_name="containers") + challenge = models.ForeignKey(Challenge, on_delete=models.CASCADE, related_name="containers") container_id = models.CharField(max_length=100) ssh_host = models.CharField(max_length=200) ssh_port = models.IntegerField() ssh_user = models.CharField(max_length=100) ssh_key = models.TextField() - status = models.CharField( - max_length=20, choices=STATUS_CHOICES, default="exited" - ) + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="exited") created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) def __str__(self): return f"{self.team.name} - {self.challenge.title}" - class Submission(models.Model): - team = models.ForeignKey(Team, on_delete=models.CASCADE) - challenge = models.ForeignKey(Challenge, on_delete=models.CASCADE) - points_awarded = models.IntegerField(validators=[MinValueValidator(0)]) - timestamp = models.DateTimeField(auto_now_add=True) + team = models.ForeignKey(Team, on_delete=models.CASCADE, related_name="submissions") + challenge = models.ForeignKey(Challenge, on_delete=models.CASCADE, related_name="submissions") + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='submissions') flag_submitted = models.CharField(max_length=200, default="") is_correct = models.BooleanField(default=False) + points_awarded = models.IntegerField(validators=[MinValueValidator(0)]) attempt_number = models.IntegerField(default=1) + timestamp = models.DateTimeField(auto_now_add=True) class Meta: - unique_together = ( - "team", - "challenge", - ) # Used to enforce one submission per challenge for any team + unique_together = ("team", "challenge") ordering = ["-timestamp"] def __str__(self): - return f"{self.team.name} - {self.challenge.title}" + return f"{self.team.name} - {self.challenge.title}" \ No newline at end of file diff --git a/backend/atlas_backend/serializers.py b/backend/atlas_backend/serializers.py index ecdec82..f2ca119 100644 --- a/backend/atlas_backend/serializers.py +++ b/backend/atlas_backend/serializers.py @@ -1,38 +1,67 @@ from rest_framework import serializers -from django.contrib.auth.models import Group -from .models import User, Challenge - +from .models import User, Challenge, Team, Submission, Container class UserSerializer(serializers.ModelSerializer): class Meta: model = User - fields = ['id', 'username', 'email', 'role', 'team', 'created_at', 'updated_at'] - + fields = ['id', 'username', 'email', 'team'] class SignupSerializer(serializers.ModelSerializer): class Meta: model = User fields = ['username', 'email', 'password'] + extra_kwargs = {'password': {'write_only': True}} + + def create(self, validated_data): + return User.objects.create_user(**validated_data) + +class ChallengeSerializer(serializers.ModelSerializer): + is_solved = serializers.SerializerMethodField() + attempts = serializers.SerializerMethodField() + + class Meta: + model = Challenge + fields = [ + 'id', + 'title', + 'description', + 'category', + 'max_points', + 'max_team_size', + 'docker_image', + 'is_solved', + 'attempts', + 'created_at', + 'updated_at' + ] extra_kwargs = { - 'password': {'write_only': True}, + 'flag': {'write_only': True} # Never expose flag in API responses } - def create(self, validated_data): - user = User( - username=validated_data['username'], - email=validated_data['email'] - ) - user.set_password(validated_data['password']) - user.save() + def get_is_solved(self, obj): + team = self.context.get('user_team') + return team and obj.submissions.filter(team=team, is_correct=True).exists() - - default_group, _ = Group.objects.get_or_create(name='user') - user.role = default_group - user.save() + def get_attempts(self, obj): + team = self.context.get('user_team') + return team and obj.submissions.filter(team=team).count() or 0 - return user +class TeamSerializer(serializers.ModelSerializer): + members = UserSerializer(many=True, read_only=True) + total_score = serializers.IntegerField(read_only=True) + member_count = serializers.IntegerField(read_only=True) + solved_count = serializers.IntegerField(read_only=True) -class ChallengeSerializer(serializers.ModelSerializer): class Meta: - model = Challenge - fields = ['id', 'title', 'description', 'category', 'max_points', 'max_team_size', 'created_at', 'updated_at'] \ No newline at end of file + model = Team + fields = ['id', 'name', 'description', 'team_size', 'members', + 'total_score', 'member_count', 'solved_count', 'challenges'] + +class SubmissionSerializer(serializers.ModelSerializer): + challenge_name = serializers.CharField(source='challenge.title') + submitted_by = serializers.CharField(source='user.username') + + class Meta: + model = Submission + fields = ['id', 'challenge_name', 'submitted_by', 'flag_submitted', + 'is_correct', 'points_awarded', 'attempt_number', 'timestamp'] diff --git a/backend/atlas_backend/urls.py b/backend/atlas_backend/urls.py index 453bd28..a651433 100644 --- a/backend/atlas_backend/urls.py +++ b/backend/atlas_backend/urls.py @@ -1,18 +1,30 @@ +# backend/atlas_backend/urls.py from django.urls import path from . import views urlpatterns = [ + # Auth routes path('auth/register', views.signup, name='signup'), path('auth/login', views.signin, name='signin'), path('auth/forgot-password', views.request_password_reset, name='request_password_reset'), path('auth/reset-password', views.reset_password, name='reset_password'), + + # Challenge routes path('challenges', views.get_challenges, name='get_challenges'), path('challenges//submit', views.submit_flag, name='submit_flag'), path('challenges//start', views.start_challenge, name='start_challenge'), - path('challenges/create', views.create_challenge, name='create_challenge'), + + # Team routes path('teams', views.get_teams, name='get_teams'), - path('teams/join', views.join_team, name='join_team'), path('teams/create', views.create_team, name='create_team'), + path('teams/join', views.join_team, name='join_team'), path('teams/leave', views.leave_team, name='leave_team'), + + # Scoreboard route path('scoreboard', views.get_scoreboard, name='get_scoreboard'), -] \ No newline at end of file + + # User routes + path('user/update-info', views.update_user_info, name='update_user_info'), + path('user/profile', views.get_user_profile, name='get_user_profile'), + path('user/team/history', views.get_team_history, name='get_team_history'), +] diff --git a/backend/atlas_backend/views.py b/backend/atlas_backend/views.py index 0d2ef9b..dfa979b 100644 --- a/backend/atlas_backend/views.py +++ b/backend/atlas_backend/views.py @@ -2,6 +2,11 @@ from django.contrib.auth import authenticate from django.core.exceptions import ValidationError from django.conf import settings +from django.db.models import Sum, Count, Max, Q +from django.db import IntegrityError +from datetime import datetime, timedelta +import jwt +from django.core.mail import send_mail from rest_framework import status from rest_framework.decorators import api_view, permission_classes @@ -9,59 +14,8 @@ from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework_simplejwt.tokens import RefreshToken -from datetime import datetime, timedelta -import jwt - -from .models import User, Challenge,Submission -from .serializers import SignupSerializer, ChallengeSerializer - - -dummy_teams = [ - { - "id": 1, - "name": "Team Alpha", - "email": "alpha@example.com", - "isHidden": False, - "isBanned": False, - "members": ["Alice", "Bob"], - "score": 1000, - }, - { - "id": 2, - "name": "Team Beta", - "email": "beta@example.com", - "isHidden": False, - "isBanned": False, - "members": ["Charlie", "Dave"], - "score": 800, - }, -] - -dummy_challenges = [ - { - "id": 1, - "name": "Challenge 1", - "description": "Solve this challenge", - "points": 100, - }, - { - "id": 2, - "name": "Challenge 2", - "description": "Solve this challenge too", - "points": 200, - }, -] - -dummy_scoreboard = [ - { - "team": "Team Alpha", - "score": 1000, - }, - { - "team": "Team Beta", - "score": 800, - }, -] +from .models import User, Challenge, Submission, Team, Container +from .serializers import SignupSerializer, ChallengeSerializer, TeamSerializer, SubmissionSerializer, UserSerializer @api_view(['POST']) @permission_classes([AllowAny]) @@ -71,13 +25,11 @@ def signup(request): user = serializer.save() refresh = RefreshToken.for_user(user) - # Add custom claims to the token refresh['user'] = { 'id': user.id, 'email': user.email, 'username': user.username, 'isAdmin': user.is_staff, - 'isVerified': user.is_active, 'teamId': user.team.id if user.team else None } @@ -87,27 +39,24 @@ def signup(request): }, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - @api_view(['POST']) @permission_classes([AllowAny]) def signin(request): - email = request.data.get('email') # Changed from username + email = request.data.get('email') password = request.data.get('password') - - if not email or not password: # Updated error message + + if not email or not password: return Response({'error': 'Email and password are required'}, status=status.HTTP_400_BAD_REQUEST) - - user = authenticate(email=email, password=password) # Changed to use email + + user = authenticate(email=email, password=password) if user: refresh = RefreshToken.for_user(user) - # Add custom claims to the token refresh['user'] = { 'id': user.id, 'email': user.email, 'username': user.username, 'isAdmin': user.is_staff, - 'isVerified': user.is_active, 'teamId': user.team.id if user.team else None } @@ -117,230 +66,407 @@ def signin(request): }) return Response({'error': 'Invalid credentials'}, status=status.HTTP_401_UNAUTHORIZED) +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def get_challenges(request): + challenges = Challenge.objects.all() + serializer = ChallengeSerializer( + challenges, + many=True, + context={'user_team': request.user.team} + ) + return Response(serializer.data) @api_view(['POST']) -@permission_classes([AllowAny]) -def request_password_reset(request): - email = request.data.get('email') +@permission_classes([IsAuthenticated]) +def submit_flag(request, challenge_id): + if not request.user.team: + return Response( + {'error': 'You must be in a team to submit flags'}, + status=status.HTTP_400_BAD_REQUEST + ) + + challenge = get_object_or_404(Challenge, id=challenge_id) + flag = request.data.get('flag') + + if not flag: + return Response( + {'error': 'Flag is required'}, + status=status.HTTP_400_BAD_REQUEST + ) + + existing_submission = Submission.objects.filter( + team=request.user.team, + challenge=challenge + ).first() + + if existing_submission: + if existing_submission.is_correct: + return Response( + {'error': 'Challenge already solved'}, + status=status.HTTP_400_BAD_REQUEST + ) + attempt_number = existing_submission.attempt_number + 1 + existing_submission.delete() # Remove previous attempt + else: + attempt_number = 1 + + is_correct = challenge.flag == flag + points = challenge.max_points if is_correct else 0 + try: - user = User.objects.get(email=email) - token = jwt.encode({ - 'user_id': user.id, - 'exp': datetime.utcnow() + timedelta(hours=24) - }, settings.SECRET_KEY, algorithm='HS256') + submission = Submission.objects.create( + team=request.user.team, + challenge=challenge, + user=request.user, + flag_submitted=flag, + is_correct=is_correct, + points_awarded=points, + attempt_number=attempt_number + ) - reset_url = f"{settings.FRONTEND_URL}/auth/reset-password?token={token}" + return Response({ + 'message': 'Correct flag!' if is_correct else 'Incorrect flag', + 'points_awarded': points, + 'is_correct': is_correct, + 'attempt_number': attempt_number + }) + except IntegrityError: + return Response( + {'error': 'Submission error occurred'}, + status=status.HTTP_400_BAD_REQUEST + ) + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +def start_challenge(request, challenge_id): + if not request.user.team: + return Response( + {'error': 'You must be in a team to start challenges'}, + status=status.HTTP_400_BAD_REQUEST + ) - # to uncomment after smtp configuration - # send_mail( - # 'Password Reset Request', - # f'Click the following link to reset your password: {reset_url}', - # settings.DEFAULT_FROM_EMAIL, - # [email], - # fail_silently=False, - # ) + challenge = get_object_or_404(Challenge, id=challenge_id) + + existing_container = Container.objects.filter( + team=request.user.team, + challenge=challenge, + status='running' + ).first() + + if existing_container: + return Response({ + 'host': existing_container.ssh_host, + 'port': existing_container.ssh_port, + 'username': existing_container.ssh_user, + 'container_id': existing_container.id, + 'ssh_key': existing_container.ssh_key + }) + + try: + from .docker_plugin import DockerManager + docker_mgr = DockerManager() - return Response({'message': 'Password reset token generated', 'reset_url': reset_url}) - except User.DoesNotExist: - return Response({'error': 'User not found'}, status=status.HTTP_404_NOT_FOUND) + container_info = docker_mgr.create_container( + challenge.docker_image, + team_id=request.user.team.id, + challenge_id=challenge_id + ) + + container = Container.objects.create( + team=request.user.team, + challenge=challenge, + container_id=container_info['container_id'], + ssh_host=container_info['host'], + ssh_port=container_info['port'], + ssh_user=container_info['username'], + ssh_key=container_info.get('ssh_key', ''), + status='running' + ) + + return Response({ + 'host': container.ssh_host, + 'port': container.ssh_port, + 'username': container.ssh_user, + 'container_id': container.id, + 'ssh_key': container.ssh_key + }) + + except Exception as e: + return Response( + {'error': str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def get_teams(request): + teams = Team.objects.annotate( + total_score=Sum('submissions__points_awarded', default=0), + member_count=Count('members', distinct=True), + solved_count=Count('submissions', filter=Q(submissions__is_correct=True)) + ).order_by('-total_score') + + serializer = TeamSerializer(teams, many=True) + return Response(serializer.data) @api_view(['POST']) -@permission_classes([AllowAny]) -def reset_password(request): - token = request.query_params.get('token') # Get token from URL query parameter - new_password = request.data.get('new_password') - confirm_password = request.data.get('confirm_password') - - if not token: +@permission_classes([IsAuthenticated]) +def create_team(request): + if request.user.team: return Response( - {'error': 'Reset token is required'}, + {'error': 'You are already in a team'}, status=status.HTTP_400_BAD_REQUEST ) + + name = request.data.get('name') + description = request.data.get('description', '') + team_size = request.data.get('team_size', 4) - if not new_password or not confirm_password: + if not name: return Response( - {'error': 'New password and password confirmation are required'}, + {'error': 'Team name is required'}, status=status.HTTP_400_BAD_REQUEST ) - if new_password != confirm_password: + try: + team = Team.objects.create( + name=name, + description=description, + team_size=team_size + ) + request.user.team = team + request.user.save() + + return Response(TeamSerializer(team).data, status=status.HTTP_201_CREATED) + except Exception as e: return Response( - {'error': 'Passwords do not match'}, + {'error': str(e)}, status=status.HTTP_400_BAD_REQUEST ) - - if len(new_password) < 8: + +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +def join_team(request): + if request.user.team: return Response( - {'error': 'Password must be at least 8 characters long'}, + {'error': 'You are already in a team'}, + status=status.HTTP_400_BAD_REQUEST + ) + + team_id = request.data.get('team_id') + if not team_id: + return Response( + {'error': 'Team ID is required'}, status=status.HTTP_400_BAD_REQUEST ) try: - payload = jwt.decode(token, settings.SECRET_KEY, algorithms=['HS256']) - user = User.objects.get(id=payload['user_id']) - user.set_password(new_password) - user.save() - return Response({'message': 'Password reset successful'}) - except jwt.ExpiredSignatureError: - return Response({'error': 'Reset token has expired'}, status=status.HTTP_400_BAD_REQUEST) - except jwt.DecodeError: - return Response({'error': 'Invalid reset token'}, status=status.HTTP_400_BAD_REQUEST) - except User.DoesNotExist: - return Response({'error': 'User not found'}, status=status.HTTP_404_NOT_FOUND) - except Exception: - return Response({'error': 'Password reset failed'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - -# @api_view(['GET']) -# @permission_classes([IsAuthenticated]) -# def get_challenges(request): -# challenges = Challenge.objects.all() -# serializer = ChallengeSerializer(challenges, many=True) -# return Response(serializer.data, status=status.HTTP_200_OK) - -# @api_view(['POST']) -# @permission_classes([IsAuthenticated]) -# def submit_flag(request, challenge_id): -# flag = request.data.get('flag') -# challenge = get_object_or_404(Challenge, id=challenge_id) -# team = request.user.team - -# if challenge.flag == flag: + team = Team.objects.get(id=team_id) + current_size = team.members.count() -# Submission.objects.create( -# team=team, -# challenge=challenge, -# points_awarded=challenge.max_points -# ) -# return Response({'message': 'Correct flag!', 'points_awarded': challenge.max_points}, status=status.HTTP_200_OK) -# return Response({'message': 'Incorrect flag'}, status=status.HTTP_400_BAD_REQUEST) + if current_size >= team.team_size: + return Response( + {'error': 'Team is full'}, + status=status.HTTP_400_BAD_REQUEST + ) + + request.user.team = team + request.user.save() + return Response({'message': 'Successfully joined team'}) + except Team.DoesNotExist: + return Response( + {'error': 'Team not found'}, + status=status.HTTP_404_NOT_FOUND + ) +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +def leave_team(request): + if not request.user.team: + return Response( + {'error': 'You are not in a team'}, + status=status.HTTP_400_BAD_REQUEST + ) + + request.user.team = None + request.user.save() + return Response({'message': 'Successfully left team'}) -# @api_view(['POST']) -# @permission_classes([IsAuthenticated]) -# def create_challenge(request): - -# data = request.data +@api_view(['GET']) +@permission_classes([AllowAny]) +def get_scoreboard(request): + teams = Team.objects.annotate( + total_score=Sum('submissions__points_awarded', default=0), + member_count=Count('members', distinct=True), + solved_count=Count('submissions', filter=Q(submissions__is_correct=True)) + ).order_by('-total_score') + serializer = TeamSerializer(teams, many=True) + return Response(serializer.data) + +@api_view(['POST']) +@permission_classes([AllowAny]) +def request_password_reset(request): + email = request.data.get('email') + if not email: + return Response( + {'error': 'Email is required'}, + status=status.HTTP_400_BAD_REQUEST + ) -# try: + try: + user = User.objects.get(email=email) + token = jwt.encode({ + 'user_id': user.id, + 'exp': datetime.utcnow() + timedelta(hours=24) + }, settings.SECRET_KEY, algorithm='HS256') -# title = data.get("title") -# description = data.get("description") -# category = data.get("category") -# docker_image = data.get("docker_image") -# flag = data.get("flag") -# max_points = data.get("max_points") -# max_team_size = data.get("max_team_size", 4) - -# if not title or not description or not category or not docker_image or not flag or max_points is None: -# raise ValidationError("All fields are required") - -# valid_categories = dict(Challenge.CATEGORY_CHOICES) -# if category not in valid_categories: -# raise ValidationError(f"Invalid category. Valid categories are: {', '.join(valid_categories.values())}") - -# challenge = Challenge.objects.create( -# title=title, -# description=description, -# category=category, -# docker_image=docker_image, -# flag=flag, -# max_points=max_points, -# max_team_size=max_team_size -# ) - -# return Response({ -# "message": "Challenge created successfully!", -# "challenge_id": challenge.id -# }, status=status.HTTP_201_CREATED) + reset_url = f"{settings.FRONTEND_URL}/reset-password?token={token}" + + # Send email if SMTP is configured + if hasattr(settings, 'EMAIL_HOST'): + send_mail( + 'Password Reset Request', + f'Click here to reset your password: {reset_url}', + settings.DEFAULT_FROM_EMAIL, + [email], + fail_silently=False, + ) + return Response({'message': 'Password reset email sent'}) + + # For development, return the reset URL + return Response({ + 'message': 'Password reset token generated', + 'reset_url': reset_url + }) + + except User.DoesNotExist: + # Return success to prevent email enumeration + return Response({'message': 'If email exists, reset instructions will be sent'}) -# except ValidationError: -# return Response({"error": "Validation error"}, status=status.HTTP_400_BAD_REQUEST) +@api_view(['POST']) +@permission_classes([AllowAny]) +def reset_password(request): + token = request.data.get('token') + new_password = request.data.get('new_password') + + if not token or not new_password: + return Response( + {'error': 'Token and new password are required'}, + status=status.HTTP_400_BAD_REQUEST + ) -# except Exception: -# return Response({"error": "Something went wrong. Please try again."}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + try: + # Validate password + if len(new_password) < 8: + return Response( + {'error': 'Password must be at least 8 characters'}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Verify and decode token + payload = jwt.decode( + token, + settings.SECRET_KEY, + algorithms=['HS256'], + options={'verify_exp': True} + ) + + user = User.objects.get(id=payload['user_id']) + user.set_password(new_password) + user.save() + return Response({'message': 'Password reset successful'}) -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -def get_teams(request): - return Response(dummy_teams, status=status.HTTP_200_OK) + except jwt.ExpiredSignatureError: + return Response( + {'error': 'Reset link has expired'}, + status=status.HTTP_400_BAD_REQUEST + ) + except (jwt.InvalidTokenError, User.DoesNotExist): + return Response( + {'error': 'Invalid reset token'}, + status=status.HTTP_400_BAD_REQUEST + ) @api_view(['POST']) @permission_classes([IsAuthenticated]) -def join_team(request): - return Response({"message": "Joined team successfully"}, status=status.HTTP_200_OK) - -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -def get_scoreboard(request): - return Response(dummy_scoreboard, status=status.HTTP_200_OK) +def create_challenge(request): + if not request.user.is_staff: + return Response( + {'error': 'Only administrators can create challenges'}, + status=status.HTTP_403_FORBIDDEN + ) + + try: + data = request.data + required_fields = ['title', 'description', 'category', 'docker_image', 'flag', 'max_points'] + + # Validate required fields + missing_fields = [field for field in required_fields if not data.get(field)] + if missing_fields: + return Response( + {'error': f'Missing required fields: {", ".join(missing_fields)}'}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Validate category + valid_categories = dict(Challenge.CATEGORY_CHOICES) + if data['category'] not in valid_categories: + return Response( + {'error': f'Invalid category. Valid categories are: {", ".join(valid_categories.keys())}'}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Create challenge + challenge = Challenge.objects.create( + title=data['title'], + description=data['description'], + category=data['category'], + docker_image=data['docker_image'], + flag=data['flag'], + max_points=int(data['max_points']), + max_team_size=int(data.get('max_team_size', 4)) + ) -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -def get_challenges(request): - return Response(dummy_challenges, status=status.HTTP_200_OK) + return Response( + ChallengeSerializer(challenge).data, + status=status.HTTP_201_CREATED + ) -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -def submit_flag(request, challenge_id): - return Response({"message": "Flag submitted successfully"}, status=status.HTTP_200_OK) + except ValueError as e: + return Response( + {'error': 'Invalid numeric value provided'}, + status=status.HTTP_400_BAD_REQUEST + ) + except Exception as e: + return Response( + {'error': str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) -@api_view(['POST']) +@api_view(['GET']) @permission_classes([IsAuthenticated]) -def create_challenge(request): - return Response({ - "message": "Challenge created successfully!", - "challenge_id": 1 - }, status=status.HTTP_201_CREATED) +def get_user_profile(request): + serializer = UserSerializer(request.user) + return Response(serializer.data) @api_view(['POST']) @permission_classes([IsAuthenticated]) -def start_challenge(request, challenge_id): - # Mocked SSH details - ssh_details = { - "host": "127.0.0.1", - "port": 2222, - "username": "user", - "password": "password" - } - return Response(ssh_details, status=status.HTTP_200_OK) - +def update_user_info(request): + serializer = UserSerializer(request.user, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) -@api_view(['POST']) +@api_view(['GET']) @permission_classes([IsAuthenticated]) -def create_team(request): - team_name = request.data.get('name') - if not team_name: - return Response({"error": "Team name is required"}, status=status.HTTP_400_BAD_REQUEST) +def get_team_history(request): + if not request.user.team: + return Response({'error': 'User not in a team'}, status=status.HTTP_400_BAD_REQUEST) - # Mocked response for team creation - new_team = { - "id": 7, # Example ID - "name": team_name, - "email": f"{team_name.lower().replace(' ', '_')}@ctf.com", - "isHidden": False, - "isBanned": False, - "points": 0, - "place": 0, - "memberCount": 1, - "users": [ - { - "id": request.user.id, - "username": request.user.username, - "email": request.user.email, - "points": 0, - "teamId": 7 - } - ], - "solvedChallenges": [] - } - return Response(new_team, status=status.HTTP_201_CREATED) - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -def leave_team(request): - # Mocked response for leaving a team - return Response({"message": "Left team successfully"}, status=status.HTTP_200_OK) \ No newline at end of file + submissions = Submission.objects.filter(team=request.user.team) + serializer = SubmissionSerializer(submissions, many=True) + return Response(serializer.data) \ No newline at end of file diff --git a/frontend/src/api/auth.js b/frontend/src/api/auth.js index 424402c..5227d8f 100644 --- a/frontend/src/api/auth.js +++ b/frontend/src/api/auth.js @@ -1,26 +1,17 @@ import apiClient from './config'; -export const register = async (username, email, password) => { - const response = await apiClient.post('/auth/register', { - username, - email, - password, - }); +export const register = async (userData) => { + const response = await apiClient.post('/auth/register', userData); return response.data; }; -export const login = async (email, password) => { - const response = await apiClient.post('/auth/login', { - email, - password, - }); +export const login = async (credentials) => { + const response = await apiClient.post('/auth/login', credentials); return response.data; }; export const requestPasswordReset = async (email) => { - const response = await apiClient.post('/auth/forgot-password', { - email, - }); + const response = await apiClient.post('/auth/forgot-password', { email }); return response.data; }; diff --git a/frontend/src/api/challenges.js b/frontend/src/api/challenges.js index ff391bc..cb08a22 100644 --- a/frontend/src/api/challenges.js +++ b/frontend/src/api/challenges.js @@ -1,33 +1,22 @@ -import axios from 'axios'; -import { API_URL } from './config'; import apiClient from './config'; -export const getChallenges = async (token) => { - const response = await axios.get(`${API_URL}/challenges`, { - headers: { Authorization: `Bearer ${token}` }, - }); +export const getChallenges = async () => { + const response = await apiClient.get('/challenges'); return response.data; }; -export const submitFlag = async (challengeId, flag, token) => { - const response = await axios.post( - `${API_URL}/challenges/${challengeId}/submit`, - { flag }, - { - headers: { Authorization: `Bearer ${token}` }, - }, - ); +export const submitFlag = async (challengeId, flag) => { + const response = await apiClient.post(`/challenges/${challengeId}/submit`, { + flag_submitted: flag, + }); return response.data; }; - -export const startChallenge = async (challengeId, token) => { - const response = await axios.post( - `${API_URL}/challenges/${challengeId}/start`, - {}, - { - headers: { Authorization: `Bearer ${token}` }, - }, - ); - return response.data; -} \ No newline at end of file +export const startChallenge = async (challengeId) => { + const response = await apiClient.post(`/challenges/${challengeId}/start`); + return { + container: response.data.container, + port: response.data.port, + status: response.data.status, + }; +}; diff --git a/frontend/src/api/config.js b/frontend/src/api/config.js index 69a8eeb..924569f 100644 --- a/frontend/src/api/config.js +++ b/frontend/src/api/config.js @@ -1,36 +1,56 @@ import axios from 'axios'; -export const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000'; -export const apiClient = axios.create({ +export const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'; + +const apiClient = axios.create({ baseURL: API_URL, timeout: 10000, headers: { 'Content-Type': 'application/json', - Accept: 'application/json', }, }); +// Token refresh logic apiClient.interceptors.response.use( (response) => response, - (error) => { - if (error.response?.status === 401) { - localStorage.removeItem('token'); - window.location.href = '/login'; + async (error) => { + const originalRequest = error.config; + if (error.response?.status === 401 && !originalRequest._retry) { + originalRequest._retry = true; + try { + const refreshToken = localStorage.getItem('refreshToken'); + const { data } = await apiClient.post('/auth/refresh', { + refresh: refreshToken, + }); + localStorage.setItem('token', data.access); + apiClient.defaults.headers.Authorization = `Bearer ${data.access}`; + return apiClient(originalRequest); + } catch (refreshError) { + localStorage.removeItem('token'); + localStorage.removeItem('refreshToken'); + window.location.href = '/login'; + } } return Promise.reject(error); }, ); -apiClient.interceptors.request.use( - (config) => { - const token = localStorage.getItem('token'); - // console.log('Token:', token); - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } - return config; - }, - (error) => Promise.reject(error) +// Add centralized error handler +apiClient.interceptors.response.use( + response => response, + error => { + console.error(`API Error: ${error.message}`); + return Promise.reject(error); + } ); +// Auth header injection +apiClient.interceptors.request.use((config) => { + const token = localStorage.getItem('token'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}); + export default apiClient; diff --git a/frontend/src/api/scoreboard.js b/frontend/src/api/scoreboard.js index 8f49263..819f12e 100644 --- a/frontend/src/api/scoreboard.js +++ b/frontend/src/api/scoreboard.js @@ -1,18 +1,18 @@ -import { API_URL } from './config'; -import axios from 'axios'; +import apiClient from './config'; -export const getScoreboard = async (token) => { - const response = await axios.get(`${API_URL}/scoreboard`, { - headers: { Authorization: `Bearer ${token}` }, - }); - return response.data; +export const getScoreboard = async () => { + try { + const response = await apiClient.get('/scoreboard'); + return response.data; + } catch (error) { + console.error('Error fetching scoreboard:', error); + throw error; + } }; -export const getTeams = async (token) => { +export const getTeams = async () => { try { - const response = await axios.get(`${API_URL}/teams`, { - headers: { Authorization: `Bearer ${token}` }, - }); + const response = await apiClient.get('/teams'); return response.data; } catch (error) { console.error('Error fetching teams:', error); diff --git a/frontend/src/api/teams.js b/frontend/src/api/teams.js index a0d6e30..6287cfc 100644 --- a/frontend/src/api/teams.js +++ b/frontend/src/api/teams.js @@ -1,27 +1,47 @@ -import axios from 'axios'; -import { API_URL } from './config'; +import apiClient from './config'; -export const getTeams = async (token) => { - const response = await axios.get(`${API_URL}/teams`, { - headers: { Authorization: `Bearer ${token}` }, - }); - return response.data; +export const getTeams = async () => { + try { + const response = await apiClient.get('/teams'); + return response.data; + } catch (error) { + console.error('Error fetching teams:', error); + throw error; + } }; -export const createTeam = async (teamData, token) => { - const response = await axios.post(`${API_URL}/teams`, teamData, { - headers: { Authorization: `Bearer ${token}` }, - }); - return response.data; +export const createTeam = async (teamData) => { + try { + const response = await apiClient.post('/teams/create', { + name: teamData.name, + description: teamData.description, + max_members: teamData.maxMembers, + }); + return response.data; + } catch (error) { + console.error('Error creating team:', error); + throw error; + } }; -export const joinTeam = async (code, token) => { - const response = await axios.post( - `${API_URL}/teams/join`, - { code }, - { - headers: { Authorization: `Bearer ${token}` }, - }, - ); - return response.data; +export const joinTeam = async (teamId) => { + try { + const response = await apiClient.post('/teams/join', { + team_id: teamId, + }); + return response.data; + } catch (error) { + console.error('Error joining team:', error); + throw error; + } +}; + +export const leaveTeam = async () => { + try { + const response = await apiClient.post('/teams/leave'); + return response.data; + } catch (error) { + console.error('Error leaving team:', error); + throw error; + } }; diff --git a/frontend/src/api/user.js b/frontend/src/api/user.js index bf122f5..a935ec2 100644 --- a/frontend/src/api/user.js +++ b/frontend/src/api/user.js @@ -1,52 +1,34 @@ import apiClient from './config'; -import axios from 'axios'; -import { API_URL } from './config'; - -// export const updateUserInfo = async (userInfo, token) => { -// const response = await axios.post(`${API_URL}/user/update-info`, userInfo, { -// headers: { Authorization: `Bearer ${token}` }, -// }); -// return response.data; -// }; - -// export const getTeamHistory = async (token) => { -// const response = await axios.get(`${API_URL}/user/team/history`, { -// headers: { Authorization: `Bearer ${token}` }, -// }); -// return response.data; -// }; +// Get user profile +export const getUserProfile = async () => { + try { + const response = await apiClient.get('/user/profile'); + return response.data; + } catch (error) { + console.error('Error fetching user profile:', error); + throw error; + } +}; -export const updateUserInfo = async (userInfo, token) => { - // Mocked response - return { - ...userInfo, - id: 1, - isAdmin: false, - isVerified: true, - teamId: 1, - }; +// Update user information +export const updateUserInfo = async (userData) => { + try { + const response = await apiClient.post('/user/update-info', userData); + return response.data; + } catch (error) { + console.error('Error updating user info:', error); + throw error; + } }; -export const getTeamHistory = async (token) => { - // Mocked response - return { - teamName: 'Team Alpha', - teamScore: 1000, - submissions: [ - { id: 1, challengeName: 'Challenge 1', points: 100, solvedAt: '2024-03-15' }, - { id: 2, challengeName: 'Challenge 2', points: 200, solvedAt: '2024-03-16' }, - ], - }; +// Get team history for the user +export const getTeamHistory = async () => { + const response = await apiClient.get('/user/team/history'); + return response.data; }; -export const leaveTeam = async (token) => { - const response = await axios.post( - `${API_URL}/teams/leave`, - {}, - { - headers: { Authorization: `Bearer ${token}` }, - }, - ); - return response.data; - }; \ No newline at end of file +export const leaveTeam = async () => { + const response = await apiClient.post('/teams/leave'); + return response.data; +}; diff --git a/frontend/src/components/ChallengeCard.jsx b/frontend/src/components/ChallengeCard.jsx index eb9a175..acacf2f 100644 --- a/frontend/src/components/ChallengeCard.jsx +++ b/frontend/src/components/ChallengeCard.jsx @@ -1,71 +1,108 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; -import { useAuth } from '../hooks/useAuth'; import { startChallenge, submitFlag } from '../api/challenges'; function ChallengeCard({ challenge }) { - const { user } = useAuth(); - const [sshDetails, setSshDetails] = useState(null); + const [containerDetails, setContainerDetails] = useState(null); const [flag, setFlag] = useState(''); - const [challengeStarted, setChallengeStarted] = useState(false); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); const handleStartChallenge = async () => { try { - const details = await startChallenge(challenge.id, user.token); - setSshDetails(details); - setChallengeStarted(true); - alert('Challenge started! Check SSH details below.'); + setLoading(true); + setError(''); + const data = await startChallenge(challenge.id); + setContainerDetails(data); } catch (error) { + setError('Failed to start challenge. Please try again.'); console.error('Failed to start challenge:', error); - alert('Failed to start the challenge. Please try again.'); + } finally { + setLoading(false); } }; const handleSubmitFlag = async () => { + if (!flag.trim()) { + setError('Please enter a flag'); + return; + } + try { - const response = await submitFlag(challenge.id, flag, user.token); - alert(response.message); + setLoading(true); + setError(''); + const data = await submitFlag(challenge.id, flag); + if (data.correct) { + alert('Congratulations! Flag is correct!'); + setFlag(''); + } else { + setError('Incorrect flag. Try again.'); + } } catch (error) { + setError('Failed to submit flag. Please try again.'); console.error('Failed to submit flag:', error); - alert('Failed to submit the flag. Please try again.'); + } finally { + setLoading(false); } }; return (
-

{challenge.name}

+

{challenge.title}

{challenge.description}

-

- Points: - {challenge.points} -

- {!challengeStarted ? ( +
+ + Category: {challenge.category} + + + {challenge.max_points} pts + +
+ + {error &&
{error}
} + + {!containerDetails ? ( ) : ( -
-

SSH Details:

-

Host: {sshDetails.host}

-

Port: {sshDetails.port}

-

Username: {sshDetails.username}

-

Password: {sshDetails.password}

-
+
+
+

Connection Details:

+
+

Container ID: {containerDetails.container}

+

Port: {containerDetails.port}

+

Status: {containerDetails.status}

+
+
+ +
setFlag(e.target.value)} placeholder="Enter flag" - className="input" + className="w-full p-2 border rounded focus:ring-2 focus:ring-blue-500" + disabled={loading} />
@@ -77,10 +114,11 @@ function ChallengeCard({ challenge }) { ChallengeCard.propTypes = { challenge: PropTypes.shape({ id: PropTypes.number.isRequired, - name: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, description: PropTypes.string.isRequired, - points: PropTypes.number.isRequired, + category: PropTypes.string.isRequired, + max_points: PropTypes.number.isRequired, }).isRequired, }; -export default ChallengeCard; \ No newline at end of file +export default ChallengeCard; diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index 03cce5a..956fa4a 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -8,7 +8,6 @@ function Navbar() { const handleLogout = () => { logout(); - navigate('/'); }; return ( @@ -45,7 +44,7 @@ function Navbar() { Admin )} - @@ -69,4 +68,4 @@ function Navbar() { ); } -export default Navbar; \ No newline at end of file +export default Navbar; diff --git a/frontend/src/components/ProtectedRoute.jsx b/frontend/src/components/ProtectedRoute.jsx index 0670d66..b3715a6 100644 --- a/frontend/src/components/ProtectedRoute.jsx +++ b/frontend/src/components/ProtectedRoute.jsx @@ -1,24 +1,24 @@ import React from 'react'; -import { Navigate } from 'react-router-dom'; +import { Navigate, useLocation } from 'react-router-dom'; import { useAuth } from '../hooks/useAuth'; function ProtectedRoute({ children, requireAdmin }) { const { isAuthenticated, user } = useAuth(); - const isAdmin = user?.isAdmin; + const location = useLocation(); if (!isAuthenticated) { - return ; + return ; } - if (requireAdmin && !isAdmin) { + if (requireAdmin && !user?.isAdmin) { return ; } - if (!requireAdmin && isAdmin) { + if (!requireAdmin && user?.isAdmin) { return ; } return children; } -export default ProtectedRoute; \ No newline at end of file +export default ProtectedRoute; diff --git a/frontend/src/components/TeamCard.jsx b/frontend/src/components/TeamCard.jsx index b5821af..0cd6858 100644 --- a/frontend/src/components/TeamCard.jsx +++ b/frontend/src/components/TeamCard.jsx @@ -1,19 +1,14 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { useAuth } from '../hooks/useAuth'; import { joinTeam } from '../api/teams'; -function TeamCard({ team }) { - const { user } = useAuth(); - +function TeamCard({ team, onJoinSuccess }) { const handleJoinTeam = async () => { try { - await joinTeam(team.id, user.token); - alert('Successfully joined the team!'); - // Optionally, you can refresh the team list or update the UI accordingly + await joinTeam(team.id); + onJoinSuccess?.(); } catch (error) { console.error('Failed to join team:', error); - alert('Failed to join the team. Please try again.'); } }; @@ -37,11 +32,12 @@ function TeamCard({ team }) { TeamCard.propTypes = { team: PropTypes.shape({ + id: PropTypes.number.isRequired, name: PropTypes.string.isRequired, members: PropTypes.arrayOf(PropTypes.string).isRequired, score: PropTypes.number.isRequired, - id: PropTypes.number.isRequired, }).isRequired, + onJoinSuccess: PropTypes.func, }; -export default TeamCard; \ No newline at end of file +export default TeamCard; diff --git a/frontend/src/context/AuthContext.jsx b/frontend/src/context/AuthContext.jsx index b3ccbe6..e2f9da1 100644 --- a/frontend/src/context/AuthContext.jsx +++ b/frontend/src/context/AuthContext.jsx @@ -1,105 +1,55 @@ import React, { createContext, useState, useEffect } from 'react'; import { jwtDecode } from 'jwt-decode'; import { register, login } from '../api/auth'; // Ensure these imports are correct +import { useNavigate } from 'react-router-dom'; +import apiClient from '../api/apiClient'; export const AuthContext = createContext(); export const AuthProvider = ({ children }) => { const [user, setUser] = useState(null); - const [token, setToken] = useState({ - access: null, - refresh: null, - }); useEffect(() => { - const storedToken = JSON.parse(localStorage.getItem('token')); - if (storedToken && isTokenValid(storedToken.access)) { - setToken(storedToken); - setUser(parseToken(storedToken.access)); - } else { - localStorage.removeItem('token'); - setUser(null); + const token = localStorage.getItem('token'); + if (token) { + try { + const decoded = jwtDecode(token); + if (decoded.exp > Date.now() / 1000) { + setUser(decoded.user); + apiClient.defaults.headers.Authorization = `Bearer ${token}`; + } else { + handleLogout(); + } + } catch (error) { + handleLogout(); + } } }, []); - const parseToken = (token) => { + const handleLogin = async (credentials) => { try { - const decoded = jwtDecode(token); - return { - token, - id: decoded.user.id, - email: decoded.user.email, - username: decoded.user.username, - isAdmin: decoded.user.isAdmin, - isVerified: decoded.user.isVerified, - teamId: decoded.user.teamId || null, - }; + const { data } = await apiClient.post('/auth/login', credentials); + localStorage.setItem('token', data.token); + apiClient.defaults.headers.Authorization = `Bearer ${data.token}`; + setUser(jwtDecode(data.token).user); } catch (error) { - console.error('Error parsing token:', error); - return null; - } - }; - - const login = (response) => { - const { access } = response; - const decodedAccess = jwtDecode(access); - const user = { - token: access, - id: decodedAccess.user.id, - email: decodedAccess.user.email, - username: decodedAccess.user.username, - isAdmin: decodedAccess.user.isAdmin, - isVerified: decodedAccess.user.isVerified, - teamId: decodedAccess.user.teamId || null, - }; - - // console.log('User:', user); - if (isTokenValid(access)) { - localStorage.setItem('token', JSON.stringify({ access })); - setToken({ access }); - setUser(user); - } else { - console.error('Invalid token on login'); - logout(); + throw error; } }; - const logout = () => { + const handleLogout = () => { localStorage.removeItem('token'); - setToken({ access: null }); + delete apiClient.defaults.headers.Authorization; setUser(null); }; - const isTokenValid = (token) => { - try { - const decoded = jwtDecode(token); - return decoded.exp > Date.now() / 1000; - } catch (error) { - console.error('Invalid token:', error); - return false; - } - }; - - const signup = async (data) => { - try { - const response = await register(data.username, data.email, data.password); - login(response); - } catch (error) { - console.error('Signup error:', error); - throw error; - } - }; - - const isAuthenticated = !!user; - return ( {children} From 51e2af9ca36b5821352dcfc51698f5d35b3444bd Mon Sep 17 00:00:00 2001 From: Vatsal Date: Sun, 19 Jan 2025 21:06:12 +0530 Subject: [PATCH 2/7] Backend : Team signup and admin challenges --- backend/atlas_backend/auth.py | 11 + backend/atlas_backend/models.py | 27 +- backend/atlas_backend/serializers.py | 33 +- backend/atlas_backend/urls.py | 25 +- backend/atlas_backend/views.py | 456 +++++++++++++-- backend/backend/settings.py | 42 +- frontend/src/App.jsx | 84 +-- frontend/src/api/auth.js | 28 +- frontend/src/api/challenges.js | 96 +++- frontend/src/api/config.js | 58 +- frontend/src/api/teams.js | 10 + frontend/src/api/user.js | 15 +- frontend/src/components/AdminRoute.jsx | 19 + frontend/src/components/ChallengeCard.jsx | 135 +---- frontend/src/components/Navbar.jsx | 55 +- frontend/src/components/ProtectedRoute.jsx | 13 +- frontend/src/context/AuthContext.jsx | 90 ++- frontend/src/hooks/useAuth.js | 23 +- frontend/src/pages/Challenges.jsx | 92 ++- frontend/src/pages/Home.jsx | 1 + frontend/src/pages/Login.jsx | 63 ++- frontend/src/pages/Register.jsx | 200 +++++-- frontend/src/pages/Scoreboard.jsx | 6 +- frontend/src/pages/TeamProfile.jsx | 76 +++ frontend/src/pages/admin/AdminChallenges.jsx | 116 ++++ frontend/src/pages/admin/AdminLogin.jsx | 41 +- frontend/src/pages/admin/ChallengeDetail.jsx | 466 +++++++++------ frontend/src/pages/admin/Challenges.jsx | 246 ++------ frontend/src/pages/admin/CreateChallenge.jsx | 567 ++++++++----------- frontend/src/pages/user/UserProfile.jsx | 110 +--- 30 files changed, 1927 insertions(+), 1277 deletions(-) create mode 100644 backend/atlas_backend/auth.py create mode 100644 frontend/src/components/AdminRoute.jsx create mode 100644 frontend/src/pages/TeamProfile.jsx create mode 100644 frontend/src/pages/admin/AdminChallenges.jsx diff --git a/backend/atlas_backend/auth.py b/backend/atlas_backend/auth.py new file mode 100644 index 0000000..6797d05 --- /dev/null +++ b/backend/atlas_backend/auth.py @@ -0,0 +1,11 @@ +from rest_framework_simplejwt.authentication import JWTAuthentication +from rest_framework_simplejwt.exceptions import InvalidToken +from .models import User + +class CustomJWTAuthentication(JWTAuthentication): + def get_user(self, validated_token): + try: + user_id = validated_token['user_id'] + return User.objects.get(id=user_id) + except (KeyError, User.DoesNotExist): + raise InvalidToken('Token contained no recognizable user identification') \ No newline at end of file diff --git a/backend/atlas_backend/models.py b/backend/atlas_backend/models.py index 6a5c8bb..f85b146 100644 --- a/backend/atlas_backend/models.py +++ b/backend/atlas_backend/models.py @@ -1,6 +1,8 @@ from django.db import models from django.contrib.auth.models import AbstractUser, BaseUserManager, Group from django.core.validators import MinValueValidator +from django.contrib.auth.hashers import make_password, check_password +from django.db.models import CharField, TextField, IntegerField, BooleanField, DateTimeField class CustomUserManager(BaseUserManager): def create_user(self, email, password=None, **extra_fields): @@ -36,10 +38,18 @@ class Team(models.Model): challenges = models.ManyToManyField("Challenge", blank=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + password = models.CharField(max_length=128, default=make_password('default_password')) + team_email = models.EmailField(unique=True, default='team@example.com') def __str__(self): return self.name + def set_password(self, raw_password): + self.password = make_password(raw_password) + + def check_password(self, raw_password): + return check_password(raw_password, self.password) + class User(AbstractUser): email = models.EmailField(unique=True) team = models.ForeignKey(Team, on_delete=models.SET_NULL, null=True, blank=True, related_name="members") @@ -58,12 +68,12 @@ def has_role(self, role_name): class Challenge(models.Model): CATEGORY_CHOICES = [ - ("web", "Web"), - ("crypto", "Cryptography"), - ("pwn", "Binary Exploitation"), - ("reverse", "Reverse Engineering"), - ("forensics", "Forensics"), - ("misc", "Miscellaneous"), + ('web', 'Web'), + ('crypto', 'Cryptography'), + ('pwn', 'Binary Exploitation'), + ('reverse', 'Reverse Engineering'), + ('forensics', 'Forensics'), + ('misc', 'Miscellaneous'), ] title = models.CharField(max_length=200, unique=True) @@ -75,6 +85,9 @@ class Challenge(models.Model): max_team_size = models.IntegerField(default=4) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + is_hidden = models.BooleanField(default=False) + hints = models.JSONField(default=list, blank=True) + file_links = models.JSONField(default=list, blank=True) def __str__(self): return self.title @@ -103,7 +116,7 @@ def __str__(self): class Submission(models.Model): team = models.ForeignKey(Team, on_delete=models.CASCADE, related_name="submissions") challenge = models.ForeignKey(Challenge, on_delete=models.CASCADE, related_name="submissions") - user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='submissions') + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='submissions', default=1) flag_submitted = models.CharField(max_length=200, default="") is_correct = models.BooleanField(default=False) points_awarded = models.IntegerField(validators=[MinValueValidator(0)]) diff --git a/backend/atlas_backend/serializers.py b/backend/atlas_backend/serializers.py index f2ca119..1a2929c 100644 --- a/backend/atlas_backend/serializers.py +++ b/backend/atlas_backend/serializers.py @@ -17,34 +17,21 @@ def create(self, validated_data): class ChallengeSerializer(serializers.ModelSerializer): is_solved = serializers.SerializerMethodField() - attempts = serializers.SerializerMethodField() class Meta: model = Challenge - fields = [ - 'id', - 'title', - 'description', - 'category', - 'max_points', - 'max_team_size', - 'docker_image', - 'is_solved', - 'attempts', - 'created_at', - 'updated_at' - ] - extra_kwargs = { - 'flag': {'write_only': True} # Never expose flag in API responses - } + fields = ['id', 'name', 'description', 'points', 'category', + 'difficulty', 'is_solved'] def get_is_solved(self, obj): - team = self.context.get('user_team') - return team and obj.submissions.filter(team=team, is_correct=True).exists() - - def get_attempts(self, obj): - team = self.context.get('user_team') - return team and obj.submissions.filter(team=team).count() or 0 + user_team = self.context.get('user_team') + if not user_team: + return False + return Submission.objects.filter( + team=user_team, + challenge=obj, + is_correct=True + ).exists() class TeamSerializer(serializers.ModelSerializer): members = UserSerializer(many=True, read_only=True) diff --git a/backend/atlas_backend/urls.py b/backend/atlas_backend/urls.py index a651433..7445bee 100644 --- a/backend/atlas_backend/urls.py +++ b/backend/atlas_backend/urls.py @@ -1,30 +1,33 @@ # backend/atlas_backend/urls.py from django.urls import path from . import views +from rest_framework_simplejwt.views import TokenRefreshView urlpatterns = [ # Auth routes path('auth/register', views.signup, name='signup'), path('auth/login', views.signin, name='signin'), - path('auth/forgot-password', views.request_password_reset, name='request_password_reset'), - path('auth/reset-password', views.reset_password, name='reset_password'), + path('auth/refresh', views.token_refresh, name='token_refresh'), # Challenge routes path('challenges', views.get_challenges, name='get_challenges'), path('challenges//submit', views.submit_flag, name='submit_flag'), - path('challenges//start', views.start_challenge, name='start_challenge'), # Team routes path('teams', views.get_teams, name='get_teams'), - path('teams/create', views.create_team, name='create_team'), - path('teams/join', views.join_team, name='join_team'), - path('teams/leave', views.leave_team, name='leave_team'), + path('teams/profile', views.team_profile, name='team_profile'), # Scoreboard route path('scoreboard', views.get_scoreboard, name='get_scoreboard'), - - # User routes - path('user/update-info', views.update_user_info, name='update_user_info'), - path('user/profile', views.get_user_profile, name='get_user_profile'), - path('user/team/history', views.get_team_history, name='get_team_history'), + path('auth/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), + + # Admin routes + path('auth/admin/login', views.admin_login, name='admin_login'), + path('challenges/admin', views.admin_get_challenges, name='admin_get_challenges'), + path('challenges/create', views.create_challenge, name='create_challenge'), + path('challenges//update', views.update_challenge, name='update_challenge'), + path('challenges//delete', views.delete_challenge, name='delete_challenge'), + path('challenges/', views.get_challenge_detail, name='get_challenge_by_id'), + + ] diff --git a/backend/atlas_backend/views.py b/backend/atlas_backend/views.py index dfa979b..2253391 100644 --- a/backend/atlas_backend/views.py +++ b/backend/atlas_backend/views.py @@ -7,75 +7,168 @@ from datetime import datetime, timedelta import jwt from django.core.mail import send_mail +from django.contrib.auth.hashers import make_password, check_password from rest_framework import status from rest_framework.decorators import api_view, permission_classes from rest_framework.response import Response -from rest_framework.permissions import AllowAny, IsAuthenticated -from rest_framework_simplejwt.tokens import RefreshToken +from rest_framework.permissions import AllowAny, IsAuthenticated, IsAdminUser +from rest_framework_simplejwt.tokens import RefreshToken, AccessToken from .models import User, Challenge, Submission, Team, Container from .serializers import SignupSerializer, ChallengeSerializer, TeamSerializer, SubmissionSerializer, UserSerializer +import logging + +logger = logging.getLogger('atlas_backend') + @api_view(['POST']) @permission_classes([AllowAny]) def signup(request): - serializer = SignupSerializer(data=request.data) - if serializer.is_valid(): - user = serializer.save() - refresh = RefreshToken.for_user(user) - - refresh['user'] = { - 'id': user.id, - 'email': user.email, - 'username': user.username, - 'isAdmin': user.is_staff, - 'teamId': user.team.id if user.team else None - } + data = request.data + try: + # Create team first + team = Team.objects.create( + name=data['teamName'], + team_email=data['teamEmail'], + team_size=3, + password=make_password(data['password']) + ) + + # Create users for all team members + users = [] + # Create required first member + member1 = User.objects.create_user( + username=data['member1Name'], + email=data['member1Email'], + password=data['password'], + team=team + ) + users.append(member1) + + # Create optional members if provided + if data.get('member2Email'): + member2 = User.objects.create_user( + username=data['member2Name'], + email=data['member2Email'], + password=data['password'], + team=team + ) + users.append(member2) + + if data.get('member3Email'): + member3 = User.objects.create_user( + username=data['member3Name'], + email=data['member3Email'], + password=data['password'], + team=team + ) + users.append(member3) + + # Generate token based on first team member + refresh = RefreshToken.for_user(member1) + refresh['team_id'] = team.id + refresh['team_name'] = team.name + refresh['team_email'] = team.team_email + refresh['member_count'] = len(users) + refresh['member_emails'] = [user.email for user in users] + return Response({ 'refresh': str(refresh), 'access': str(refresh.access_token), }, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) @api_view(['POST']) @permission_classes([AllowAny]) def signin(request): - email = request.data.get('email') + team_name = request.data.get('teamName') password = request.data.get('password') - if not email or not password: - return Response({'error': 'Email and password are required'}, status=status.HTTP_400_BAD_REQUEST) + if not team_name or not password: + return Response( + {'error': 'Team name and password are required'}, + status=status.HTTP_400_BAD_REQUEST + ) - user = authenticate(email=email, password=password) - if user: - refresh = RefreshToken.for_user(user) + try: + team = Team.objects.get(name=team_name) + if not check_password(password, team.password): + raise Team.DoesNotExist + + # Get the first team member as the "main" user for authentication + team_member = User.objects.filter(team=team).first() + if not team_member: + return Response( + {'error': 'No team members found'}, + status=status.HTTP_400_BAD_REQUEST + ) + + team_members = User.objects.filter(team=team) + + refresh = RefreshToken.for_user(team_member) - refresh['user'] = { - 'id': user.id, - 'email': user.email, - 'username': user.username, - 'isAdmin': user.is_staff, - 'teamId': user.team.id if user.team else None - } + # Add required claims + refresh['user_id'] = team_member.id + refresh['username'] = team_member.username + refresh['email'] = team_member.email + + # Add custom team claims + refresh['team_id'] = team.id + refresh['team_name'] = team.name + refresh['team_email'] = team.team_email + refresh['member_count'] = team_members.count() + refresh['member_emails'] = [member.email for member in team_members] + + logger.info('Generated token with payload: %s', { + 'user_id': team_member.id, + 'username': team_member.username, + 'email': team_member.email, + 'team_id': team.id, + 'team_name': team.name, + 'team_email': team.team_email, + 'member_count': team_members.count(), + }) return Response({ - 'refresh': str(refresh), 'access': str(refresh.access_token), + 'refresh': str(refresh), }) - return Response({'error': 'Invalid credentials'}, status=status.HTTP_401_UNAUTHORIZED) + except Team.DoesNotExist: + return Response( + {'error': 'Invalid credentials'}, + status=status.HTTP_401_UNAUTHORIZED + ) @api_view(['GET']) @permission_classes([IsAuthenticated]) def get_challenges(request): - challenges = Challenge.objects.all() - serializer = ChallengeSerializer( - challenges, - many=True, - context={'user_team': request.user.team} + logger.info("=== GET /challenges ===") + logger.info(f"Auth header: {request.headers.get('Authorization')}") + logger.info(f"User: {request.user}") + logger.info(f"Is authenticated: {request.user.is_authenticated}") + + # Fetch only non-hidden challenges + challenges = Challenge.objects.filter(is_hidden=False).values( + 'id', + 'title', + 'description', + 'category', + 'max_points', + 'hints', + 'file_links', + 'docker_image' ) - return Response(serializer.data) + + # Convert QuerySet to list for JSON serialization + challenges_list = list(challenges) + + logger.info(f"Challenges: {challenges_list}") + + return Response(challenges_list) @api_view(['POST']) @permission_classes([IsAuthenticated]) @@ -393,31 +486,24 @@ def reset_password(request): @api_view(['POST']) @permission_classes([IsAuthenticated]) def create_challenge(request): - if not request.user.is_staff: + # Check if user is superuser + if not request.user.is_superuser: return Response( - {'error': 'Only administrators can create challenges'}, + {"error": "Only administrators can create challenges"}, status=status.HTTP_403_FORBIDDEN ) try: data = request.data - required_fields = ['title', 'description', 'category', 'docker_image', 'flag', 'max_points'] # Validate required fields - missing_fields = [field for field in required_fields if not data.get(field)] - if missing_fields: - return Response( - {'error': f'Missing required fields: {", ".join(missing_fields)}'}, - status=status.HTTP_400_BAD_REQUEST - ) - - # Validate category - valid_categories = dict(Challenge.CATEGORY_CHOICES) - if data['category'] not in valid_categories: - return Response( - {'error': f'Invalid category. Valid categories are: {", ".join(valid_categories.keys())}'}, - status=status.HTTP_400_BAD_REQUEST - ) + required_fields = ['title', 'description', 'category', 'docker_image', 'flag', 'max_points'] + for field in required_fields: + if not data.get(field): + return Response( + {"error": f"{field} is required"}, + status=status.HTTP_400_BAD_REQUEST + ) # Create challenge challenge = Challenge.objects.create( @@ -427,22 +513,71 @@ def create_challenge(request): docker_image=data['docker_image'], flag=data['flag'], max_points=int(data['max_points']), - max_team_size=int(data.get('max_team_size', 4)) + max_team_size=3, # Fixed as 3 + is_hidden=data.get('is_hidden', False), + hints=data.get('hints', []), + file_links=data.get('file_links', []) ) + return Response({ + "message": "Challenge created successfully", + "challenge_id": challenge.id + }, status=status.HTTP_201_CREATED) + + except Exception as e: return Response( - ChallengeSerializer(challenge).data, - status=status.HTTP_201_CREATED + {"error": "Failed to create challenge"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR ) - except ValueError as e: +@api_view(['PATCH']) +@permission_classes([IsAdminUser]) +def update_challenge(request, challenge_id): + try: + challenge = Challenge.objects.get(id=challenge_id) + data = request.data + + # Update fields if they exist in request + fields = [ + 'title', 'description', 'category', 'docker_image', + 'flag', 'max_points', 'is_hidden', 'hints', 'file_links' + ] + + for field in fields: + if field in data: + setattr(challenge, field, data[field]) + + challenge.save() + return Response({"message": "Challenge updated successfully"}) + + except Challenge.DoesNotExist: return Response( - {'error': 'Invalid numeric value provided'}, - status=status.HTTP_400_BAD_REQUEST + {"error": "Challenge not found"}, + status=status.HTTP_404_NOT_FOUND ) except Exception as e: + logger.error(f"Error updating challenge: {str(e)}") return Response( - {'error': str(e)}, + {"error": "Failed to update challenge"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + +@api_view(['DELETE']) +@permission_classes([IsAdminUser]) +def delete_challenge(request, challenge_id): + try: + challenge = Challenge.objects.get(id=challenge_id) + challenge.delete() + logger.info(f"Challenge {challenge_id} deleted successfully") + return Response({"message": "Challenge deleted successfully"}, status=status.HTTP_200_OK) + + except Challenge.DoesNotExist: + logger.error(f"Challenge {challenge_id} not found") + return Response({"error": "Challenge not found"}, status=status.HTTP_404_NOT_FOUND) + except Exception as e: + logger.error(f"Error deleting challenge {challenge_id}: {str(e)}") + return Response( + {"error": "An error occurred while deleting the challenge"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) @@ -469,4 +604,203 @@ def get_team_history(request): submissions = Submission.objects.filter(team=request.user.team) serializer = SubmissionSerializer(submissions, many=True) - return Response(serializer.data) \ No newline at end of file + return Response(serializer.data) + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def team_profile(request): + try: + # Get token from authorization header + auth_header = request.headers.get('Authorization') + if not auth_header or not auth_header.startswith('Bearer '): + return Response({'error': 'No token provided'}, status=status.HTTP_401_UNAUTHORIZED) + + # Extract token and decode it + token = auth_header.split(' ')[1] + decoded_token = AccessToken(token) + + # Get team info from token + team_id = decoded_token['team_id'] + team_name = decoded_token['team_name'] + team_email = decoded_token['team_email'] + member_count = decoded_token['member_count'] + member_emails = decoded_token['member_emails'] + + # Get team from database using team_id from token + team = Team.objects.get(id=team_id) + + # Get team members using team_id + team_members = User.objects.filter(team_id=team_id) + + # Get team statistics + solved_challenges = Challenge.objects.filter( + submissions__team=team, + submissions__is_correct=True + ).distinct().count() + + # Get total score + total_score = team.submissions.filter(is_correct=True).aggregate( + total=Sum('challenge__points') + )['total'] or 0 + + # Get team rank + teams_ranking = Team.objects.annotate( + score=Sum('submissions__challenge__points', + filter=Q(submissions__is_correct=True)) + ).order_by('-score') + team_rank = list(teams_ranking.values_list('id', flat=True)).index(team_id) + 1 + + # Get recent activity + recent_submissions = team.submissions.filter( + is_correct=True + ).order_by('-submitted_at')[:5] + + response_data = { + 'id': team_id, + 'name': team_name, + 'team_email': team_email, + 'members': [{ + 'id': member.id, + 'username': member.username, + 'email': member.email + } for member in team_members], + 'total_score': total_score, + 'solved_challenges': solved_challenges, + 'rank': team_rank, + 'recent_activity': [{ + 'id': sub.id, + 'challenge_name': sub.challenge.name, + 'points': sub.challenge.points, + 'solved_at': sub.submitted_at.isoformat() + } for sub in recent_submissions] + } + + return Response(response_data) + + except Exception as e: + print(f"Error in team_profile: {str(e)}") + return Response( + {'error': str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + +@api_view(['POST']) +@permission_classes([AllowAny]) +def token_refresh(request): + try: + refresh_token = request.data.get('refresh') + if not refresh_token: + return Response( + {'error': 'Refresh token required'}, + status=status.HTTP_400_BAD_REQUEST + ) + + refresh = RefreshToken(refresh_token) + return Response({ + 'access': str(refresh.access_token), + }) + except Exception as e: + return Response( + {'error': str(e)}, + status=status.HTTP_401_UNAUTHORIZED + ) + +@api_view(['POST']) +@permission_classes([AllowAny]) +def admin_login(request): + try: + email = request.data.get('email') + password = request.data.get('password') + + # Get superuser by email + user = User.objects.get(email=email, is_superuser=True) + + if not user.check_password(password): + raise User.DoesNotExist + + refresh = RefreshToken.for_user(user) + + # Add admin claims to token + refresh['is_admin'] = True + refresh['email'] = user.email + refresh['user_id'] = user.id + + return Response({ + 'access': str(refresh.access_token), + 'refresh': str(refresh), + }) + + except User.DoesNotExist: + return Response( + {'error': 'Invalid admin credentials'}, + status=status.HTTP_401_UNAUTHORIZED + ) + except Exception as e: + return Response( + {'error': 'Login failed'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def admin_get_challenges(request): + # Check if user is superuser + if not request.user.is_superuser: + return Response( + {"error": "Only administrators can access this"}, + status=status.HTTP_403_FORBIDDEN + ) + + try: + challenges = Challenge.objects.all() + data = [] + for challenge in challenges: + data.append({ + 'id': challenge.id, + 'title': challenge.title, + 'description': challenge.description, + 'category': challenge.category, + 'docker_image': challenge.docker_image, + 'flag': challenge.flag, + 'max_points': challenge.max_points, + 'max_team_size': challenge.max_team_size, + 'is_hidden': challenge.is_hidden, + 'hints': challenge.hints, + 'file_links': challenge.file_links, + 'created_at': challenge.created_at, + 'updated_at': challenge.updated_at + }) + return Response(data) + except Exception as e: + return Response( + {"error": "Failed to fetch challenges"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) + +@api_view(['GET']) +@permission_classes([IsAdminUser]) +def get_challenge_detail(request, challenge_id): + try: + challenge = Challenge.objects.get(id=challenge_id) + data = { + 'id': challenge.id, + 'title': challenge.title, + 'description': challenge.description, + 'category': challenge.category, + 'docker_image': challenge.docker_image, + 'flag': challenge.flag, + 'max_points': challenge.max_points, + 'max_team_size': challenge.max_team_size, + 'created_at': challenge.created_at, + 'updated_at': challenge.updated_at, + 'is_hidden': challenge.is_hidden, + 'hints': challenge.hints, + 'file_links': challenge.file_links + } + return Response(data) + except Challenge.DoesNotExist: + return Response( + {"error": "Challenge not found"}, + status=status.HTTP_404_NOT_FOUND + ) + diff --git a/backend/backend/settings.py b/backend/backend/settings.py index 3f00863..d8f116b 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -58,6 +58,14 @@ CORS_ALLOW_ALL_ORIGINS = True +CORS_ALLOW_CREDENTIALS = True +CORS_ALLOWED_ORIGINS = [ + "http://localhost:3000", # React development server +] + +CSRF_TRUSTED_ORIGINS = [ + "http://localhost:3000", # React development server +] ROOT_URLCONF = "backend.urls" @@ -94,22 +102,34 @@ "django.contrib.auth.backends.ModelBackend", ] -# JWT Authentication settings +# REST Framework settings REST_FRAMEWORK = { - "DEFAULT_AUTHENTICATION_CLASSES": [ - "rest_framework_simplejwt.authentication.JWTAuthentication", - ], - "DEFAULT_PERMISSION_CLASSES": [ - "rest_framework.permissions.IsAuthenticated", + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'atlas_backend.auth.CustomJWTAuthentication', + ), + 'DEFAULT_PERMISSION_CLASSES': ( + 'rest_framework.permissions.IsAuthenticated', + ), + 'DEFAULT_PARSER_CLASSES': [ + 'rest_framework.parsers.JSONParser', + 'rest_framework.parsers.FormParser', + 'rest_framework.parsers.MultiPartParser' ], } +# JWT settings SIMPLE_JWT = { - "ACCESS_TOKEN_LIFETIME": timedelta(minutes=15), # Adjust as needed - "REFRESH_TOKEN_LIFETIME": timedelta(days=1), - "ALGORITHM": "HS256", - "SIGNING_KEY": django.conf.settings.SECRET_KEY, # Use django.conf.settings instead - "AUTH_HEADER_TYPES": ("Bearer",), + 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), + 'ALGORITHM': 'HS256', + 'SIGNING_KEY': SECRET_KEY, + 'AUTH_HEADER_TYPES': ('Bearer',), + 'USER_ID_FIELD': 'id', + 'USER_ID_CLAIM': 'user_id', + 'TOKEN_TYPE_CLAIM': 'token_type', + 'JTI_CLAIM': 'jti', + 'TOKEN_USER_CLASS': 'atlas_backend.models.User', + 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',), } # Password validation diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 89df2d3..4a7008d 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -22,6 +22,8 @@ import AdminChallengeDetail from './pages/admin/ChallengeDetail'; import CreateChallenge from './pages/admin/CreateChallenge'; import AdminLogin from './pages/admin/AdminLogin'; import UserProfile from './pages/user/UserProfile'; +import TeamProfile from './pages/TeamProfile'; +import AdminRoute from './components/AdminRoute'; function App() { return ( @@ -30,68 +32,74 @@ function App() {
+ {/* Public routes */} } /> } /> } /> } /> + } /> - {/* Redirect /user/dashboard to /challenges */} + {/* Protected routes */} } - /> - } - /> - - {/* Regular user routes */} - - + + } /> + } /> - + + } /> + {/* Protected admin routes */} + + + + } + /> + {/* Admin routes */} - - } /> - - - - } - > - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - + + + + } + /> + + + + } + /> + + + + } + /> {/* Catch-all route */} } /> diff --git a/frontend/src/api/auth.js b/frontend/src/api/auth.js index 5227d8f..b7ba1be 100644 --- a/frontend/src/api/auth.js +++ b/frontend/src/api/auth.js @@ -1,17 +1,22 @@ import apiClient from './config'; -export const register = async (userData) => { - const response = await apiClient.post('/auth/register', userData); +export const login = async (teamName, password) => { + const response = await apiClient.post('/auth/login', { + teamName, + password + }); return response.data; }; -export const login = async (credentials) => { - const response = await apiClient.post('/auth/login', credentials); +export const register = async (formData) => { + const response = await apiClient.post('/auth/register', formData); return response.data; }; export const requestPasswordReset = async (email) => { - const response = await apiClient.post('/auth/forgot-password', { email }); + const response = await apiClient.post('/auth/forgot-password', { + email, + }); return response.data; }; @@ -22,3 +27,16 @@ export const resetPassword = async (token, newPassword) => { }); return response.data; }; + +export const adminLogin = async (email, password) => { + try { + const response = await apiClient.post('/auth/admin/login', { + email, + password + }); + return response.data; + } catch (error) { + console.error('Admin login error:', error); + throw error; + } +}; diff --git a/frontend/src/api/challenges.js b/frontend/src/api/challenges.js index cb08a22..4a03f64 100644 --- a/frontend/src/api/challenges.js +++ b/frontend/src/api/challenges.js @@ -1,22 +1,94 @@ import apiClient from './config'; +import { jwtDecode } from 'jwt-decode'; export const getChallenges = async () => { - const response = await apiClient.get('/challenges'); - return response.data; + console.log('Token before request:', localStorage.getItem('token')); + console.log('Sending the request with token:', jwtDecode(localStorage.getItem('token'))); + try { + const response = await apiClient.get('/challenges'); + return response.data; + } catch (error) { + console.error('Challenge request error:', { + status: error.response?.status, + data: error.response?.data, + headers: error.config?.headers + }); + throw error; + } }; export const submitFlag = async (challengeId, flag) => { - const response = await apiClient.post(`/challenges/${challengeId}/submit`, { - flag_submitted: flag, - }); - return response.data; + try { + const response = await apiClient.post(`/challenges/${challengeId}/submit`, { + flag_submitted: flag, + }); + return response.data; + } catch (error) { + console.error('Error submitting flag:', error); + throw error; + } }; export const startChallenge = async (challengeId) => { - const response = await apiClient.post(`/challenges/${challengeId}/start`); - return { - container: response.data.container, - port: response.data.port, - status: response.data.status, - }; + try { + const response = await apiClient.post(`/challenges/${challengeId}/start`); + return response.data; + } catch (error) { + console.error('Error starting challenge:', error); + throw error; + } +}; + +// Admin challenge APIs +export const getAdminChallenges = async () => { + try { + const response = await apiClient.get('/challenges/admin'); + return response.data; + } catch (error) { + console.error('Error fetching admin challenges:', error); + throw error; + } +}; + +export const createChallenge = async (challengeData) => { + try { + const response = await apiClient.post('/challenges/create', challengeData); + return response.data; + } catch (error) { + console.error('Error creating challenge:', error); + throw error; + } +}; + +export const updateChallenge = async (challengeData, challengeId) => { + try { + const response = await apiClient.patch( + `/challenges/${challengeId}/update`, + challengeData + ); + return response.data; + } catch (error) { + console.error('Error updating challenge:', error); + throw error; + } +}; + +export const deleteChallenge = async (challengeId) => { + try { + const response = await apiClient.delete(`/challenges/${challengeId}/delete`); + return response.data; + } catch (error) { + console.error('Error deleting challenge:', error); + throw error; + } +}; + +export const getChallengeById = async (challengeId) => { + try { + const response = await apiClient.get(`/challenges/${challengeId}`); + return response.data; + } catch (error) { + console.error('Error fetching challenge:', error); + throw error; + } }; diff --git a/frontend/src/api/config.js b/frontend/src/api/config.js index 924569f..e2e5a7f 100644 --- a/frontend/src/api/config.js +++ b/frontend/src/api/config.js @@ -1,56 +1,36 @@ import axios from 'axios'; -export const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'; - const apiClient = axios.create({ - baseURL: API_URL, - timeout: 10000, + baseURL: 'http://localhost:8000', headers: { 'Content-Type': 'application/json', - }, + } }); -// Token refresh logic -apiClient.interceptors.response.use( - (response) => response, - async (error) => { - const originalRequest = error.config; - if (error.response?.status === 401 && !originalRequest._retry) { - originalRequest._retry = true; - try { - const refreshToken = localStorage.getItem('refreshToken'); - const { data } = await apiClient.post('/auth/refresh', { - refresh: refreshToken, - }); - localStorage.setItem('token', data.access); - apiClient.defaults.headers.Authorization = `Bearer ${data.access}`; - return apiClient(originalRequest); - } catch (refreshError) { - localStorage.removeItem('token'); - localStorage.removeItem('refreshToken'); - window.location.href = '/login'; - } +// Add request interceptor to include auth token +apiClient.interceptors.request.use( + (config) => { + const tokenString = localStorage.getItem('token'); + if (tokenString) { + const { access } = JSON.parse(tokenString); + config.headers.Authorization = `Bearer ${access}`; } - return Promise.reject(error); + return config; }, + (error) => Promise.reject(error) ); -// Add centralized error handler +// Add response interceptor for error handling apiClient.interceptors.response.use( - response => response, - error => { - console.error(`API Error: ${error.message}`); + (response) => response, + (error) => { + console.error('Response error:', error); + if (error.response?.status === 403) { + // Handle forbidden error - could be auth issue + console.error('Authentication error:', error.response.data); + } return Promise.reject(error); } ); -// Auth header injection -apiClient.interceptors.request.use((config) => { - const token = localStorage.getItem('token'); - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } - return config; -}); - export default apiClient; diff --git a/frontend/src/api/teams.js b/frontend/src/api/teams.js index 6287cfc..ea52c87 100644 --- a/frontend/src/api/teams.js +++ b/frontend/src/api/teams.js @@ -45,3 +45,13 @@ export const leaveTeam = async () => { throw error; } }; + +export const getTeamProfile = async () => { + try { + const response = await apiClient.get('/teams/profile'); + return response.data; + } catch (error) { + console.error('Error fetching team profile:', error); + throw error; + } +}; diff --git a/frontend/src/api/user.js b/frontend/src/api/user.js index a935ec2..d26fd51 100644 --- a/frontend/src/api/user.js +++ b/frontend/src/api/user.js @@ -12,20 +12,25 @@ export const getUserProfile = async () => { }; // Update user information -export const updateUserInfo = async (userData) => { +export const updateUserInfo = async (data) => { try { - const response = await apiClient.post('/user/update-info', userData); + const response = await apiClient.put('/user/profile', data); return response.data; } catch (error) { - console.error('Error updating user info:', error); + console.error('Error in updateUserInfo:', error); throw error; } }; // Get team history for the user export const getTeamHistory = async () => { - const response = await apiClient.get('/user/team/history'); - return response.data; + try { + const response = await apiClient.get('/user/team/history'); + return response.data; + } catch (error) { + console.error('Error in getTeamHistory:', error); + throw error; + } }; export const leaveTeam = async () => { diff --git a/frontend/src/components/AdminRoute.jsx b/frontend/src/components/AdminRoute.jsx new file mode 100644 index 0000000..6e8dc09 --- /dev/null +++ b/frontend/src/components/AdminRoute.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { Navigate } from 'react-router-dom'; +import { useAuth } from '../hooks/useAuth'; + +function AdminRoute({ children }) { + const { isAuthenticated, isAdmin } = useAuth(); + + if (!isAuthenticated) { + return ; + } + + if (!isAdmin) { + return ; + } + + return children; +} + +export default AdminRoute; \ No newline at end of file diff --git a/frontend/src/components/ChallengeCard.jsx b/frontend/src/components/ChallengeCard.jsx index acacf2f..13db465 100644 --- a/frontend/src/components/ChallengeCard.jsx +++ b/frontend/src/components/ChallengeCard.jsx @@ -1,124 +1,37 @@ -import React, { useState } from 'react'; -import PropTypes from 'prop-types'; -import { startChallenge, submitFlag } from '../api/challenges'; +import React from 'react'; function ChallengeCard({ challenge }) { - const [containerDetails, setContainerDetails] = useState(null); - const [flag, setFlag] = useState(''); - const [error, setError] = useState(''); - const [loading, setLoading] = useState(false); - - const handleStartChallenge = async () => { - try { - setLoading(true); - setError(''); - const data = await startChallenge(challenge.id); - setContainerDetails(data); - } catch (error) { - setError('Failed to start challenge. Please try again.'); - console.error('Failed to start challenge:', error); - } finally { - setLoading(false); - } - }; - - const handleSubmitFlag = async () => { - if (!flag.trim()) { - setError('Please enter a flag'); - return; - } - - try { - setLoading(true); - setError(''); - const data = await submitFlag(challenge.id, flag); - if (data.correct) { - alert('Congratulations! Flag is correct!'); - setFlag(''); - } else { - setError('Incorrect flag. Try again.'); - } - } catch (error) { - setError('Failed to submit flag. Please try again.'); - console.error('Failed to submit flag:', error); - } finally { - setLoading(false); - } + const categoryColors = { + web: 'bg-blue-100 text-blue-800', + crypto: 'bg-green-100 text-green-800', + pwn: 'bg-red-100 text-red-800', + reverse: 'bg-purple-100 text-purple-800', + forensics: 'bg-yellow-100 text-yellow-800', + misc: 'bg-gray-100 text-gray-800' }; return ( -
-

{challenge.title}

-

{challenge.description}

-
- - Category: {challenge.category} - - - {challenge.max_points} pts +
+
+

{challenge.title}

+ {challenge.max_points}pts +
+

{challenge.description}

+
+ + {challenge.category} + {challenge.hints?.length > 0 && ( + + {challenge.hints.length} hint{challenge.hints.length !== 1 ? 's' : ''} available + + )}
- - {error &&
{error}
} - - {!containerDetails ? ( - - ) : ( -
-
-

Connection Details:

-
-

Container ID: {containerDetails.container}

-

Port: {containerDetails.port}

-

Status: {containerDetails.status}

-
-
- -
- setFlag(e.target.value)} - placeholder="Enter flag" - className="w-full p-2 border rounded focus:ring-2 focus:ring-blue-500" - disabled={loading} - /> - -
-
+ {challenge.is_solved && ( +
✓ Solved
)}
); } -ChallengeCard.propTypes = { - challenge: PropTypes.shape({ - id: PropTypes.number.isRequired, - title: PropTypes.string.isRequired, - description: PropTypes.string.isRequired, - category: PropTypes.string.isRequired, - max_points: PropTypes.number.isRequired, - }).isRequired, -}; - export default ChallengeCard; diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index 956fa4a..9a22edc 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -3,11 +3,12 @@ import { Link, useNavigate } from 'react-router-dom'; import { useAuth } from '../hooks/useAuth'; function Navbar() { - const { isAuthenticated, user, logout } = useAuth(); + const { isAuthenticated, isAdmin, logout } = useAuth(); const navigate = useNavigate(); const handleLogout = () => { logout(); + navigate('/'); }; return ( @@ -22,32 +23,36 @@ function Navbar() { Home {isAuthenticated ? ( - <> - {!user?.isAdmin && ( - <> - - Join Team - - - Challenges - - - Scoreboard - - - User - - - )} - {user?.isAdmin && ( + isAdmin ? ( + // Admin Navigation + <> - Admin + Dashboard - )} - - + + + ) : ( + // Team Navigation + <> + + Join Team + + + Challenges + + + Scoreboard + + + Team Profile + + + + ) ) : ( <> diff --git a/frontend/src/components/ProtectedRoute.jsx b/frontend/src/components/ProtectedRoute.jsx index b3715a6..b0bf2ca 100644 --- a/frontend/src/components/ProtectedRoute.jsx +++ b/frontend/src/components/ProtectedRoute.jsx @@ -2,22 +2,15 @@ import React from 'react'; import { Navigate, useLocation } from 'react-router-dom'; import { useAuth } from '../hooks/useAuth'; -function ProtectedRoute({ children, requireAdmin }) { - const { isAuthenticated, user } = useAuth(); +function ProtectedRoute({ children }) { + const { isAuthenticated } = useAuth(); const location = useLocation(); if (!isAuthenticated) { + // Redirect to login but remember where we came from return ; } - if (requireAdmin && !user?.isAdmin) { - return ; - } - - if (!requireAdmin && user?.isAdmin) { - return ; - } - return children; } diff --git a/frontend/src/context/AuthContext.jsx b/frontend/src/context/AuthContext.jsx index e2f9da1..edc7076 100644 --- a/frontend/src/context/AuthContext.jsx +++ b/frontend/src/context/AuthContext.jsx @@ -1,57 +1,87 @@ import React, { createContext, useState, useEffect } from 'react'; import { jwtDecode } from 'jwt-decode'; -import { register, login } from '../api/auth'; // Ensure these imports are correct -import { useNavigate } from 'react-router-dom'; -import apiClient from '../api/apiClient'; export const AuthContext = createContext(); +const decodeToken = (token) => { + try { + return jwtDecode(token); + } catch (error) { + return null; + } +}; + export const AuthProvider = ({ children }) => { const [user, setUser] = useState(null); + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [isAdmin, setIsAdmin] = useState(false); useEffect(() => { + // Check for token on load const token = localStorage.getItem('token'); + console.log('Initial token check:', !!token); + if (token) { try { - const decoded = jwtDecode(token); - if (decoded.exp > Date.now() / 1000) { - setUser(decoded.user); - apiClient.defaults.headers.Authorization = `Bearer ${token}`; - } else { - handleLogout(); - } + const decoded = jwtDecode(JSON.parse(token).access); + console.log('Token decoded successfully:', decoded); + + setUser({ + teamId: decoded.team_id, + teamName: decoded.team_name, + teamEmail: decoded.team_email, + memberCount: decoded.member_count, + memberEmails: decoded.member_emails + }); + setIsAuthenticated(true); + setIsAdmin(decoded.is_admin || false); + console.log('Auth state set to true on load'); } catch (error) { - handleLogout(); + console.error('Error loading user:', error); + localStorage.removeItem('token'); + setIsAuthenticated(false); } } }, []); - const handleLogin = async (credentials) => { - try { - const { data } = await apiClient.post('/auth/login', credentials); - localStorage.setItem('token', data.token); - apiClient.defaults.headers.Authorization = `Bearer ${data.token}`; - setUser(jwtDecode(data.token).user); - } catch (error) { - throw error; - } + const login = (tokenData) => { + console.log('Login called with token data:', tokenData); + + localStorage.setItem('token', JSON.stringify(tokenData)); + const decoded = decodeToken(tokenData.access); + + setUser({ + teamId: decoded.team_id, + teamName: decoded.team_name, + teamEmail: decoded.team_email, + memberCount: decoded.member_count, + memberEmails: decoded.member_emails + }); + setIsAuthenticated(true); + setIsAdmin(decoded?.is_admin || false); + console.log('Auth state set to true after login'); }; - const handleLogout = () => { + const logout = () => { localStorage.removeItem('token'); - delete apiClient.defaults.headers.Authorization; setUser(null); + setIsAuthenticated(false); + setIsAdmin(false); + console.log('Auth state set to false after logout'); }; + const value = { + user, + login, + logout, + isAuthenticated, + isAdmin + }; + + console.log('Current auth state:', value); + return ( - + {children} ); diff --git a/frontend/src/hooks/useAuth.js b/frontend/src/hooks/useAuth.js index bd78584..3fce285 100644 --- a/frontend/src/hooks/useAuth.js +++ b/frontend/src/hooks/useAuth.js @@ -1,11 +1,30 @@ import { useContext } from 'react' import { AuthContext } from '../context/AuthContext' +import { jwtDecode } from 'jwt-decode' -export const useAuth = () => { +export function useAuth() { const context = useContext(AuthContext) if (!context) { throw new Error('useAuth must be used within an AuthProvider') } - return context + + const isAdmin = () => { + try { + console.log('isAdmin called'); + const tokenString = localStorage.getItem('token') + if (!tokenString) return false + + const { access } = JSON.parse(tokenString) + const decoded = jwtDecode(access) + return decoded.is_admin === true + } catch (error) { + return false + } + } + + return { + ...context, + isAdmin: isAdmin() + } } diff --git a/frontend/src/pages/Challenges.jsx b/frontend/src/pages/Challenges.jsx index 7738b24..c1de95a 100644 --- a/frontend/src/pages/Challenges.jsx +++ b/frontend/src/pages/Challenges.jsx @@ -1,41 +1,95 @@ import React, { useState, useEffect } from 'react'; import { getChallenges } from '../api/challenges'; -import { useAuth } from '../hooks/useAuth'; import ChallengeCard from '../components/ChallengeCard'; function Challenges() { const [challenges, setChallenges] = useState([]); const [error, setError] = useState(''); const [loading, setLoading] = useState(true); - const { user } = useAuth(); + const [selectedCategory, setSelectedCategory] = useState('all'); useEffect(() => { + const fetchChallenges = async () => { + try { + setLoading(true); + const data = await getChallenges(); + console.log(data); + setChallenges(data); + } catch (err) { + console.error('Error fetching challenges:', err); + setError('Failed to fetch challenges'); + } finally { + setLoading(false); + } + }; + fetchChallenges(); }, []); - const fetchChallenges = async () => { - try { - setLoading(true); - const fetchedChallenges = await getChallenges(user.token); - setChallenges(fetchedChallenges); - } catch (err) { - setError('Failed to fetch challenges'); - } finally { - setLoading(false); + // Group challenges by category + const challengesByCategory = challenges.reduce((acc, challenge) => { + if (!acc[challenge.category]) { + acc[challenge.category] = []; } - }; + acc[challenge.category].push(challenge); + return acc; + }, {}); + + const categories = ['all', ...Object.keys(challengesByCategory)]; - if (loading) return
Loading...
; + if (loading) return
Loading challenges...
; return (
-

Challenges

- {error &&

{error}

} -
- {challenges.map((challenge) => ( - - ))} + {/* Category Filter */} +
+
+ {categories.map(category => ( + + ))} +
+ + {/* Challenges Display */} + {selectedCategory === 'all' ? ( + // Show all categories + Object.entries(challengesByCategory).map(([category, categoryChallenges]) => ( +
+

+ {category} Challenges +

+
+ {categoryChallenges.map(challenge => ( + + ))} +
+
+ )) + ) : ( + // Show selected category +
+

+ {selectedCategory} Challenges +

+
+ {challengesByCategory[selectedCategory]?.map(challenge => ( + + ))} +
+
+ )} + + {error &&

{error}

}
); } diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx index 7b419fa..2fa2924 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.jsx @@ -4,6 +4,7 @@ import { useAuth } from '../hooks/useAuth'; function Home() { const { isAuthenticated, user } = useAuth(); + console.log(user); return (
diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx index 47fba1c..10360ab 100644 --- a/frontend/src/pages/Login.jsx +++ b/frontend/src/pages/Login.jsx @@ -1,60 +1,69 @@ -import React, { useState } from 'react' -import { useNavigate, Link } from 'react-router-dom' +import React, { useState, useEffect } from 'react' +import { useNavigate, useLocation } from 'react-router-dom' import { login as apiLogin } from '../api/auth' import { useAuth } from '../hooks/useAuth' function Login() { - const [email, setEmail] = useState('') + const [teamName, setTeamName] = useState('') const [password, setPassword] = useState('') const [error, setError] = useState('') const navigate = useNavigate() - const { login } = useAuth() + const location = useLocation() + const { login, isAuthenticated } = useAuth() + + useEffect(() => { + if (isAuthenticated) { + const from = location.state?.from?.pathname || '/challenges' + navigate(from, { replace: true }) + } + }, [isAuthenticated, navigate, location]) const handleSubmit = async (e) => { e.preventDefault() + setError('') + try { - const data = await apiLogin(email, password) - login(data) - navigate('/') + const response = await apiLogin(teamName, password) + login(response) } catch (err) { - setError('Login failed. Please check your credentials.') + console.error('Login error:', err) + setError('Invalid team credentials') } } return ( -
-

Login

- {error &&

{error}

} +
+

Team Login

+ {error && ( +
+ {error} +
+ )}
- + setEmail(e.target.value)} - className="input" + type="text" + value={teamName} + onChange={(e) => setTeamName(e.target.value)} + className="w-full p-2 border rounded" required />
-
- +
+ setPassword(e.target.value)} - className="input" + className="w-full p-2 border rounded" required />
- + -
- - Forgot Password? - -
) } diff --git a/frontend/src/pages/Register.jsx b/frontend/src/pages/Register.jsx index d6ccd90..ef53f82 100644 --- a/frontend/src/pages/Register.jsx +++ b/frontend/src/pages/Register.jsx @@ -2,38 +2,48 @@ import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { register } from '../api/auth'; import { useAuth } from '../hooks/useAuth'; -import { - validateEmail, - validatePassword, - validateUsername, -} from '../utils/validators'; +import { validateEmail } from '../utils/validators'; function Register() { - const [username, setUsername] = useState(''); - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); + const [formData, setFormData] = useState({ + teamName: '', + teamEmail: '', + password: '', + member1Name: '', + member1Email: '', + member2Name: '', + member2Email: '', + member3Name: '', + member3Email: '', + }); const [error, setError] = useState(''); const navigate = useNavigate(); const { login } = useAuth(); + const handleChange = (e) => { + setFormData({ + ...formData, + [e.target.name]: e.target.value + }); + }; + const handleSubmit = async (e) => { e.preventDefault(); + setError(''); - if (!validateUsername(username)) { - setError('Invalid username format'); - return; - } - if (!validateEmail(email)) { - setError('Invalid email format'); - return; - } - if (!validatePassword(password)) { - setError('Password must be at least 8 characters'); - return; - } + // Calculate team size based on provided member emails + const memberCount = [ + formData.member1Email, + formData.member2Email, + formData.member3Email + ].filter(Boolean).length; try { - const data = await register(username, email, password); + console.log(formData); + const data = await register({ + ...formData, + teamSize: memberCount // Add team size to registration data + }); login(data); navigate('/'); } catch (err) { @@ -43,51 +53,127 @@ function Register() { return (
-

Register

+

Team Registration

{error &&

{error}

}
+ {/* Team Information */}
- - setUsername(e.target.value)} - className="input" - required - /> +

Team Information

+
+ + +
+
+ + +
+
+ + +
+ + {/* Member 1 (Required) */}
- - setEmail(e.target.value)} - className="input" - required - /> +

Member 1 (Required)

+
+ + +
+
+ + +
+ + {/* Member 2 (Optional) */}
- - setPassword(e.target.value)} - className="input" - required - minLength={8} - /> +

Member 2 (Optional)

+
+ + +
+
+ + +
+ + {/* Member 3 (Optional) */} +
+

Member 3 (Optional)

+
+ + +
+
+ + +
+
+
diff --git a/frontend/src/pages/Scoreboard.jsx b/frontend/src/pages/Scoreboard.jsx index 81d50ba..26898cb 100644 --- a/frontend/src/pages/Scoreboard.jsx +++ b/frontend/src/pages/Scoreboard.jsx @@ -6,7 +6,6 @@ function Scoreboard() { const [scores, setScores] = useState([]) const [error, setError] = useState('') const [loading, setLoading] = useState(true) - const { user } = useAuth() useEffect(() => { fetchScoreboard() @@ -14,11 +13,12 @@ function Scoreboard() { const fetchScoreboard = async () => { try { - const scoreboard = await getScoreboard(user.token) + const scoreboard = await getScoreboard() setScores(scoreboard) - setLoading(false) } catch (err) { + console.error('Error fetching scoreboard:', err) setError('Failed to fetch scoreboard') + } finally { setLoading(false) } } diff --git a/frontend/src/pages/TeamProfile.jsx b/frontend/src/pages/TeamProfile.jsx new file mode 100644 index 0000000..bd9f836 --- /dev/null +++ b/frontend/src/pages/TeamProfile.jsx @@ -0,0 +1,76 @@ +import React from 'react'; +import { useAuth } from '../hooks/useAuth'; +import { jwtDecode } from 'jwt-decode'; + +function TeamProfile() { + const { user } = useAuth(); + + // Get additional data from token if needed + const getTokenData = () => { + try { + const tokenString = localStorage.getItem('token'); + if (tokenString) { + const { access } = JSON.parse(tokenString); + return jwtDecode(access); + } + } catch (error) { + console.error('Error decoding token:', error); + } + return null; + }; + + const tokenData = getTokenData(); + console.log('Token data in TeamProfile:', tokenData); + + return ( +
+

Team Profile

+ + {/* Team Basic Info */} +
+
+

Team Information

+
+
+ +

{tokenData?.team_name}

+
+
+ +

{tokenData?.team_email}

+
+
+
+ + {/* Team Members */} +
+

Team Members

+
+ {tokenData?.member_emails.map((email, index) => ( +
+

{email}

+
+ ))} +
+
+ + {/* Team Stats */} +
+

Team Information

+
+
+

Member Count

+

{tokenData?.member_count}

+
+
+

Team ID

+

#{tokenData?.team_id}

+
+
+
+
+
+ ); +} + +export default TeamProfile; \ No newline at end of file diff --git a/frontend/src/pages/admin/AdminChallenges.jsx b/frontend/src/pages/admin/AdminChallenges.jsx new file mode 100644 index 0000000..1f65155 --- /dev/null +++ b/frontend/src/pages/admin/AdminChallenges.jsx @@ -0,0 +1,116 @@ +import React, { useState, useEffect } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; +import { getAdminChallenges, deleteChallenge } from '../../api/challenges'; + +function AdminChallenges() { + const [challenges, setChallenges] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const navigate = useNavigate(); + + useEffect(() => { + fetchChallenges(); + }, []); + + const fetchChallenges = async () => { + try { + const data = await getAdminChallenges(); + setChallenges(data); + } catch (err) { + console.error('Error fetching challenges:', err); + setError('Failed to fetch challenges'); + } finally { + setLoading(false); + } + }; + + const handleDelete = async (id) => { + if (window.confirm('Are you sure you want to delete this challenge?')) { + try { + await deleteChallenge(id); + fetchChallenges(); // Refresh the list + } catch (err) { + console.error('Error deleting challenge:', err); + setError('Failed to delete challenge'); + } + } + }; + + if (loading) return
Loading...
; + + return ( +
+
+

Challenges

+ + Create Challenge + +
+ + {error && ( +
+ {error} +
+ )} + +
+ + + + + + + + + + + + {challenges.map((challenge) => ( + + + + + + + + ))} + +
TitleCategoryPointsStatusActions
+
{challenge.title}
+
+ + {challenge.category} + + + {challenge.max_points} + + + {challenge.is_hidden ? 'Hidden' : 'Visible'} + + + + +
+
+
+ ); +} + +export default AdminChallenges; \ No newline at end of file diff --git a/frontend/src/pages/admin/AdminLogin.jsx b/frontend/src/pages/admin/AdminLogin.jsx index d060331..0fc161b 100644 --- a/frontend/src/pages/admin/AdminLogin.jsx +++ b/frontend/src/pages/admin/AdminLogin.jsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { login as apiLogin } from '../../api/auth'; import { useAuth } from '../../hooks/useAuth'; +import { adminLogin } from '../../api/auth'; function AdminLogin() { const [email, setEmail] = useState(''); @@ -12,40 +12,41 @@ function AdminLogin() { const handleSubmit = async (e) => { e.preventDefault(); + setError(''); + try { - const data = await apiLogin(email, password); - if (!data.user?.isAdmin) { - setError('Unauthorized access'); - return; - } - login(data.token); - navigate('/admin/dashboard'); + const response = await adminLogin(email, password); + login(response); + navigate('/admin/challenges'); } catch (err) { - setError('Login failed. Please check your credentials.'); + console.error('Admin login error:', err); + setError(err.response?.data?.error || 'Failed to login'); } }; return ( -
-

Admin Login

- {error &&

{error}

} +
+

Admin Login

+ {error && ( +
+ {error} +
+ )}
- + setEmail(e.target.value)} className="w-full p-2 border rounded" required />
-
- +
+ setPassword(e.target.value)} className="w-full p-2 border rounded" @@ -53,10 +54,10 @@ function AdminLogin() { />
diff --git a/frontend/src/pages/admin/ChallengeDetail.jsx b/frontend/src/pages/admin/ChallengeDetail.jsx index 7aa8303..8bd8dc9 100644 --- a/frontend/src/pages/admin/ChallengeDetail.jsx +++ b/frontend/src/pages/admin/ChallengeDetail.jsx @@ -1,210 +1,253 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { useParams, useNavigate, Link } from 'react-router-dom'; -import { getChallengeById } from '../../data/dummyChallenges'; +import { getChallengeById, updateChallenge, deleteChallenge } from '../../api/challenges'; import { getUserById } from '../../data/dummyUsers'; import { dummyUsers } from '../../data/dummyUsers'; import { dummyTeams } from '../../data/dummyTeams'; function EditChallengeModal({ challenge, onClose, onSave }) { const [formData, setFormData] = useState({ - ...challenge, - newHint: { content: '', cost: 0 } + title: challenge.title, + description: challenge.description, + category: challenge.category, + max_points: challenge.max_points, + docker_image: challenge.docker_image, + flag: challenge.flag, + is_hidden: challenge.is_hidden, + hints: [...challenge.hints], + file_links: [...challenge.file_links] }); - const addHint = () => { - if (formData.newHint.content) { - setFormData({ - ...formData, - hints: [...formData.hints, { ...formData.newHint, id: Date.now() }], - newHint: { content: '', cost: 0 } - }); - } - }; - - const removeHint = (hintId) => { - setFormData({ - ...formData, - hints: formData.hints.filter(hint => hint.id !== hintId) - }); - }; + const categoryOptions = [ + 'web', + 'crypto', + 'pwn', + 'reverse', + 'forensics', + 'misc' + ]; - const handleSubmit = (e) => { + const handleSubmit = async (e) => { e.preventDefault(); - - // Remove the newHint field before saving - const { newHint, ...challengeData } = formData; - - // TODO: Validate the data before saving - // - Check if name is not empty - // - Check if value is a positive number - // - Check if flag is not empty - // - Validate hints have content and valid costs - - onSave(challengeData); + try { + await onSave(formData); + onClose(); + } catch (error) { + console.error('Error updating challenge:', error); + } }; return (
-

Edit Challenge

+
+

Edit Challenge

+ +
+
- + setFormData({...formData, title: e.target.value})} className="w-full border rounded-lg px-4 py-2" - value={formData.name} - onChange={(e) => setFormData({...formData, name: e.target.value})} + required />
+
+ + +
+