diff --git a/source/apps/ctf/admin.py b/source/apps/ctf/admin.py index 8c38f3f..5a74873 100644 --- a/source/apps/ctf/admin.py +++ b/source/apps/ctf/admin.py @@ -1,3 +1,17 @@ from django.contrib import admin -# Register your models here. +from source.apps.ctf import models + + +@admin.register(models.Team) +class TeamAdmin(admin.ModelAdmin): + list_display = ("id", "name") + +@admin.register(models.Flag) +class FlagAdmin(admin.ModelAdmin): + list_display = ("id", "name", "type") + + +@admin.register(models.Hint) +class HintAdmin(admin.ModelAdmin): + list_display = ("id", "flag") diff --git a/source/apps/ctf/apps.py b/source/apps/ctf/apps.py index 72da749..a24299d 100644 --- a/source/apps/ctf/apps.py +++ b/source/apps/ctf/apps.py @@ -3,4 +3,4 @@ from django.apps import AppConfig class CtfConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' - name = 'apps.ctf' + name = 'source.apps.ctf' diff --git a/source/apps/ctf/forms.py b/source/apps/ctf/forms.py new file mode 100644 index 0000000..b059663 --- /dev/null +++ b/source/apps/ctf/forms.py @@ -0,0 +1,9 @@ +from django import forms + + +class TeamForm(forms.Form): + name = forms.CharField(label="Team's name", max_length=32) + + +class FlagForm(forms.Form): + name = forms.UUIDField(label="Identifier", max_length=32) diff --git a/source/apps/ctf/models.py b/source/apps/ctf/models.py index 71a8362..9c0c276 100644 --- a/source/apps/ctf/models.py +++ b/source/apps/ctf/models.py @@ -1,3 +1,65 @@ +import uuid + from django.db import models -# Create your models here. + +class Team(models.Model): + """ + Represent a team participating in the event + """ + + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + name = models.CharField(max_length=32) + + validated_flags = models.ManyToManyField(to="Flag", blank=True, related_name="validated_by_teams") + + def __str__(self): + return f"<{self.__class__.__name__}({self.name!r}, {self.id!r})>" + + +class Flag(models.Model): + """ + Represent a flag that can be validated by the team + """ + + class FlagType(models.IntegerChoices): + NORMAL = 0 + BONUS = 1 + MALUS = 2 + + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + + # the flag might depend on other flags + parents = models.ManyToManyField( + "self", + blank=True, + symmetrical=False, + related_name="children", + ) + + # information about the flags + # TODO(Faraphel): store code as hash ? + code = models.CharField(max_length=32) # the password that must be given to obtain the flag + name = models.CharField(max_length=32) # the name of the flag + description = models.TextField() # the description on how to obtain the flag + + # TODO(Faraphel): replace with or add a points amount you gain ? + type = models.IntegerField(choices=FlagType.choices, default=FlagType.NORMAL) + + def __str__(self): + return f"<{self.__class__.__name__}({self.name!r}, {self.id!r})>" + + +class Hint(models.Model): + """ + Represent a hint that can be given to a team + """ + + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + importance = models.PositiveIntegerField() + description = models.TextField() + penalty = models.PositiveIntegerField() + flag = models.ForeignKey(to=Flag, on_delete=models.CASCADE, related_name="hints") + + def __str__(self): + return f"<{self.__class__.__name__}({self.flag.name!r}, {self.id!r})>" diff --git a/source/apps/ctf/templates/ctf/base/base.html b/source/apps/ctf/templates/ctf/base/base.html index 35ddc74..8ebfeb3 100644 --- a/source/apps/ctf/templates/ctf/base/base.html +++ b/source/apps/ctf/templates/ctf/base/base.html @@ -2,6 +2,7 @@ <html lang="en"> <head> <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{% block title %}{% endblock %}</title> {% block head %} {% endblock %} diff --git a/source/apps/ctf/templates/ctf/flag_list.html b/source/apps/ctf/templates/ctf/flag_list.html new file mode 100644 index 0000000..8704fe7 --- /dev/null +++ b/source/apps/ctf/templates/ctf/flag_list.html @@ -0,0 +1,105 @@ +{% extends "ctf/base/base.html" %} + +{% block title %}Flags{% endblock %} + +{% block head %} + <script src="https://unpkg.com/@popperjs/core@2"></script> + <script src="https://unpkg.com/tippy.js@6"></script> + <script src="https://cytoscape.org/cytoscape.js-popper/cytoscape-popper.js"></script> + <script src="https://unpkg.com/cytoscape/dist/cytoscape.min.js"></script> + + <style> + #cy { + width: 100vw; + height: 100vh; + } + </style> +{% endblock %} + +{% block body %} + <div id="cy"></div> + <script> + function tippyFactory(reference, content) { + // create a basic element for the tips object + const element = document.createElement('div'); + + // build the tip inside + return tippy(element, { + getReferenceClientRect: reference.getBoundingClientRect, + trigger: "manual", + content: content, + + arrow: true, + placement: "bottom", + hideOnClick: false, + sticky: "reference", + + interactive: true, + appendTo: cy.container() + }); + } + // use the factory when creating tips + cytoscape.use(cytoscapePopper(tippyFactory)); + + // create a graph based on our data + const cy = cytoscape({ + container: document.getElementById('cy'), + // content + elements: [ + // nodes + {% for flag in flags %} + {data: { + id: "{{ flag.id }}", + name: "{{ flag.name }}", + description: "{{ flag.description }}", + teams: [ + {% for team in flag.validated_by_teams.all %} + "{{ team.name }}", + {% endfor %} + ], + }}, + {% endfor %} + + // links + {% for flag in flags %} + {% for child_flag in flag.children.all %} + {data: {source: "{{ child_flag.id }}", target: "{{ flag.id }}"}}, + {% endfor %} + {% endfor %} + ], + // style + style: [ + {selector: 'node', style: {'label': 'data(name)'}}, + {selector: 'edge', style: {'target-arrow-shape': 'triangle'}}, + ], + // layout + layout: { + name: 'breadthfirst', + directed: true + } + }); + + // show the description of the nodes on hover + cy.ready(() => { + cy.nodes().forEach(node => { + // build the tip + const tippy = node.popper({ + content: () => { + const div = document.createElement('div'); + div.innerHTML = node.data("description") + "<br/>---<br/>" + node.data("teams"); + return div; + }, + trigger: 'manual', + interactive: true, + appendTo: cy.container() + }); + + // events + node.on('mouseover', () => tippy.show()); + node.on('mouseout', () => tippy.hide()); + }); + }); + </script> +{% endblock %} + + diff --git a/source/apps/ctf/templates/ctf/flag_submit.html b/source/apps/ctf/templates/ctf/flag_submit.html new file mode 100644 index 0000000..ff4e8f0 --- /dev/null +++ b/source/apps/ctf/templates/ctf/flag_submit.html @@ -0,0 +1,11 @@ +{% extends "ctf/base/base.html" %} + +{% block title %}Flag - Submit{% endblock %} + +{% block body %} + <form method="POST"> + {% csrf_token %} + {{ form }} + <input type="submit"> + </form> +{% endblock %} \ No newline at end of file diff --git a/source/apps/ctf/templates/ctf/team_create.html b/source/apps/ctf/templates/ctf/team_create.html new file mode 100644 index 0000000..67d9789 --- /dev/null +++ b/source/apps/ctf/templates/ctf/team_create.html @@ -0,0 +1,11 @@ +{% extends "ctf/base/base.html" %} + +{% block title %}Team - Create{% endblock %} + +{% block body %} + <form method="POST"> + {% csrf_token %} + {{ form }} + <input type="submit"> + </form> +{% endblock %} diff --git a/source/apps/ctf/templates/ctf/team_list.html b/source/apps/ctf/templates/ctf/team_list.html new file mode 100644 index 0000000..c03f003 --- /dev/null +++ b/source/apps/ctf/templates/ctf/team_list.html @@ -0,0 +1,13 @@ +{% extends "ctf/base/base.html" %} + +{% block title %}Teams{% endblock %} + +{% block body %} + <ul> + {% for team in teams %} + <li>{{ team.name }}</li> + {% endfor %} + </ul> + + <a href="{% url "team_create" %}">Create</a> +{% endblock %} diff --git a/source/apps/ctf/urls.py b/source/apps/ctf/urls.py index 3a7d2a0..e7e276a 100644 --- a/source/apps/ctf/urls.py +++ b/source/apps/ctf/urls.py @@ -3,5 +3,9 @@ from django.urls import path from source.apps.ctf import views urlpatterns = [ - path("", views.homepage, name='homepage'), + path("", views.view_homepage, name='homepage'), + path("teams", views.view_teams, name='teams'), + path("teams/create", views.view_team_create, name='team_create'), + path("flags", views.view_flags, name='flags'), + path("flags/submit", views.view_flag_submit, name='flag_submit'), ] diff --git a/source/apps/ctf/views.py b/source/apps/ctf/views.py index 10888f3..b4971f5 100644 --- a/source/apps/ctf/views.py +++ b/source/apps/ctf/views.py @@ -1,5 +1,43 @@ -from django.shortcuts import render +from django.shortcuts import render, redirect + +from source.apps.ctf import forms, models + # Create your views here. -async def homepage(request): +async def view_homepage(request): return render(request, "ctf/homepage.html") + + +async def view_teams(request): + teams = [team async for team in models.Team.objects.all()] + return render(request, "ctf/team_list.html", context={"teams": teams}) + + +async def view_team_create(request): + if request.method == "POST": + form = forms.TeamForm(request.POST) + if form.is_valid(): + # TODO(Faraphel): additional password to prevent unwanted team creation ? + await models.Team.objects.acreate(name=form.cleaned_data["name"]) + return redirect("/") + + else: + form = forms.TeamForm() + + return render(request, "ctf/team_create.html", context={"form": form}) + + +def view_flags(request): + flags = models.Flag.objects.all() + return render(request, "ctf/flag_list.html", context={"flags": flags}) + + +async def view_flag_submit(request): + if request.method == "POST": + form = forms.FlagForm(request.POST) + if form.is_valid(): + return redirect("/") + else: + form = forms.FlagForm() + + return render(request, "ctf/flag_submit.html", context={"form": form})