├── .gitignore ├── LICENSE.md ├── README.md ├── admin ├── .gitignore ├── Dockerfile ├── ctf │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── tests.py │ └── views.py ├── ctfplatformadmin │ ├── __init__.py │ ├── settings │ │ ├── base.py │ │ ├── dev.py │ │ ├── production.py │ │ └── static.py │ ├── urls.py │ └── wsgi.py ├── manage.py ├── requirements.txt └── run.sh ├── data └── .gitkeep ├── db ├── Dockerfile └── init.sql ├── docker-compose-local.yml ├── frontend ├── .gitignore ├── .nvmrc ├── .stylelintrc ├── README.md ├── package.json ├── public │ ├── favicons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ └── site.webmanifest │ └── index.html ├── src │ ├── assets │ │ ├── fonts │ │ │ ├── Downtown.otf │ │ │ └── Downtown.ttf │ │ └── images │ │ │ ├── avatar_default.png │ │ │ ├── avatar_default_big.png │ │ │ ├── blue_graphic_left.svg │ │ │ ├── blue_graphic_left_gora.svg │ │ │ ├── blue_graphic_right.svg │ │ │ ├── blue_graphic_right_gora.svg │ │ │ ├── check_icon_zadanie.svg │ │ │ ├── checkbox_icon_black_big.svg │ │ │ ├── checkbox_icon_green.svg │ │ │ ├── email_icon.svg │ │ │ ├── expand_icon.png │ │ │ ├── first_blood_icon.svg │ │ │ ├── flag_icon.svg │ │ │ ├── full_background_LEFT.svg │ │ │ ├── full_background_RIGHT.svg │ │ │ ├── icon_checkbox.svg │ │ │ ├── loader_logo.png │ │ │ ├── logo.svg │ │ │ ├── medal_icon.svg │ │ │ ├── no_avatar_scoreboard.png │ │ │ ├── no_country.png │ │ │ ├── rwd_icon.png │ │ │ ├── scoreboard_medal_1.png │ │ │ ├── scoreboard_medal_2.png │ │ │ ├── scoreboard_medal_3.png │ │ │ ├── scoreboard_medal_any.png │ │ │ ├── star_icon.svg │ │ │ ├── trail_of_bits_logo.png │ │ │ ├── twitter_icon.svg │ │ │ └── wp_logo.png │ ├── components │ │ ├── AnnouncementElement.tsx │ │ ├── Challenge.tsx │ │ ├── ChallengeModal.tsx │ │ ├── Container.tsx │ │ ├── FlagSubmit.tsx │ │ ├── Footer.tsx │ │ ├── Link.tsx │ │ ├── Loader.tsx │ │ ├── Modal.tsx │ │ ├── Navbar.tsx │ │ ├── RemovableMessage.tsx │ │ └── Timer.tsx │ ├── consts │ │ ├── country.ts │ │ └── index.ts │ ├── index.tsx │ ├── libs │ │ ├── api.ts │ │ ├── date.ts │ │ ├── http.ts │ │ └── types.ts │ ├── pages │ │ ├── Announcements.tsx │ │ ├── Challenges.tsx │ │ ├── Home.tsx │ │ ├── Login.tsx │ │ ├── NotFound.tsx │ │ ├── Register.tsx │ │ ├── Rules.tsx │ │ ├── Scoreboard.tsx │ │ ├── Settings.tsx │ │ ├── Team.tsx │ │ ├── Teams.tsx │ │ ├── UnavailablePage.tsx │ │ └── index.tsx │ ├── store │ │ ├── CtfStore.ts │ │ └── index.ts │ ├── styles │ │ ├── announcements.scss │ │ ├── challenges.scss │ │ ├── homepage.scss │ │ ├── login.scss │ │ ├── main.scss │ │ ├── modal.scss │ │ ├── navbar.scss │ │ ├── rules.scss │ │ ├── scoreboard.scss │ │ ├── settings.scss │ │ ├── team.scss │ │ ├── teams.scss │ │ └── timer.scss │ └── types │ │ ├── global.d.ts │ │ └── import-png.d.ts ├── tsconfig.json ├── tslint.json ├── webpack.config.js │ └── index.js └── yarn.lock ├── nginx ├── Dockerfile ├── admin.conf └── web.conf └── web ├── Dockerfile ├── actions ├── audit.go └── country.go ├── cmd └── main │ └── main.go ├── config └── config.go ├── db └── db.go ├── go.mod ├── go.sum ├── log └── log.go ├── models ├── audit.go ├── task.go └── team.go ├── rand └── rand.go ├── sentry └── hook.go └── session └── session.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 justCatTheFish CTF team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Ctfplatform 2 | Site: 2019.justctf.team 3 | 4 | #### Requirements 5 | - [docker](https://www.docker.com/) 6 | - [docker-compose](https://docs.docker.com/compose/) 7 | 8 | #### Run 9 | `docker-compose -f docker-compose-local.yml up` 10 | 11 | #### Usage 12 | Main page: `http://localhost:8081` 13 | Admin: `http://localhost:8082` 14 | 15 | #### Author 16 | Created by [cypis](https://github.com/patryk4815). 17 | -------------------------------------------------------------------------------- /admin/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.pyc 3 | __pycache__/ 4 | -------------------------------------------------------------------------------- /admin/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8.0-buster 2 | 3 | COPY requirements.txt /tmp/ 4 | RUN pip install --no-cache-dir -r /tmp/requirements.txt 5 | 6 | WORKDIR /code 7 | COPY . . 8 | 9 | RUN chmod +x run.sh 10 | CMD ["./run.sh"] 11 | -------------------------------------------------------------------------------- /admin/ctf/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justcatthefish/ctfplatform/015c62d270ab834475aaa186f769be4628f1ecc1/admin/ctf/__init__.py -------------------------------------------------------------------------------- /admin/ctf/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Announcement, Audit, Task, TaskFlags, Team 3 | 4 | 5 | @admin.register(Announcement) 6 | class AnnouncementAdmin(admin.ModelAdmin): 7 | list_display = ('id', 'title', 'description', 'created_at') 8 | 9 | 10 | @admin.register(Audit) 11 | class AuditAdmin(admin.ModelAdmin): 12 | list_display = ('id', 'task', 'team', 'created_at') 13 | 14 | 15 | @admin.register(Task) 16 | class TaskAdmin(admin.ModelAdmin): 17 | list_display = ('id', 'name', 'category', 'difficult', 'started_at', 'created_at') 18 | 19 | 20 | @admin.register(TaskFlags) 21 | class TaskFlagsAdmin(admin.ModelAdmin): 22 | list_display = ('id', 'task', 'flag') 23 | 24 | 25 | @admin.register(Team) 26 | class TeamAdmin(admin.ModelAdmin): 27 | list_display = ('id', 'name', 'email', 'active', 'country', 'created_at') 28 | -------------------------------------------------------------------------------- /admin/ctf/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CtfConfig(AppConfig): 5 | name = 'ctf' 6 | -------------------------------------------------------------------------------- /admin/ctf/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0 on 2019-12-06 15:39 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Announcement', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('title', models.CharField(max_length=255)), 19 | ('description', models.TextField()), 20 | ('created_at', models.DateTimeField(auto_now_add=True)), 21 | ], 22 | options={ 23 | 'db_table': 'announcement', 24 | 'managed': False, 25 | }, 26 | ), 27 | migrations.CreateModel( 28 | name='Audit', 29 | fields=[ 30 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 31 | ('created_at', models.DateTimeField(auto_now_add=True)), 32 | ], 33 | options={ 34 | 'db_table': 'audit', 35 | 'managed': False, 36 | }, 37 | ), 38 | migrations.CreateModel( 39 | name='Task', 40 | fields=[ 41 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 42 | ('name', models.CharField(max_length=255)), 43 | ('description', models.TextField()), 44 | ('category', models.CharField(max_length=128)), 45 | ('difficult', models.CharField(max_length=32)), 46 | ('active', models.BooleanField(default=False)), 47 | ('created_at', models.DateTimeField(auto_now_add=True)), 48 | ], 49 | options={ 50 | 'db_table': 'task', 51 | 'managed': False, 52 | }, 53 | ), 54 | migrations.CreateModel( 55 | name='TaskFlags', 56 | fields=[ 57 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 58 | ('flag', models.CharField(max_length=255, unique=True)), 59 | ], 60 | options={ 61 | 'db_table': 'task_flags', 62 | 'managed': False, 63 | }, 64 | ), 65 | migrations.CreateModel( 66 | name='Team', 67 | fields=[ 68 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 69 | ('name', models.CharField(max_length=255, unique=True)), 70 | ('email', models.CharField(max_length=255, unique=True)), 71 | ('password', models.CharField(max_length=255)), 72 | ('active', models.BooleanField(default=False)), 73 | ('created_at', models.DateTimeField(auto_now_add=True)), 74 | ('country', models.CharField(max_length=4)), 75 | ('avatar', models.CharField(max_length=64)), 76 | ], 77 | options={ 78 | 'db_table': 'team', 79 | 'managed': False, 80 | }, 81 | ), 82 | ] 83 | -------------------------------------------------------------------------------- /admin/ctf/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justcatthefish/ctfplatform/015c62d270ab834475aaa186f769be4628f1ecc1/admin/ctf/migrations/__init__.py -------------------------------------------------------------------------------- /admin/ctf/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Announcement(models.Model): 5 | title = models.CharField(max_length=255, null=False, blank=False) 6 | description = models.TextField(null=False, blank=False) 7 | created_at = models.DateTimeField(auto_now_add=True, null=False) 8 | 9 | def __str__(self): 10 | return f'{self.title} (#{self.id})' 11 | 12 | class Meta: 13 | db_table = 'announcement' 14 | managed = False 15 | 16 | 17 | class Task(models.Model): 18 | name = models.CharField(max_length=255, null=False) 19 | description = models.TextField(null=False) 20 | category = models.CharField(max_length=128, null=False) 21 | difficult = models.CharField(max_length=32, null=False) 22 | started_at = models.DateTimeField(null=True, default=None, blank=True) 23 | created_at = models.DateTimeField(auto_now_add=True, null=False) 24 | 25 | def __str__(self): 26 | return f'{self.name} (#{self.id})' 27 | 28 | class Meta: 29 | db_table = 'task' 30 | managed = False 31 | 32 | 33 | class TaskFlags(models.Model): 34 | task = models.ForeignKey('Task', on_delete=models.DO_NOTHING, null=False, related_name='+') 35 | flag = models.CharField(max_length=255, unique=True, null=False) 36 | 37 | class Meta: 38 | db_table = 'task_flags' 39 | managed = False 40 | 41 | 42 | class Team(models.Model): 43 | name = models.CharField(max_length=255, unique=True, null=False, blank=False) 44 | email = models.CharField(max_length=255, unique=True, null=False, blank=False) 45 | password = models.CharField(max_length=255, null=False) 46 | active = models.BooleanField(default=False) 47 | created_at = models.DateTimeField(auto_now_add=True, null=False) 48 | 49 | country = models.CharField(max_length=4, null=False, blank=True) 50 | avatar = models.CharField(max_length=64, null=False, blank=True) 51 | affiliation = models.CharField(max_length=64, null=False, blank=True) 52 | website = models.CharField(max_length=255, null=False, blank=True) 53 | 54 | def __str__(self): 55 | return f'{self.name} (#{self.id})' 56 | 57 | class Meta: 58 | db_table = 'team' 59 | managed = False 60 | 61 | 62 | class TeamAvatar(models.Model): 63 | team_id = models.ForeignKey('Team', unique=True, on_delete=models.CASCADE, null=False, related_name='+') 64 | avatar_path = models.CharField(max_length=64, null=False) 65 | avatar = models.BinaryField(null=False) 66 | 67 | 68 | class Audit(models.Model): 69 | task = models.ForeignKey('Task', on_delete=models.DO_NOTHING, null=False, related_name='+') 70 | team = models.ForeignKey('Team', on_delete=models.DO_NOTHING, null=False, related_name='+') 71 | created_at = models.DateTimeField(auto_now_add=True, null=False) 72 | 73 | class Meta: 74 | db_table = 'audit' 75 | unique_together = (('task', 'team'),) 76 | managed = False 77 | -------------------------------------------------------------------------------- /admin/ctf/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /admin/ctf/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /admin/ctfplatformadmin/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justcatthefish/ctfplatform/015c62d270ab834475aaa186f769be4628f1ecc1/admin/ctfplatformadmin/__init__.py -------------------------------------------------------------------------------- /admin/ctfplatformadmin/settings/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for ctfplatformadmin project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.11. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.11/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | def get_env(key, default=None): 20 | try: 21 | return os.environ[key] 22 | except KeyError: 23 | if callable(default): 24 | return default() 25 | return default 26 | 27 | 28 | # Quick-start development settings - unsuitable for production 29 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ 30 | 31 | # SECURITY WARNING: keep the secret key used in production secret! 32 | SECRET_KEY = get_env('SECRET_KEY') 33 | 34 | # SECURITY WARNING: don't run with debug turned on in production! 35 | DEBUG = True 36 | 37 | ALLOWED_HOSTS = [] 38 | 39 | 40 | # Application definition 41 | 42 | INSTALLED_APPS = [ 43 | 'django.contrib.admin', 44 | 'django.contrib.auth', 45 | 'django.contrib.contenttypes', 46 | 'django.contrib.sessions', 47 | 'django.contrib.messages', 48 | 'django.contrib.staticfiles', 49 | 'ctf', 50 | ] 51 | 52 | MIDDLEWARE = [ 53 | 'django.middleware.security.SecurityMiddleware', 54 | 'django.contrib.sessions.middleware.SessionMiddleware', 55 | 'django.middleware.common.CommonMiddleware', 56 | 'django.middleware.csrf.CsrfViewMiddleware', 57 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 58 | 'django.contrib.messages.middleware.MessageMiddleware', 59 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 60 | ] 61 | 62 | ROOT_URLCONF = 'ctfplatformadmin.urls' 63 | 64 | TEMPLATES = [ 65 | { 66 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 67 | 'DIRS': [], 68 | 'APP_DIRS': True, 69 | 'OPTIONS': { 70 | 'context_processors': [ 71 | 'django.template.context_processors.debug', 72 | 'django.template.context_processors.request', 73 | 'django.contrib.auth.context_processors.auth', 74 | 'django.contrib.messages.context_processors.messages', 75 | ], 76 | }, 77 | }, 78 | ] 79 | 80 | WSGI_APPLICATION = 'ctfplatformadmin.wsgi.application' 81 | 82 | 83 | # Database 84 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases 85 | 86 | DATABASES = { 87 | 'default': { 88 | 'ENGINE': 'django.db.backends.mysql', 89 | 'NAME': get_env('DATABASE_DB'), 90 | 'USER': get_env('DATABASE_USER'), 91 | 'PASSWORD': get_env('DATABASE_PASSWORD'), 92 | 'HOST': get_env('DATABASE_HOST'), 93 | 'PORT': get_env('DATABASE_PORT', 3306), 94 | } 95 | } 96 | 97 | 98 | # Password validation 99 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators 100 | 101 | AUTH_PASSWORD_VALIDATORS = [ 102 | { 103 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 104 | }, 105 | { 106 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 107 | }, 108 | { 109 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 110 | }, 111 | { 112 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 113 | }, 114 | ] 115 | 116 | 117 | # Internationalization 118 | # https://docs.djangoproject.com/en/1.11/topics/i18n/ 119 | 120 | LANGUAGE_CODE = 'en-us' 121 | 122 | TIME_ZONE = 'UTC' 123 | 124 | USE_I18N = True 125 | 126 | USE_L10N = True 127 | 128 | USE_TZ = True 129 | 130 | 131 | # Static files (CSS, JavaScript, Images) 132 | # https://docs.djangoproject.com/en/1.11/howto/static-files/ 133 | 134 | STATIC_URL = '/static/' 135 | STATIC_ROOT = os.path.join(BASE_DIR, '..', 'static') 136 | -------------------------------------------------------------------------------- /admin/ctfplatformadmin/settings/dev.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from .base import * 3 | 4 | DEBUG = True 5 | ALLOWED_HOSTS = ["*"] 6 | 7 | LOGGING = { 8 | 'version': 1, 9 | 'disable_existing_loggers': False, 10 | 'formatters': { 11 | 'verbose': {'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s'}, 12 | 'standard': {'format': '%(asctime)s [%(levelname)s] %(name)s: %(message)s'}, 13 | }, 14 | 'handlers': {'console': {'class': 'logging.StreamHandler', 'stream': sys.stdout, 'formatter': 'verbose'}, 'null': {'class': 'logging.NullHandler'}}, 15 | 'loggers': { 16 | 'django': {'handlers': ['console'], 'level': 'DEBUG', 'propagate': False}, 17 | 'django.request': {'handlers': ['console'], 'level': 'DEBUG', 'propagate': False}, 18 | 'django.template': {'handlers': ['console'], 'level': 'INFO', 'propagate': False}, 19 | }, 20 | 'root': {'handlers': ['console'], 'level': 'DEBUG'}, 21 | } 22 | -------------------------------------------------------------------------------- /admin/ctfplatformadmin/settings/production.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | 3 | DEBUG = False 4 | ALLOWED_HOSTS = ["*"] # yolo 5 | -------------------------------------------------------------------------------- /admin/ctfplatformadmin/settings/static.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from .base import * 3 | import os 4 | 5 | DEBUG = True 6 | 7 | DATABASES = {'default': {'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(BASE_DIR, 'db.sqlite')}} 8 | 9 | LOGGING = { 10 | 'version': 1, 11 | 'disable_existing_loggers': False, 12 | 'formatters': { 13 | 'verbose': {'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s'}, 14 | 'standard': {'format': '%(asctime)s [%(levelname)s] %(name)s: %(message)s'}, 15 | }, 16 | 'handlers': {'console': {'class': 'logging.StreamHandler', 'stream': sys.stdout, 'formatter': 'verbose'}, 'null': {'class': 'logging.NullHandler'}}, 17 | 'loggers': {'django': {'handlers': ['console'], 'level': 'DEBUG', 'propagate': False}, 'django.request': {'handlers': ['console'], 'level': 'DEBUG', 'propagate': False}}, 18 | 'root': {'handlers': ['console'], 'level': 'DEBUG'}, 19 | } 20 | -------------------------------------------------------------------------------- /admin/ctfplatformadmin/urls.py: -------------------------------------------------------------------------------- 1 | """ctfplatformadmin URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.11/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf import settings 17 | from django.conf.urls import url 18 | from django.conf.urls.static import static 19 | from django.contrib import admin 20 | 21 | 22 | admin.site.site_header = 'justCTF Admin' 23 | admin.site.site_title = 'justCTF Admin' 24 | admin.site.index_title = 'Main' 25 | 26 | urlpatterns = [ 27 | url(r'^admin/', admin.site.urls), 28 | ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 29 | -------------------------------------------------------------------------------- /admin/ctfplatformadmin/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for ctfplatformadmin project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ctfplatformadmin.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /admin/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ctfplatformadmin.settings.static") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /admin/requirements.txt: -------------------------------------------------------------------------------- 1 | Django==3.0.7 2 | gunicorn==20.0.4 3 | requests==2.22.0 4 | mysqlclient==1.4.6 5 | -------------------------------------------------------------------------------- /admin/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #exec python manage.py runserver 0.0.0.0:${APP_PORT} 4 | 5 | python manage.py collectstatic --no-input # TODO: rm 6 | 7 | exec gunicorn ctfplatformadmin.wsgi:application \ 8 | --name ctfplatformadmin \ 9 | --bind 0.0.0.0:${APP_PORT} \ 10 | --workers ${GUNICORN_WORKERS} \ 11 | --timeout ${GUNICORN_TIMEOUT} \ 12 | --log-level=info \ 13 | --limit-request-line=0 \ 14 | --limit-request-field_size 32380 \ 15 | --limit-request-fields 300 \ 16 | --log-file=- 17 | -------------------------------------------------------------------------------- /data/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justcatthefish/ctfplatform/015c62d270ab834475aaa186f769be4628f1ecc1/data/.gitkeep -------------------------------------------------------------------------------- /db/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mysql:8.0.18 2 | 3 | ADD init.sql /docker-entrypoint-initdb.d/a.sql 4 | # /my/custom:/etc/mysql/conf.d 5 | #/my/own/datadir:/var/lib/mysql -------------------------------------------------------------------------------- /docker-compose-local.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | nginx: 5 | build: 6 | context: . 7 | dockerfile: nginx/Dockerfile 8 | restart: unless-stopped 9 | # volumes: 10 | # - ./frontend/dist:/frontend 11 | links: 12 | - web 13 | ports: 14 | - 8081:81 15 | - 8082:82 16 | logging: 17 | driver: "json-file" 18 | options: 19 | max-size: "30m" 20 | max-file: "5" 21 | 22 | db: 23 | build: db 24 | restart: unless-stopped 25 | volumes: 26 | - ./data/db/mysql:/var/lib/mysql 27 | command: mysqld --default-authentication-plugin=mysql_native_password --skip-mysqlx 28 | environment: 29 | - MYSQL_ROOT_PASSWORD=rootctfplatform 30 | - MYSQL_DATABASE=ctfplatform 31 | - MYSQL_USER=ctfplatform 32 | - MYSQL_PASSWORD=ctfplatform 33 | expose: 34 | - 3306 35 | logging: 36 | driver: "json-file" 37 | options: 38 | max-size: "30m" 39 | max-file: "5" 40 | 41 | web: 42 | build: web 43 | restart: unless-stopped 44 | environment: 45 | - MYSQL_DSN=ctfplatform:ctfplatform@tcp(db:3306)/ctfplatform?charset=utf8&loc=UTC&time_zone=%27%2B00%3A00%27&parseTime=true&readTimeout=3s&timeout=3s&writeTimeout=3s 46 | - HMAC_SECRET_KEY=32 chars secret here 47 | - AES_SECRET_KEY=32 chars secret here 48 | - CAPTCHA_SECRET=google recaptcha v3 secret token here 49 | healthcheck: 50 | test: ["CMD", "curl", "-f", "http://127.0.0.1:8080/api/v1/healthcheck"] 51 | interval: 10s 52 | timeout: 5s 53 | retries: 3 54 | links: 55 | - db 56 | expose: 57 | - 8080 58 | logging: 59 | driver: "json-file" 60 | options: 61 | max-size: "30m" 62 | max-file: "5" 63 | 64 | admin: 65 | build: admin 66 | restart: unless-stopped 67 | environment: 68 | - DJANGO_SETTINGS_MODULE=ctfplatformadmin.settings.dev 69 | - GUNICORN_WORKERS=4 70 | - GUNICORN_TIMEOUT=30 71 | - APP_PORT=8080 72 | - DATABASE_DB=ctfplatform 73 | - DATABASE_USER=ctfplatform 74 | - DATABASE_PASSWORD=ctfplatform 75 | - DATABASE_HOST=db 76 | - DATABASE_PORT=3306 77 | - SECRET_KEY=django secret here 78 | links: 79 | - db 80 | logging: 81 | driver: "json-file" 82 | options: 83 | max-size: "30m" 84 | max-file: "5" 85 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | yarn-error.log 4 | -------------------------------------------------------------------------------- /frontend/.nvmrc: -------------------------------------------------------------------------------- 1 | 13.2.0 -------------------------------------------------------------------------------- /frontend/.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "processors": [], 3 | "extends": [ 4 | "stylelint-config-recommended", 5 | ] 6 | } -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | 2 | ### Building 3 | ``` 4 | npm run build:dev 5 | npm run build:prod 6 | npm run watch 7 | ``` 8 | 9 | ### Linter 10 | ``` 11 | npm run lint 12 | ``` 13 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ctfplatform-front", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start:dev": "webpack-dev-server --mode=development", 8 | "watch": "webpack --mode=development --watch", 9 | "build:prod": "webpack --mode=production", 10 | "build:dev": "webpack --mode=development", 11 | "lint": "npm run lint:ts && npm run lint:css && npm run ts:check", 12 | "lint:ts": "tslint 'src/**/*{.ts,.tsx}'", 13 | "lint:css": "stylelint './src/**/*{.ts,.tsx}'", 14 | "lint:autofix": "tslint --fix 'src/**/*{.ts,.tsx}'", 15 | "ts:check": "tsc --noEmit" 16 | }, 17 | "dependencies": { 18 | "mobx": "^5.9.0", 19 | "mobx-react": "^5.4.3", 20 | "mobx-react-router": "^4.0.7", 21 | "mobx-state-tree": "^3.10.2", 22 | "react": "^16.7.0", 23 | "react-dom": "^16.7.0", 24 | "react-markdown": "^4.2.2", 25 | "react-recaptcha": "^2.3.10", 26 | "react-router": "^5.1.2" 27 | }, 28 | "devDependencies": { 29 | "@types/react": "^16.7.20", 30 | "@types/react-dom": "^16.0.11", 31 | "@types/react-recaptcha": "^2.3.3", 32 | "@types/react-router": "^5.1.3", 33 | "copy-webpack-plugin": "^5.1.1", 34 | "css-loader": "^3.3.0", 35 | "escape-string-regexp": "^2.0.0", 36 | "file-loader": "^5.0.2", 37 | "html-webpack-plugin": "^3.2.0", 38 | "mini-css-extract-plugin": "^0.8.0", 39 | "node-sass": "^4.13.0", 40 | "sass-loader": "^8.0.0", 41 | "source-map-loader": "^0.2.4", 42 | "stylelint": "^9.10.1", 43 | "stylelint-config-recommended": "^2.1.0", 44 | "ts-loader": "^6.2.1", 45 | "tsconfig-paths-webpack-plugin": "^3.2.0", 46 | "tslint": "^5.12.1", 47 | "typescript": "^3.7.3", 48 | "webpack": "^4.29.0", 49 | "webpack-cli": "^3.2.1", 50 | "webpack-dev-server": "^3.9.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /frontend/public/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justcatthefish/ctfplatform/015c62d270ab834475aaa186f769be4628f1ecc1/frontend/public/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /frontend/public/favicons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justcatthefish/ctfplatform/015c62d270ab834475aaa186f769be4628f1ecc1/frontend/public/favicons/android-chrome-512x512.png -------------------------------------------------------------------------------- /frontend/public/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justcatthefish/ctfplatform/015c62d270ab834475aaa186f769be4628f1ecc1/frontend/public/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /frontend/public/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justcatthefish/ctfplatform/015c62d270ab834475aaa186f769be4628f1ecc1/frontend/public/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /frontend/public/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justcatthefish/ctfplatform/015c62d270ab834475aaa186f769be4628f1ecc1/frontend/public/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /frontend/public/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justcatthefish/ctfplatform/015c62d270ab834475aaa186f769be4628f1ecc1/frontend/public/favicons/favicon.ico -------------------------------------------------------------------------------- /frontend/public/favicons/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | justCTF 2019 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /frontend/src/assets/fonts/Downtown.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justcatthefish/ctfplatform/015c62d270ab834475aaa186f769be4628f1ecc1/frontend/src/assets/fonts/Downtown.otf -------------------------------------------------------------------------------- /frontend/src/assets/fonts/Downtown.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justcatthefish/ctfplatform/015c62d270ab834475aaa186f769be4628f1ecc1/frontend/src/assets/fonts/Downtown.ttf -------------------------------------------------------------------------------- /frontend/src/assets/images/avatar_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justcatthefish/ctfplatform/015c62d270ab834475aaa186f769be4628f1ecc1/frontend/src/assets/images/avatar_default.png -------------------------------------------------------------------------------- /frontend/src/assets/images/avatar_default_big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justcatthefish/ctfplatform/015c62d270ab834475aaa186f769be4628f1ecc1/frontend/src/assets/images/avatar_default_big.png -------------------------------------------------------------------------------- /frontend/src/assets/images/check_icon_zadanie.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Check_icon_zadanie 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /frontend/src/assets/images/checkbox_icon_black_big.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | checkbox_icon_black_big 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /frontend/src/assets/images/checkbox_icon_green.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | checkbox_icon_green 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /frontend/src/assets/images/email_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | email_icon 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /frontend/src/assets/images/expand_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justcatthefish/ctfplatform/015c62d270ab834475aaa186f769be4628f1ecc1/frontend/src/assets/images/expand_icon.png -------------------------------------------------------------------------------- /frontend/src/assets/images/first_blood_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | first_blood_icon 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 1 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /frontend/src/assets/images/flag_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | flag_icon 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /frontend/src/assets/images/icon_checkbox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Check_icon_checkbox 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /frontend/src/assets/images/loader_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justcatthefish/ctfplatform/015c62d270ab834475aaa186f769be4628f1ecc1/frontend/src/assets/images/loader_logo.png -------------------------------------------------------------------------------- /frontend/src/assets/images/medal_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | medal_icon 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /frontend/src/assets/images/no_avatar_scoreboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justcatthefish/ctfplatform/015c62d270ab834475aaa186f769be4628f1ecc1/frontend/src/assets/images/no_avatar_scoreboard.png -------------------------------------------------------------------------------- /frontend/src/assets/images/no_country.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justcatthefish/ctfplatform/015c62d270ab834475aaa186f769be4628f1ecc1/frontend/src/assets/images/no_country.png -------------------------------------------------------------------------------- /frontend/src/assets/images/rwd_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justcatthefish/ctfplatform/015c62d270ab834475aaa186f769be4628f1ecc1/frontend/src/assets/images/rwd_icon.png -------------------------------------------------------------------------------- /frontend/src/assets/images/scoreboard_medal_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justcatthefish/ctfplatform/015c62d270ab834475aaa186f769be4628f1ecc1/frontend/src/assets/images/scoreboard_medal_1.png -------------------------------------------------------------------------------- /frontend/src/assets/images/scoreboard_medal_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justcatthefish/ctfplatform/015c62d270ab834475aaa186f769be4628f1ecc1/frontend/src/assets/images/scoreboard_medal_2.png -------------------------------------------------------------------------------- /frontend/src/assets/images/scoreboard_medal_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justcatthefish/ctfplatform/015c62d270ab834475aaa186f769be4628f1ecc1/frontend/src/assets/images/scoreboard_medal_3.png -------------------------------------------------------------------------------- /frontend/src/assets/images/scoreboard_medal_any.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justcatthefish/ctfplatform/015c62d270ab834475aaa186f769be4628f1ecc1/frontend/src/assets/images/scoreboard_medal_any.png -------------------------------------------------------------------------------- /frontend/src/assets/images/star_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | star_icon 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /frontend/src/assets/images/trail_of_bits_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justcatthefish/ctfplatform/015c62d270ab834475aaa186f769be4628f1ecc1/frontend/src/assets/images/trail_of_bits_logo.png -------------------------------------------------------------------------------- /frontend/src/assets/images/twitter_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | twitter_icon 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /frontend/src/assets/images/wp_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justcatthefish/ctfplatform/015c62d270ab834475aaa186f769be4628f1ecc1/frontend/src/assets/images/wp_logo.png -------------------------------------------------------------------------------- /frontend/src/components/AnnouncementElement.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import {IAnnouncement} from "@store/CtfStore"; 4 | import {formatDate} from "@libs/date"; 5 | import ReactMarkdown from "react-markdown"; 6 | 7 | const AnnouncementElement: React.FunctionComponent = ( { info }) => { 8 | return ( 9 |
10 |
11 |

{info.api.title}

12 | {info.isNew() && New} 13 |
14 | {formatDate(info.api.created_at)} 15 |
16 |
17 | 18 | {/*

{info.api.description}

*/} 19 |
20 | 21 |
22 |
23 | ); 24 | }; 25 | 26 | interface IAnnouncementElementProps { 27 | info: IAnnouncement; 28 | index: number; 29 | } 30 | 31 | export default AnnouncementElement; -------------------------------------------------------------------------------- /frontend/src/components/Challenge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {observer} from "mobx-react"; 3 | 4 | import {ITask} from "@store/CtfStore"; 5 | 6 | @observer 7 | class Challenge extends React.Component { 8 | render() { 9 | const { info, onClick } = this.props; 10 | return ( 11 |
12 |
13 |
14 | {info.api.categories.map(category => ( 15 | 16 | ))} 17 |
18 | 19 |
20 |

{info.api.name}

21 | 22 |
    23 |
  • Points: {info.api.points}
  • 24 |
  • Solved: {info.api.solvers}
  • 25 |
26 | 27 |
28 |
29 | {Array.from(info.api.categories.values()).map(category => category.name).join(", ").toUpperCase()} 30 |
31 | 32 |
33 | {info.api.difficult.toUpperCase()} 34 |
35 |
36 |
37 |
38 |
39 | ); 40 | } 41 | } 42 | 43 | interface IChallengeProps { 44 | info: ITask; 45 | onClick(): void; 46 | } 47 | 48 | export default Challenge; -------------------------------------------------------------------------------- /frontend/src/components/ChallengeModal.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {observer} from "mobx-react"; 3 | 4 | import {ITask} from "@store/CtfStore"; 5 | import ReactMarkdown from "react-markdown"; 6 | import {observable} from "mobx"; 7 | import Loader from "@components/Loader"; 8 | import {ErrorCodes, GetTaskSolvers, ITaskAuditResponse, SetFlag, SetSettings} from "@libs/api"; 9 | import {formatDate} from "@libs/date"; 10 | import Link from "@components/Link"; 11 | import FlagSubmit from "@components/FlagSubmit"; 12 | 13 | interface IProps { 14 | task: ITask; 15 | } 16 | 17 | @observer 18 | class ChallengeModal extends React.Component { 19 | @observable activeTab: number = 1; 20 | @observable solversState: string = "none"; 21 | @observable solvers: ITaskAuditResponse[] = []; 22 | 23 | async componentDidMount() { 24 | this.fetchSolvers(); 25 | } 26 | 27 | onChangeTab = (tabId: number) => ( ) => { 28 | if(this.activeTab !== tabId) 29 | this.activeTab = tabId; 30 | }; 31 | 32 | fetchSolvers = () => { 33 | this.solversState = "pending"; 34 | GetTaskSolvers(this.props.task.id).then(([data, err]) => { 35 | if(err !== null) { 36 | this.solversState = "error"; 37 | console.error('fetch err', err); 38 | return; 39 | } 40 | this.solvers = data; 41 | this.solversState = "done"; 42 | }).catch((err) => { 43 | console.error('fetch err', err); 44 | this.solversState = "error"; 45 | }); 46 | }; 47 | 48 | render() { 49 | const { task } = this.props; 50 | const finished = task.hasTaskSolved(); 51 | return ( 52 |
53 |
54 |
Challenges
55 |
Solves ({task.api.solvers})
56 |
57 | 58 |
59 |

{task.api.name}

60 | 61 |
62 |
Points: {task.api.points}
63 |
{this.solvers.length > 0 ? this.solvers[0].name : "---"}
64 |
{Array.from(task.api.categories.values()).map(category => category.name).join(", ").toUpperCase()}
65 |
66 | 67 |
68 | 69 |
70 | 71 |
72 | {finished && ( 73 |

Challenge solved

74 | )} 75 | {!finished && } 76 |
77 |
78 | 79 |
80 |

{task.api.name}

81 | 82 |
83 | {this.solversState === "pending" && <>








} 84 | {this.solversState === "error" && <>








} 85 | {this.solversState === "done" && ( 86 | 87 | 88 | 89 | 90 | 91 | {this.solvers.map((solver, index) => ( 92 | 93 | 94 | 95 | 96 | 97 | ))} 98 | 99 |
#TeamSubmit time
{index+1}{formatDate(new Date(solver.created_at))}
100 | )} 101 |
102 |
103 |
104 | ); 105 | } 106 | } 107 | 108 | export default ChallengeModal; -------------------------------------------------------------------------------- /frontend/src/components/Container.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {inject, observer} from "mobx-react"; 3 | import {RouterStore} from "mobx-react-router"; 4 | 5 | import { 6 | AnnouncementsPage, 7 | ChallengesPage, 8 | HomePage, 9 | LoginPage, 10 | NotFoundPage, 11 | RegisterPage, 12 | RulesPage, 13 | ScoreboardPage, 14 | SettingsPage, 15 | TeamPage, 16 | TeamsPage 17 | } from "../pages"; 18 | import {UnavailablePage} from "../pages/UnavailablePage"; 19 | 20 | const Container: React.FunctionComponent = ( { routing}: IContainerProps ) => { 21 | if( !routing ) 22 | return null; 23 | 24 | const { location, push } = routing; 25 | 26 | const componentDict: { [key: string]: any; } = { 27 | "/": HomePage, 28 | "/challenges": ChallengesPage, 29 | "/scoreboard": ScoreboardPage, 30 | // "/challenges": UnavailablePage, 31 | // "/scoreboard": UnavailablePage, 32 | "/rules": RulesPage, 33 | "/teams": TeamsPage, 34 | "/news": AnnouncementsPage, 35 | "/settings": SettingsPage, 36 | "/login": LoginPage, 37 | "/register": RegisterPage, 38 | "/404": NotFoundPage, 39 | }; 40 | 41 | const teamRegex = new RegExp(`^\/team\/(\\d+)$`); 42 | 43 | if( teamRegex.test(location.pathname) ) { 44 | const matches = teamRegex.exec( location.pathname ); 45 | 46 | if( matches ) 47 | return 48 | } 49 | 50 | let ComponentTag = componentDict[location.pathname]; 51 | 52 | if (!ComponentTag) { 53 | push('/404'); 54 | ComponentTag = componentDict['/404']; 55 | } 56 | 57 | return ; 58 | 59 | }; 60 | 61 | interface IContainerProps { 62 | routing?: RouterStore; 63 | } 64 | 65 | export default inject("routing")(observer(Container)) -------------------------------------------------------------------------------- /frontend/src/components/FlagSubmit.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {observer} from "mobx-react"; 3 | 4 | import {observable} from "mobx"; 5 | import {ErrorCodes, SetFlag} from "@libs/api"; 6 | import RemovableMessage from "@components/RemovableMessage"; 7 | 8 | @observer 9 | class FlagSubmit extends React.Component<{}, {}> { 10 | private refFlag = React.createRef(); 11 | 12 | @observable flagMessageOk: string = ""; 13 | @observable flagMessageError: string = ""; 14 | 15 | onFlagPressEnter = (e: React.KeyboardEvent) => { 16 | // on click enter 17 | if(e.keyCode == 13){ 18 | this.onFlagSend(); 19 | } 20 | }; 21 | 22 | onFlagSubmit = (e: React.MouseEvent) => { 23 | this.onFlagSend(); 24 | }; 25 | 26 | async onFlagSend() { 27 | const value = (this.refFlag.current && this.refFlag.current.value) || ''; 28 | if(!value) { 29 | return; 30 | } 31 | 32 | this.flagMessageOk = ""; 33 | this.flagMessageError = ""; 34 | let err = null; 35 | try { 36 | const err2 = await SetFlag({flag: value.trim()}); 37 | if(err2) { 38 | err = ErrorCodes.toHumanMessage(err2); 39 | } 40 | } catch (e) { 41 | console.error('send err', e); 42 | err = String(e); 43 | } 44 | if(err === null) { 45 | this.flagMessageError = ""; 46 | this.flagMessageOk = "Congratulation! You solved the task."; 47 | } else { 48 | this.flagMessageOk = ""; 49 | this.flagMessageError = err; 50 | } 51 | } 52 | 53 | render() { 54 | return ( 55 |
56 | {this.flagMessageError && {this.flagMessageError}} 57 | {this.flagMessageOk && {this.flagMessageOk}} 58 | 59 |
60 | 61 |
63 |
64 | ); 65 | } 66 | } 67 | 68 | export default FlagSubmit; 69 | -------------------------------------------------------------------------------- /frontend/src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const Footer: React.FunctionComponent = ( { sticky }: IFooterProps ) => { 4 | sticky = true; 5 | 6 | return ( 7 |
8 | Copyright © 2019 JustCatTheFish All Rights Reserved 9 |
10 | ) 11 | }; 12 | 13 | interface IFooterProps { 14 | sticky?: boolean 15 | } 16 | 17 | export default Footer; -------------------------------------------------------------------------------- /frontend/src/components/Link.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {inject, observer} from "mobx-react"; 3 | import {RouterStore} from "mobx-react-router"; 4 | 5 | interface IProps { 6 | routing?: RouterStore; 7 | href: string; 8 | title: string; 9 | child: string; 10 | } 11 | 12 | @inject("routing") 13 | @observer 14 | class Link extends React.Component { 15 | render() { 16 | return ( 17 | {this.props.child} 18 | ); 19 | } 20 | 21 | private onClick = (e: React.MouseEvent) => { 22 | e.preventDefault(); 23 | 24 | const href = e.currentTarget.attributes.getNamedItem("href"); 25 | if( !!href && !!this.props.routing ) 26 | this.props.routing.push(href.value); 27 | }; 28 | } 29 | 30 | export default Link; -------------------------------------------------------------------------------- /frontend/src/components/Loader.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const Loader: React.FunctionComponent = ({ text, position = "center", background = false, small = false }: LoaderProps) => { 4 | if (!text || !text.length) 5 | return null; 6 | 7 | const classes: string[ ] = ["loader", position]; 8 | 9 | if( small ) 10 | classes.push( "small" ); 11 | 12 | const loader =
{!!text &&

{text}

}
; 13 | 14 | return background ?
{loader}
: loader; 15 | }; 16 | 17 | type LoaderProps = { 18 | text?: string; 19 | position?: "center" | "inline"; 20 | background?: boolean; 21 | small?: boolean; 22 | }; 23 | 24 | export default Loader; 25 | -------------------------------------------------------------------------------- /frontend/src/components/Modal.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import "@styles/modal.scss"; 4 | 5 | const Modal: React.FunctionComponent = ( props ) => { 6 | if( typeof props.closable === "undefined" ) 7 | props.closable = true; 8 | 9 | if( !props.active ) 10 | return null; 11 | 12 | function onBackgroundClick( e: React.MouseEvent ) { 13 | if( e.target instanceof Element && !!props.closable ) { 14 | if( e.target.className === "modalBackground" ) { 15 | e.stopPropagation(); 16 | 17 | if( typeof props.onBackgroundClick === "function" ) 18 | props.onBackgroundClick( e ); 19 | } 20 | } 21 | } 22 | 23 | function onCloseButtonClick( e: React.MouseEvent ) { 24 | if( e.target instanceof Element && !!props.closable ) { 25 | e.stopPropagation(); 26 | 27 | if( typeof props.onCloseButtonClick === "function" ) 28 | props.onCloseButtonClick( e ); 29 | } 30 | } 31 | 32 | return ( 33 |
34 |
35 | {!!props.closable &&
} 36 | 37 |
38 | {props.children} 39 |
40 |
41 |
42 | ); 43 | }; 44 | 45 | interface IModalProps { 46 | closable?: boolean; 47 | active?: boolean; 48 | onBackgroundClick?: ( e: React.MouseEvent ) => void; 49 | onCloseButtonClick?: ( e: React.MouseEvent ) => void; 50 | } 51 | 52 | export default Modal; -------------------------------------------------------------------------------- /frontend/src/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {inject, observer} from "mobx-react"; 3 | import {observable} from "mobx"; 4 | import {RouterStore} from "mobx-react-router"; 5 | 6 | import {IRootStore} from "@store/index"; 7 | import {SetLogout} from "@libs/api"; 8 | 9 | import "@styles/navbar.scss"; 10 | 11 | @inject("routing", "store") 12 | @observer 13 | export default class Navbar extends React.Component { 14 | @observable menuActive: boolean = false; 15 | @observable isMobile: boolean = window.innerWidth < 980; 16 | 17 | private timeout: any; 18 | 19 | componentDidMount(): void { 20 | window.addEventListener("resize", this.onResize); 21 | } 22 | 23 | componentWillUnmount(): void { 24 | window.removeEventListener("resize", this.onResize); 25 | 26 | if( this.timeout ) 27 | clearTimeout( this.timeout ); 28 | } 29 | 30 | render( ) { 31 | const { routing, store } = this.props; 32 | 33 | return ( 34 | 74 | ); 75 | } 76 | 77 | private onResize = ( ) => { 78 | if( this.timeout ) 79 | clearTimeout( this.timeout ); 80 | 81 | this.timeout = setTimeout( ( ) => { 82 | const check = window.innerWidth < 980; 83 | 84 | if( this.isMobile !== check ) 85 | this.isMobile = check; 86 | }, 100 ); 87 | }; 88 | 89 | private onClick = (e: React.MouseEvent) => { 90 | e.preventDefault(); 91 | 92 | const href = e.currentTarget.attributes.getNamedItem("href"); 93 | 94 | if( !!href && !!this.props.routing ) 95 | this.props.routing.push(href.value); 96 | }; 97 | 98 | private toggleMenu = () => { 99 | this.menuActive = !this.menuActive; 100 | }; 101 | 102 | private onLogout = (e: React.MouseEvent) => { 103 | e.preventDefault(); 104 | 105 | (async () => { 106 | let err; 107 | try { 108 | err = await SetLogout(); 109 | } catch (e) { 110 | err = String(e); 111 | } 112 | if(err !== null) { 113 | // TODO: error logout? 114 | // return 115 | } 116 | this.props.store && this.props.store.ctf.removeUserSession(); 117 | this.props.routing && this.props.routing.push('/'); 118 | })(); 119 | } 120 | 121 | } 122 | 123 | interface INavbarProps { 124 | routing?: RouterStore; 125 | store?: IRootStore; 126 | } -------------------------------------------------------------------------------- /frontend/src/components/RemovableMessage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const RemovableMessage: React.FunctionComponent = ( props ) => { 4 | const [ renderMessage, setRenderMessage ] = React.useState(true); 5 | 6 | let timeout: any; 7 | 8 | React.useEffect( ( ) => { 9 | if( props.time && props.time > 0 ) { 10 | timeout = setTimeout( ( ) => { 11 | setRenderMessage( false ); 12 | }, props.time ); 13 | } 14 | 15 | return ( ) => { 16 | if( timeout ) 17 | clearTimeout( timeout ); 18 | } 19 | }, [ ] ); 20 | 21 | return renderMessage ?
{props.children}
: null; 22 | }; 23 | 24 | interface IRemovableMessageProps { 25 | type: "error" | "success", 26 | time?: number; 27 | children: string; 28 | } 29 | 30 | export default RemovableMessage; -------------------------------------------------------------------------------- /frontend/src/components/Timer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {observable} from "mobx"; 3 | import {observer} from "mobx-react"; 4 | 5 | import "@styles/timer.scss"; 6 | 7 | @observer 8 | export default class Timer extends React.Component { 9 | @observable days: string = "00"; 10 | @observable hours: string = "00"; 11 | @observable minutes: string = "00"; 12 | @observable seconds: string = "00"; 13 | 14 | interval: any; 15 | 16 | componentDidMount(): void { 17 | this.updateTime( ); 18 | 19 | this.interval = setInterval( this.updateTime, 1000 ); 20 | } 21 | 22 | componentWillUnmount(): void { 23 | if( this.interval ) 24 | clearInterval( this.interval ); 25 | } 26 | 27 | componentDidUpdate(prevProps: Readonly): void { 28 | if( this.props.date !== prevProps.date ) { 29 | if( this.interval ) 30 | clearInterval( this.interval ); 31 | 32 | this.interval = setInterval( this.updateTime, 1000 ); 33 | } 34 | } 35 | 36 | render( ) { 37 | return ( 38 |
39 |
{this.days}
40 |
41 |
{this.hours}
42 |
43 |
{this.minutes}
44 |
45 |
{this.seconds}
46 |
47 | ); 48 | } 49 | 50 | updateTime = ( ) => { 51 | const t = this.props.date.getTime() - new Date( ).getTime(); 52 | 53 | if( t <= 0 ) { 54 | if( this.interval ) 55 | clearInterval(this.interval); 56 | 57 | return; 58 | } 59 | 60 | const tempSeconds = Math.floor( (t/1000) % 60 ); 61 | const tempMinutes = Math.floor( (t/1000/60) % 60 ); 62 | const tempHours = Math.floor( (t/(1000*60*60)) % 24 ); 63 | const tempDays = Math.floor( t/(1000*60*60*24) ); 64 | 65 | if( this.seconds !== tempSeconds.toString( ) ) this.seconds = tempSeconds < 10 ? "0" + tempSeconds : tempSeconds.toString(); 66 | if( this.minutes !== tempMinutes.toString( ) ) this.minutes = tempMinutes < 10 ? "0" + tempMinutes : tempMinutes.toString(); 67 | if( this.hours !== tempHours.toString( ) ) this.hours = tempHours < 10 ? "0" + tempHours : tempHours.toString(); 68 | if( this.days !== tempDays.toString( )) this.days = tempDays < 10 ? "0" + tempDays : tempDays.toString(); 69 | }; 70 | } 71 | 72 | interface ITimerProps { 73 | date: Date 74 | } -------------------------------------------------------------------------------- /frontend/src/consts/country.ts: -------------------------------------------------------------------------------- 1 | 2 | export const Countries: { [key: string]: string; } = { 3 | "": "International", 4 | "AF":"Afghanistan", 5 | "AX":"Åland Islands", 6 | "AL":"Albania", 7 | "DZ":"Algeria", 8 | "AS":"American Samoa", 9 | "AD":"Andorra", 10 | "AO":"Angola", 11 | "AI":"Anguilla", 12 | "AQ":"Antarctica", 13 | "AG":"Antigua and Barbuda", 14 | "AR":"Argentina", 15 | "AM":"Armenia", 16 | "AW":"Aruba", 17 | "AU":"Australia", 18 | "AT":"Austria", 19 | "AZ":"Azerbaijan", 20 | "BS":"The Bahamas", 21 | "BH":"Bahrain", 22 | "BD":"Bangladesh", 23 | "BB":"Barbados", 24 | "BY":"Belarus", 25 | "BE":"Belgium", 26 | "BZ":"Belize", 27 | "BJ":"Benin", 28 | "BM":"Bermuda", 29 | "BT":"Bhutan", 30 | "BO":"Bolivia", 31 | "BQ":"Bonaire", 32 | "BA":"Bosnia and Herzegovina", 33 | "BW":"Botswana", 34 | "BV":"Bouvet Island", 35 | "BR":"Brazil", 36 | "IO":"British Indian Ocean Territory", 37 | "UM":"United States Minor Outlying Islands", 38 | "VG":"Virgin Islands (British)", 39 | "VI":"Virgin Islands (U.S.)", 40 | "BN":"Brunei", 41 | "BG":"Bulgaria", 42 | "BF":"Burkina Faso", 43 | "BI":"Burundi", 44 | "KH":"Cambodia", 45 | "CM":"Cameroon", 46 | "CA":"Canada", 47 | "CV":"Cape Verde", 48 | "KY":"Cayman Islands", 49 | "CF":"Central African Republic", 50 | "TD":"Chad", 51 | "CL":"Chile", 52 | "CN":"China", 53 | "CX":"Christmas Island", 54 | "CC":"Cocos (Keeling) Islands", 55 | "CO":"Colombia", 56 | "KM":"Comoros", 57 | "CG":"Republic of the Congo", 58 | "CD":"Democratic Republic of the Congo", 59 | "CK":"Cook Islands", 60 | "CR":"Costa Rica", 61 | "HR":"Croatia", 62 | "CU":"Cuba", 63 | "CW":"Curaçao", 64 | "CY":"Cyprus", 65 | "CZ":"Czech Republic", 66 | "DK":"Denmark", 67 | "DJ":"Djibouti", 68 | "DM":"Dominica", 69 | "DO":"Dominican Republic", 70 | "EC":"Ecuador", 71 | "EG":"Egypt", 72 | "SV":"El Salvador", 73 | "GQ":"Equatorial Guinea", 74 | "ER":"Eritrea", 75 | "EE":"Estonia", 76 | "ET":"Ethiopia", 77 | "FK":"Falkland Islands", 78 | "FO":"Faroe Islands", 79 | "FJ":"Fiji", 80 | "FI":"Finland", 81 | "FR":"France", 82 | "GF":"French Guiana", 83 | "PF":"French Polynesia", 84 | "TF":"French Southern and Antarctic Lands", 85 | "GA":"Gabon", 86 | "GM":"The Gambia", 87 | "GE":"Georgia", 88 | "DE":"Germany", 89 | "GH":"Ghana", 90 | "GI":"Gibraltar", 91 | "GR":"Greece", 92 | "GL":"Greenland", 93 | "GD":"Grenada", 94 | "GP":"Guadeloupe", 95 | "GU":"Guam", 96 | "GT":"Guatemala", 97 | "GG":"Guernsey", 98 | "GW":"Guinea-Bissau", 99 | "GY":"Guyana", 100 | "HT":"Haiti", 101 | "HM":"Heard Island and McDonald Islands", 102 | "VA":"Holy See", 103 | "HN":"Honduras", 104 | "HK":"Hong Kong", 105 | "HU":"Hungary", 106 | "IS":"Iceland", 107 | "IN":"India", 108 | "ID":"Indonesia", 109 | "CI":"Ivory Coast", 110 | "IR":"Iran", 111 | "IQ":"Iraq", 112 | "IE":"Republic of Ireland", 113 | "IM":"Isle of Man", 114 | "IL":"Israel", 115 | "IT":"Italy", 116 | "JM":"Jamaica", 117 | "JP":"Japan", 118 | "JE":"Jersey", 119 | "JO":"Jordan", 120 | "KZ":"Kazakhstan", 121 | "KE":"Kenya", 122 | "KI":"Kiribati", 123 | "KW":"Kuwait", 124 | "KG":"Kyrgyzstan", 125 | "LA":"Laos", 126 | "LV":"Latvia", 127 | "LB":"Lebanon", 128 | "LS":"Lesotho", 129 | "LR":"Liberia", 130 | "LY":"Libya", 131 | "LI":"Liechtenstein", 132 | "LT":"Lithuania", 133 | "LU":"Luxembourg", 134 | "MO":"Macau", 135 | "MK":"Republic of Macedonia", 136 | "MG":"Madagascar", 137 | "MW":"Malawi", 138 | "MY":"Malaysia", 139 | "MV":"Maldives", 140 | "ML":"Mali", 141 | "MT":"Malta", 142 | "MH":"Marshall Islands", 143 | "MQ":"Martinique", 144 | "MR":"Mauritania", 145 | "MU":"Mauritius", 146 | "YT":"Mayotte", 147 | "MX":"Mexico", 148 | "FM":"Federated States of Micronesia", 149 | "MD":"Moldova", 150 | "MC":"Monaco", 151 | "MN":"Mongolia", 152 | "ME":"Montenegro", 153 | "MS":"Montserrat", 154 | "MA":"Morocco", 155 | "MZ":"Mozambique", 156 | "MM":"Myanmar", 157 | "NA":"Namibia", 158 | "NR":"Nauru", 159 | "NP":"Nepal", 160 | "NL":"Netherlands", 161 | "NC":"New Caledonia", 162 | "NZ":"New Zealand", 163 | "NI":"Nicaragua", 164 | "NE":"Niger", 165 | "NG":"Nigeria", 166 | "NU":"Niue", 167 | "NF":"Norfolk Island", 168 | "KP":"North Korea", 169 | "MP":"Northern Mariana Islands", 170 | "NO":"Norway", 171 | "OM":"Oman", 172 | "PK":"Pakistan", 173 | "PW":"Palau", 174 | "PS":"Palestine", 175 | "PA":"Panama", 176 | "PG":"Papua New Guinea", 177 | "PY":"Paraguay", 178 | "PE":"Peru", 179 | "PH":"Philippines", 180 | "PN":"Pitcairn Islands", 181 | "PL":"Poland", 182 | "PT":"Portugal", 183 | "PR":"Puerto Rico", 184 | "QA":"Qatar", 185 | "RE":"Réunion", 186 | "RO":"Romania", 187 | "RU":"Russia", 188 | "RW":"Rwanda", 189 | "BL":"Saint Barthélemy", 190 | "SH":"Saint Helena", 191 | "KN":"Saint Kitts and Nevis", 192 | "LC":"Saint Lucia", 193 | "MF":"Saint Martin", 194 | "PM":"Saint Pierre and Miquelon", 195 | "VC":"Saint Vincent and the Grenadines", 196 | "WS":"Samoa", 197 | "SM":"San Marino", 198 | "ST":"São Tomé and Príncipe", 199 | "SA":"Saudi Arabia", 200 | "SN":"Senegal", 201 | "RS":"Serbia", 202 | "SC":"Seychelles", 203 | "SL":"Sierra Leone", 204 | "SG":"Singapore", 205 | "SX":"Sint Maarten", 206 | "SK":"Slovakia", 207 | "SI":"Slovenia", 208 | "SB":"Solomon Islands", 209 | "SO":"Somalia", 210 | "ZA":"South Africa", 211 | "GS":"South Georgia", 212 | "KR":"South Korea", 213 | "SS":"South Sudan", 214 | "ES":"Spain", 215 | "LK":"Sri Lanka", 216 | "SD":"Sudan", 217 | "SR":"Surinae", 218 | "SJ":"Svalbard and Jan Mayen", 219 | "SZ":"Swaziland", 220 | "SE":"Sweden", 221 | "CH":"Switzerland", 222 | "SY":"Syria", 223 | "TW":"Taiwan", 224 | "TJ":"Tajikistan", 225 | "TZ":"Tanzania", 226 | "TH":"Thailand", 227 | "TL":"East Timor", 228 | "TG":"Togo", 229 | "TK":"Tokelau", 230 | "TO":"Tonga", 231 | "TT":"Trinidad and Tobago", 232 | "TN":"Tunisia", 233 | "TR":"Turkey", 234 | "TM":"Turkmenistan", 235 | "TC":"Turks and Caicos Islands", 236 | "TV":"Tuvalu", 237 | "UG":"Uganda", 238 | "UA":"Ukraine", 239 | "AE":"United Arab Emirates", 240 | "GB":"United Kingdom", 241 | "US":"United States", 242 | "UY":"Uruguay", 243 | "UZ":"Uzbekistan", 244 | "VU":"Vanuatu", 245 | "VE":"Venezuela", 246 | "VN":"Vietnam", 247 | "WF":"Wallis and Futuna", 248 | "EH":"Western Sahara", 249 | "YE":"Yemen", 250 | "ZM":"Zambia", 251 | "ZW":"Zimbabwe" 252 | }; 253 | -------------------------------------------------------------------------------- /frontend/src/consts/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export const nullDate = new Date("1970-01-01T00:00:00+00:00"); 3 | 4 | let _baseUrl = "/api/v1"; 5 | let _avatarUrl = ""; 6 | 7 | export const baseUrl = _baseUrl; 8 | export const avatarUrl = _avatarUrl; 9 | export const recaptchaToken = "6LeRCcQUAAAAAB2p9yYIK52LvYLLkSP-Sn8ByZcD"; 10 | -------------------------------------------------------------------------------- /frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { RootStore } from "@store/index"; 2 | import { createBrowserHistory } from "history"; 3 | import { Provider } from "mobx-react"; 4 | import { RouterStore, syncHistoryWithStore } from "mobx-react-router"; 5 | import * as React from "react"; 6 | import * as ReactDOM from "react-dom"; 7 | import { Router } from "react-router"; 8 | 9 | import Container from "@components/Container"; 10 | import Navbar from "@components/Navbar"; 11 | 12 | import "@styles/main.scss"; 13 | 14 | if (__IS_DEV__) { 15 | console.log("dev mode"); 16 | } else { 17 | console.log("prod mode"); 18 | } 19 | 20 | const storeStore = RootStore.create({}); 21 | const browserHistory = createBrowserHistory(); 22 | const routingStore = new RouterStore(); 23 | 24 | const stores = { 25 | routing: routingStore, 26 | store: storeStore, 27 | }; 28 | 29 | const history = syncHistoryWithStore(browserHistory, routingStore); 30 | 31 | ReactDOM.render(( 32 | 33 | 34 | 35 | 36 | 37 | 38 | ), document.getElementById("root")); 39 | -------------------------------------------------------------------------------- /frontend/src/libs/date.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export function formatDate(val: Date): string { 4 | return val.toLocaleString('en-GB', { timeZone: 'UTC' }) + " UTC" 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/libs/http.ts: -------------------------------------------------------------------------------- 1 | interface IHttpParams { 2 | url: string; 3 | method?: string; 4 | headers?: { [key: string]: string }; 5 | params?: { [key: string]: string | number }; 6 | data?: { [key: string]: any }; 7 | } 8 | 9 | function getQueryString(params: any) { 10 | return Object.keys(params) 11 | .map((k) => encodeURIComponent(k) + "=" + encodeURIComponent(params[k])) 12 | .join("&"); 13 | } 14 | 15 | function getCookie(name: string): string { 16 | let cookieValue = ""; 17 | if (document.cookie && document.cookie !== "") { 18 | const cookies = document.cookie.split(";"); 19 | for (const cookie of cookies) { 20 | if (cookie.substring(0, name.length + 1) === (name + "=")) { 21 | cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); 22 | break; 23 | } 24 | } 25 | } 26 | return cookieValue; 27 | } 28 | 29 | function request(params: IHttpParams) { 30 | const method = params.method || "GET"; 31 | const headers = params.headers || { 32 | "Accept": "application/json", 33 | "Content-Type": "application/json", 34 | }; 35 | 36 | const qs = Object.keys(params.params || {}).length > 0 ? ("?" + getQueryString(params.params)) : ""; 37 | const body = typeof params.data === "object" ? JSON.stringify(params.data) : undefined; 38 | 39 | return fetch(params.url + qs, { method, headers, body }); 40 | } 41 | 42 | export default { 43 | getCookie: getCookie, 44 | delete: async (params: IHttpParams): Promise => request(Object.assign({ method: "DELETE" }, params)), 45 | get: async (params: IHttpParams): Promise => request(Object.assign({ method: "GET" }, params)), 46 | post: async (params: IHttpParams): Promise => request(Object.assign({ method: "POST" }, params)), 47 | put: async (params: IHttpParams): Promise => request(Object.assign({ method: "PUT" }, params)), 48 | }; 49 | -------------------------------------------------------------------------------- /frontend/src/libs/types.ts: -------------------------------------------------------------------------------- 1 | import { types } from "mobx-state-tree"; 2 | 3 | export const DateFromString = types.custom({ 4 | name: "Date", 5 | fromSnapshot(value: string) { 6 | return new Date(value); 7 | }, 8 | toSnapshot(value: Date) { 9 | return value.toString(); 10 | }, 11 | isTargetType(value: string | Date): boolean { 12 | return value instanceof Date; 13 | }, 14 | getValidationMessage(value: string): string { 15 | if (!isNaN((new Date(value)).getDate())) { 16 | return ""; 17 | } 18 | return `'${value}' doesn't look like a valid date`; 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /frontend/src/pages/Announcements.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {inject, observer} from "mobx-react"; 3 | 4 | import Footer from "@components/Footer"; 5 | import Loader from "@components/Loader"; 6 | import AnnouncementElement from "@components/AnnouncementElement"; 7 | 8 | import {IRootStore} from "@store/index"; 9 | 10 | import "@styles/announcements.scss"; 11 | 12 | @inject("store") 13 | @observer 14 | export class AnnouncementsPage extends React.Component { 15 | async componentDidMount() { 16 | await this.props.store.ctf.fetchAnnouncements(); 17 | this.props.store.ctf.setSeenAnnouncements(); 18 | } 19 | 20 | render( ) { 21 | return ( 22 |
23 |
24 |

News

25 | 26 | {this.props.store.ctf.announcementsState === "pending" && } 27 | {this.props.store.ctf.announcementsState === "error" && } 28 | {this.props.store.ctf.announcementsState === "done" && (
29 | {this.props.store && Array.from(this.props.store.ctf.announcements.values()).map((row, index) => ( 30 | 31 | ))} 32 |
)} 33 |
34 |
35 | 36 |
37 | ); 38 | } 39 | } 40 | 41 | interface IAnnouncementsPageProps { 42 | store: IRootStore 43 | } -------------------------------------------------------------------------------- /frontend/src/pages/Challenges.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {inject, observer} from "mobx-react"; 3 | import {observable} from "mobx"; 4 | 5 | import {IRootStore} from "@store/index"; 6 | import {ITask} from "@store/CtfStore"; 7 | 8 | import Loader from "@components/Loader"; 9 | import Challenge from "@components/Challenge"; 10 | import Modal from "@components/Modal"; 11 | import Footer from "@components/Footer"; 12 | 13 | import "@styles/challenges.scss"; 14 | import ChallengeModal from "@components/ChallengeModal"; 15 | import FlagSubmit from "@components/FlagSubmit"; 16 | 17 | @inject("store") 18 | @observer 19 | export class ChallengesPage extends React.Component { 20 | @observable selectedCategories: Set = new Set(); 21 | @observable showUnsolved: boolean = false; 22 | 23 | // Modal 24 | @observable fetchedTask?: ITask; 25 | 26 | async componentDidMount( ) { 27 | await this.props.store.ctf.fetchTasks(); 28 | await this.props.store.ctf.fetchMyTeam(); 29 | } 30 | 31 | onUnsolved = () => { 32 | this.showUnsolved = !this.showUnsolved; 33 | }; 34 | 35 | onCategory = (categoryId: string) => () => { 36 | if(this.selectedCategories.has(categoryId)) { 37 | this.selectedCategories.delete(categoryId) 38 | } else { 39 | this.selectedCategories.clear(); 40 | this.selectedCategories.add(categoryId) 41 | } 42 | }; 43 | 44 | onClickChallenge = (challengeId: number) => () => { 45 | const { store: { ctf: { tasks } } } = this.props; 46 | 47 | if(tasks.has(String(challengeId))) 48 | this.fetchedTask = tasks.get(String(challengeId)); 49 | }; 50 | 51 | onClickCloseChallenge = ( ) => { 52 | if(!!this.fetchedTask) 53 | this.fetchedTask = undefined; 54 | }; 55 | 56 | render( ) { 57 | return ( 58 |
59 |
60 |

Challenges

61 | 62 | {this.props.store.ctf.tasksState === "pending" && } 63 | {this.props.store.ctf.tasksState === "error" && } 64 | {this.props.store.ctf.tasksState === "done" && <> 65 |
66 |
    67 | {Array.from(this.props.store.ctf.categories.values()).map((category) => ( 68 |
  • 76 | {category.name.toUpperCase()} 77 |
  • 78 | ))} 79 |
  • 80 | 81 | 82 | 86 |
  • 87 |
88 | 89 | 90 |
91 | 92 |
93 | {Array.from(this.props.store.ctf.filteredTasks( 94 | this.selectedCategories, 95 | this.showUnsolved, 96 | ).values()).map((challenge) => ( 97 | 98 | ))} 99 |
100 | 101 | {!!this.fetchedTask && 102 | 103 | } 104 | } 105 | 106 |
107 |
108 |
109 | ) 110 | } 111 | } 112 | 113 | interface IChallengesPageProps { 114 | store: IRootStore; 115 | } -------------------------------------------------------------------------------- /frontend/src/pages/Home.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {inject, observer} from "mobx-react"; 3 | import {RouterStore} from "mobx-react-router"; 4 | 5 | import {IRootStore} from "@store/index"; 6 | 7 | import Timer from "@components/Timer"; 8 | import Footer from "@components/Footer"; 9 | 10 | import TrailOfBitsLogo from "../assets/images/trail_of_bits_logo.png"; 11 | import WPLogo from "../assets/images/wp_logo.png"; 12 | 13 | import "@styles/homepage.scss"; 14 | 15 | @inject("routing", "store") 16 | @observer 17 | export class HomePage extends React.Component { 18 | async componentDidMount( ) { 19 | await this.props.store.ctf.fetchInfo( ); 20 | } 21 | 22 | render( ) { 23 | const { store } = this.props; 24 | const now = new Date(); 25 | 26 | return ( 27 |
28 |
29 | 30 | 31 |
32 | 33 |
34 |
35 |
36 | just 37 | ctf 38 | 2019 39 |
40 |
41 | Capture The Competition 42 |
43 | 44 |
45 | {store.ctf.info.start > now && ( 46 | <> 47 |
48 |
{store.ctf.info.teams_count}
49 |

Teams

50 |
51 | 52 |
53 |
{store.ctf.info.countries_count}
54 |

Countries

55 |
56 | 57 | )} 58 | {store.ctf.info.start <= now && ( 59 | <> 60 |
61 |
{store.ctf.info.flags_count}
62 |

Flags submitted

63 |
64 | 65 |
66 |
{store.ctf.info.teams_count}
67 |

Teams registered

68 |
69 | 70 |
71 |
{store.ctf.info.tasks_unsolved_count}
72 |

Unsolved challenges

73 |
74 | 75 | )} 76 |
77 | 78 |
79 | {store.ctf.info.start > now && ( 80 | <> 81 |

Starts in

82 | 83 | 84 | )} 85 | {store.ctf.info.start <= now && now <= store.ctf.info.end && ( 86 | <> 87 |

Ends in

88 | 89 | 90 | )} 91 | {store.ctf.info.end < now && ( 92 | <> 93 |

CTF is over!

94 | 95 | 96 | )} 97 |
98 | 99 | {!store.ctf.isLoggedIn && Register} 100 |
101 | 102 |
103 |

Info

104 | 105 |
    106 |
  • 107 |

    Start:

    108 |

    Friday, 20th of December 20:00 UTC

    109 |
  • 110 |
  • 111 |

    Time:

    112 |

    37h

    113 |
  • 114 |
  • 115 |

    Format:

    116 |

    jeopardy on-line

    117 |
  • 118 |
  • 119 |

    Discord:

    120 |

    https://discord.gg/c7uEsq

    121 |
  • 122 |
123 |
124 | 125 |
126 |

Prizes

127 | 128 |
    129 |
  • 130 |
    131 |

    1st place

    132 |

    1,337 USD

    133 |
    134 |
  • 135 |
  • 136 |
    137 |

    2nd place

    138 |

    777 USD

    139 |
    140 |
  • 141 |
  • 142 |
    143 |

    3rd place

    144 |

    337 USD

    145 |
    146 |
  • 147 |
148 |
149 | 150 |
151 |

Sponsors

152 | 153 |
    154 |
  • {"Trail
  • 155 |
  • {"Wirtualna
  • 156 |
157 |
158 | 159 |
160 | 164 |
165 | 166 |
167 |
168 |
169 | ) 170 | } 171 | 172 | redirectToRegister = ( e: React.MouseEvent ) => { 173 | e.preventDefault(); 174 | 175 | const href = e.currentTarget.attributes.getNamedItem("href"); 176 | 177 | if( !!href && !!this.props.routing ) 178 | this.props.routing.push(href.value); 179 | }; 180 | } 181 | 182 | interface IHomePageProps { 183 | routing: RouterStore; 184 | store: IRootStore; 185 | } 186 | -------------------------------------------------------------------------------- /frontend/src/pages/Login.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {inject, observer} from "mobx-react"; 3 | import {observable} from "mobx"; 4 | import {RouterStore} from "mobx-react-router"; 5 | import * as Recaptcha from "react-recaptcha"; 6 | 7 | import Footer from "@components/Footer"; 8 | 9 | import {IRootStore} from "@store/index"; 10 | import {recaptchaToken} from "@consts/index"; 11 | 12 | import {ErrorCodes, ILoginRequest, SetLogin} from "@libs/api"; 13 | 14 | import "@styles/login.scss"; 15 | 16 | 17 | @inject("store", "routing") 18 | @observer 19 | export class LoginPage extends React.Component { 20 | private refEmailAddress = React.createRef(); 21 | private refPassword = React.createRef(); 22 | private refSubmit = React.createRef(); 23 | private recaptchaInstance: Recaptcha | null = null; 24 | 25 | @observable errorMessage: string = ""; 26 | 27 | render( ) { 28 | return ( 29 |
30 |
31 |

Log in

32 | 33 | {this.errorMessage && this.errorMessage.length &&
{this.errorMessage}
} 34 | 35 |
36 |
37 | 38 | 39 |
40 | 41 |
42 | 43 | 44 |
45 | 46 | 47 | 48 | this.recaptchaInstance = e} 50 | sitekey={recaptchaToken} 51 | size="invisible" 52 | render={"explicit"} 53 | onloadCallback={() => null} 54 | verifyCallback={this.verifyCaptcha} 55 | theme={"dark"} 56 | /> 57 | 58 | 59 | {/*Reset password*/} 60 | Don’t have an account? Register now 61 | 62 |
63 |
64 |
65 | ) 66 | } 67 | 68 | private formSubmit = (e: React.FormEvent) => { 69 | e.preventDefault(); 70 | 71 | this.recaptchaInstance && this.recaptchaInstance.execute(); 72 | }; 73 | 74 | private onClick = (e: React.MouseEvent) => { 75 | e.preventDefault(); 76 | 77 | const href = e.currentTarget.attributes.getNamedItem("href"); 78 | 79 | if( !!href && !!this.props.routing ) 80 | this.props.routing.push(href.value); 81 | }; 82 | 83 | private verifyCaptcha = (captchaToken: string) => { 84 | const form: ILoginRequest = { 85 | email: (this.refEmailAddress.current && this.refEmailAddress.current.value) || '', 86 | password: (this.refPassword.current && this.refPassword.current.value) || '', 87 | captcha: captchaToken, 88 | }; 89 | 90 | this.refSubmit.current && this.refSubmit.current.setAttribute("disabled", "disabled"); 91 | 92 | (async () => { 93 | let err = null; 94 | let data = null; 95 | try { 96 | const [data2, err2] = await SetLogin(form); 97 | if(err2) { 98 | err = ErrorCodes.toHumanMessage(err2); 99 | } 100 | data = data2; 101 | } catch (e) { 102 | err = String(e); 103 | } 104 | if(err !== null || data == null) { 105 | this.errorMessage = String(err); 106 | 107 | return 108 | } 109 | this.props.store.ctf.setUserSession(data); 110 | this.props.routing.push('/challenges'); 111 | })().finally(() => { 112 | this.refSubmit.current && this.refSubmit.current.removeAttribute("disabled"); 113 | }); 114 | }; 115 | } 116 | 117 | interface ILoginPageProps { 118 | store: IRootStore, 119 | routing: RouterStore 120 | } 121 | -------------------------------------------------------------------------------- /frontend/src/pages/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const NotFoundPage: React.FunctionComponent = ( ) => { 4 | return ( 5 |
6 |

not found

7 |
8 | ) 9 | }; 10 | 11 | interface INotFoundPageProps { 12 | 13 | } 14 | 15 | export { NotFoundPage }; -------------------------------------------------------------------------------- /frontend/src/pages/Register.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {inject, observer} from "mobx-react"; 3 | import {observable} from "mobx"; 4 | import {RouterStore} from "mobx-react-router"; 5 | import * as Recaptcha from "react-recaptcha"; 6 | 7 | import Footer from "@components/Footer"; 8 | 9 | import {IRootStore} from "@store/index"; 10 | import {Countries} from "@consts/country"; 11 | import {recaptchaToken} from "@consts/index"; 12 | 13 | import {ErrorCodes, IRegisterRequest, SetRegister} from "@libs/api"; 14 | 15 | @inject("store", "routing") 16 | @observer 17 | export class RegisterPage extends React.Component { 18 | // private refFile = React.createRef(); 19 | private refTeamName = React.createRef(); 20 | private refEmailAddress = React.createRef(); 21 | private refPassword = React.createRef(); 22 | private refCountry = React.createRef(); 23 | private refSubmit = React.createRef(); 24 | private recaptchaInstance: Recaptcha | null = null; 25 | 26 | @observable errorMessage: string = ""; 27 | @observable successMessage: string = ""; 28 | 29 | render( ) { 30 | return ( 31 |
32 |
33 |

Register

34 | 35 | {this.errorMessage && this.errorMessage.length &&
{this.errorMessage}
} 36 | {this.successMessage && this.successMessage.length &&
{this.successMessage}
} 37 | 38 |
39 | {/*
*/} 40 | {/* */} 41 | {/*
*/} 42 | {/* */} 43 | {/* */} 47 | {/*
*/} 48 | {/*
*/} 49 | 50 |
51 | 52 | 53 |
54 | 55 |
56 | 57 | 58 |
59 | 60 |
61 | 62 | 63 |
64 | 65 |
66 | 67 | 72 |
73 | 74 | 75 | 76 | this.recaptchaInstance = e} 78 | sitekey={recaptchaToken} 79 | size="invisible" 80 | render={"explicit"} 81 | onloadCallback={() => null} 82 | verifyCallback={this.verifyCaptcha} 83 | theme={"dark"} 84 | /> 85 | 86 | 87 |
88 |
89 |
90 | ) 91 | } 92 | 93 | private formSubmit = ( e: React.FormEvent ) => { 94 | e.preventDefault(); 95 | 96 | this.recaptchaInstance && this.recaptchaInstance.execute(); 97 | }; 98 | 99 | private verifyCaptcha = (captchaToken: string) => { 100 | const form: IRegisterRequest = { 101 | name: (this.refTeamName.current && this.refTeamName.current.value) || '', 102 | email: (this.refEmailAddress.current && this.refEmailAddress.current.value) || '', 103 | password: (this.refPassword.current && this.refPassword.current.value) || '', 104 | country: (this.refCountry.current && this.refCountry.current.value) || '', 105 | // avatar: (this.refFile.current && this.refFile.current.files && this.refFile.current.files[0]) || null, 106 | avatar: null, 107 | captcha: captchaToken, 108 | }; 109 | 110 | this.refSubmit.current && this.refSubmit.current.setAttribute("disabled", "disabled"); 111 | (async () => { 112 | let err = null; 113 | try { 114 | const err2 = await SetRegister(form); 115 | if(err2) { 116 | err = ErrorCodes.toHumanMessage(err2); 117 | } 118 | } catch (e) { 119 | err = String(e); 120 | } 121 | if(err !== null) { 122 | this.errorMessage = String(err); 123 | this.successMessage = ""; 124 | 125 | return 126 | } 127 | 128 | this.errorMessage = ""; 129 | this.successMessage = "Now please login ;)"; 130 | 131 | if(this.refTeamName.current) this.refTeamName.current.value = ""; 132 | if(this.refEmailAddress.current) this.refEmailAddress.current.value = ""; 133 | if(this.refPassword.current) this.refPassword.current.value = ""; 134 | if(this.refCountry.current) this.refCountry.current.value = ""; 135 | // if(this.refFile.current) this.refFile.current.value = ""; 136 | 137 | })().finally(() => { 138 | this.refSubmit.current && this.refSubmit.current.removeAttribute("disabled"); 139 | }); 140 | 141 | }; 142 | } 143 | 144 | interface IRegisterPageProps { 145 | store: IRootStore, 146 | routing: RouterStore 147 | } 148 | -------------------------------------------------------------------------------- /frontend/src/pages/Rules.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import Footer from "@components/Footer"; 4 | 5 | import "@styles/rules.scss"; 6 | 7 | export const RulesPage: React.FunctionComponent = ( ) => ( 8 |
9 |
10 |

Rules

11 | 12 |
    13 |
  1. Each team is allowed to participate under one account and each member must belong to exactly one team. Teams may consist of any number of members.
  2. 14 |
  3. During the contest, sharing flags, solutions, hints or asking for help outside the team is prohibited.
  4. 15 |
  5. If you have questions about challenges or you believe that you found a correct flag, but the system is not accepting it, ask organizers on chat or via e-mail justcatthefish+2019@gmail.com. Do not brute-force flag validation endpoint.
  6. 16 |
  7. Attacking the infrastructure or any attempt to disrupt the competition is prohibited.
  8. 17 |
  9. Please, report any bugs you find in the infrastructure or tasks directly to the organizers.
  10. 18 |
  11. Breaking any of the above rules may result in team disqualification.
  12. 19 |
  13. We have a custom dynamic scoring system, which means that the challenge’s points depend on the number of its solves. We will embed the equation before the CTF starts.
  14. 20 |
  15. All flags fall into the following format: {"justCTF{something_h3re!}"}, unless the challenge description states otherwise.
  16. 21 |
  17. Challenges might be released at different times, but it is guaranteed that all of them will be released no later than 10 hours before the end of the competition.
  18. 22 |
  19. The live chat address will be announced on the CTF page.
  20. 23 |
  21. All crucial information about challenges or the competition will be announced in the news section on the CTF page and on the corresponding channel on the live chat.
  22. 24 |
  23. Registration will be open before and throughout the competition.
  24. 25 |
  25. The competition will last for 37 hours straight.
  26. 26 |
  27. During the last hour, the scoreboard will be frozen until the end (its changes won’t be available for players). However, points for tasks will be updated at all times.
  28. 27 |
  29. The presented set of rules might change before the start of the competition.
  30. 28 |
  31. In order to receive the prizes, winning teams may be asked to submit write-ups in 10 business days after the end of the competition.
  32. 29 |
  33. Resolving any unregulated cases remains on organizers' discretion.
  34. 30 |
31 | 32 |
33 |
34 |
35 | ); 36 | -------------------------------------------------------------------------------- /frontend/src/pages/Settings.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {inject, observer} from "mobx-react"; 3 | import {observable} from "mobx"; 4 | 5 | import Footer from "@components/Footer"; 6 | 7 | import {IRootStore} from "@store/index"; 8 | import {Countries} from "@consts/country"; 9 | 10 | import {ErrorCodes, ISettingsRequest, resizeImage, SetSettings} from "@libs/api"; 11 | 12 | import defaultAvatar from "../assets/images/avatar_default_big.png"; 13 | 14 | import "@styles/settings.scss"; 15 | 16 | @inject("store") 17 | @observer 18 | export class SettingsPage extends React.Component { 19 | private refFile = React.createRef(); 20 | private refTeamName = React.createRef(); 21 | private refEmailAddress = React.createRef(); 22 | // private refCurrentPassword = React.createRef(); 23 | // private refNewPassword = React.createRef(); 24 | private refCountry = React.createRef(); 25 | private refAffiliation = React.createRef(); 26 | private refWebsite = React.createRef(); 27 | private refSubmit = React.createRef(); 28 | 29 | @observable errorMessage: string = ""; 30 | @observable successMessage: string = ""; 31 | @observable avatarData: string = ""; 32 | 33 | async componentDidMount() { 34 | await this.props.store.ctf.fetchMyTeam(); 35 | } 36 | 37 | render( ) { 38 | if(!this.props.store.ctf.myTeam) 39 | return null; 40 | 41 | return ( 42 |
43 |
44 |

Profile

45 | 46 | {this.errorMessage && this.errorMessage.length &&
{this.errorMessage}
} 47 | {this.successMessage && this.successMessage.length &&
{this.successMessage}
} 48 | 49 |
50 |
51 | 52 | 53 |
54 | 55 | 58 |
59 |
60 | 61 |
62 | 63 | 64 |
65 | 66 |
67 | 68 | 69 |
70 | 71 | {/*
*/} 72 | {/* */} 73 | {/* */} 74 | {/*
*/} 75 | 76 | {/*
*/} 77 | {/* */} 78 | {/* */} 79 | {/*
*/} 80 | 81 |
82 | 83 | 88 |
89 | 90 |
91 | 92 | 93 |
94 | 95 |
96 | 97 | 98 |
99 | 100 | 101 |
102 | 103 |
104 |
105 |
106 | ) 107 | } 108 | 109 | private formSubmit = ( e: React.FormEvent ) => { 110 | e.preventDefault(); 111 | 112 | const form: ISettingsRequest = { 113 | // current_password: (this.refCurrentPassword.current && this.refCurrentPassword.current.value) || '', 114 | // new_password: (this.refNewPassword.current && this.refNewPassword.current.value) || '', 115 | current_password: '', 116 | new_password: '', 117 | country: (this.refCountry.current && this.refCountry.current.value) || '', 118 | affiliation: (this.refAffiliation.current && this.refAffiliation.current.value) || '', 119 | website: (this.refWebsite.current && this.refWebsite.current.value) || '', 120 | avatar: (this.refFile.current && this.refFile.current.files && this.refFile.current.files[0]) || null, 121 | }; 122 | this.refSubmit.current && this.refSubmit.current.setAttribute("disabled", "disabled"); 123 | 124 | (async () => { 125 | let err = null; 126 | try { 127 | const err2 = await SetSettings(form); 128 | if(err2) { 129 | err = ErrorCodes.toHumanMessage(err2); 130 | } 131 | } catch (e) { 132 | err = String(e); 133 | } 134 | if(err !== null) { 135 | this.errorMessage = String(err); 136 | this.successMessage = ""; 137 | 138 | return; 139 | } 140 | 141 | this.errorMessage = ""; 142 | this.successMessage = "Settings updated!"; 143 | 144 | if(this.refFile.current) this.refFile.current.value = ""; 145 | })().finally(() => { 146 | this.refSubmit.current && this.refSubmit.current.removeAttribute("disabled"); 147 | }); 148 | }; 149 | 150 | private onChangeAvatar = (event: React.ChangeEvent) => { 151 | const file = (event.target.files && event.target.files[0]) || null; 152 | if(!file) { 153 | return; 154 | } 155 | resizeImage({ 156 | maxSize: 256, 157 | file: file, 158 | }).then((data: string) => { 159 | this.avatarData = 'data:image/png;base64,' + data; 160 | }).catch((e: Error) => { 161 | this.successMessage = ''; 162 | this.errorMessage = String(e); 163 | 164 | event.target.value = ""; 165 | }); 166 | }; 167 | } 168 | 169 | interface ISettingsPageProps { 170 | store: IRootStore; 171 | } -------------------------------------------------------------------------------- /frontend/src/pages/Team.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {inject, observer} from "mobx-react"; 3 | import {IRootStore} from "@store/index"; 4 | 5 | import Footer from "@components/Footer"; 6 | 7 | import defaultAvatar from "../assets/images/avatar_default_big.png"; 8 | import flagNotFound from "../assets/images/no_country.png"; 9 | 10 | import {Countries} from "@consts/country"; 11 | 12 | import "@styles/team.scss"; 13 | import {GetTeam, ITeamResponse} from "@libs/api"; 14 | import {observable} from "mobx"; 15 | import Loader from "@components/Loader"; 16 | import {formatDate} from "@libs/date"; 17 | 18 | @inject("store") 19 | @observer 20 | export class TeamPage extends React.Component { 21 | @observable teamState: string = "none"; 22 | @observable team: ITeamResponse|null = null; 23 | 24 | async componentDidMount() { 25 | this.fetchTeam(this.props.id); 26 | this.props.store && this.props.store.ctf.fetchScoreboard(); 27 | this.props.store && this.props.store.ctf.fetchTasks(); 28 | } 29 | 30 | async componentDidUpdate(prevProps: Readonly) { 31 | if(prevProps.id !== this.props.id) { 32 | this.fetchTeam(this.props.id); 33 | } 34 | } 35 | 36 | fetchTeam(teamId: number) { 37 | this.teamState = "pending"; 38 | GetTeam(teamId).then(([data, err]) => { 39 | if(err !== null) { 40 | this.teamState = "error"; 41 | console.error('fetch err', err); 42 | return; 43 | } 44 | this.team = data; 45 | this.teamState = "done"; 46 | }).catch((err) => { 47 | console.error('fetch err', err); 48 | this.teamState = "error"; 49 | }) 50 | }; 51 | 52 | render() { 53 | if(!this.props.store) { 54 | return null; 55 | } 56 | 57 | let rankingData = null; 58 | let _teamRanking = 0; 59 | let teamRanking = 0; 60 | if(this.team) { 61 | rankingData = this.props.store.ctf.scoreboard.get(String(this.team.id)); 62 | for(const row of this.props.store.ctf.scoreboard.values()) { 63 | _teamRanking += 1; 64 | if(row.id === this.team.id) { 65 | teamRanking = _teamRanking; 66 | break; 67 | } 68 | } 69 | } 70 | 71 | return ( 72 |
73 |
74 | {this.teamState === "pending" && <>} 75 | {this.teamState === "error" && <>} 76 | {this.teamState === "done" && this.team !== null && (<> 77 |
{this.team.name}
78 |
79 | {""} 80 |
    81 |
  • 82 |

    Ranking

    83 |

    {teamRanking || "-"} / {this.props.store.ctf.scoreboard.size}

    84 |
  • 85 | {this.team.website &&
  • 86 |

    Url

    87 |

    {this.team.website}

    88 |
  • } 89 |
  • 90 |

    Score

    91 |

    {(rankingData && rankingData.api.points) || "-"}

    92 |
  • 93 |
  • 94 |

    Country

    95 |

    96 | {Countries[this.team.country.toUpperCase()]} 97 | {this.team.country && ( 98 | {this.team.country 99 | )} 100 | {!this.team.country && ( 101 | {""} 102 | )} 103 |

    104 |
  • 105 |
  • 106 |

    Affiliation

    107 |

    {this.team.affiliation || "---"}

    108 |
  • 109 |
110 |
111 | 112 |
113 |

Challenges

114 |

Solved {(this.team.task_solved && this.team.task_solved.length) || 0} / {this.props.store.ctf.tasks.size} Total

115 |
116 | 117 |
118 | 119 | 120 | 121 | 122 | 123 | {this.team.task_solved && this.team.task_solved.map(task => { 124 | const taskObj = (this.props.store && this.props.store.ctf.tasks.get(String(task.id))) || null; 125 | return ( 126 | 127 | 128 | 129 | 130 | 131 | ) 132 | })} 133 | 134 |
NameScoreTime
{task.name}{(taskObj && taskObj.api.points) || "-"}{formatDate(new Date(task.created_at))}
135 |
136 | )} 137 | 138 |
139 |
140 |
141 | ) 142 | } 143 | } 144 | 145 | interface ITeamPageProps { 146 | store?: IRootStore; 147 | id: number; 148 | } -------------------------------------------------------------------------------- /frontend/src/pages/Teams.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {inject, observer} from "mobx-react"; 3 | 4 | import Loader from "@components/Loader"; 5 | import Footer from "@components/Footer"; 6 | 7 | import {IRootStore} from "@store/index"; 8 | import {avatarUrl} from "@consts/index"; 9 | 10 | import expandImage from "../assets/images/expand_icon.png"; 11 | import flagNotFound from "../assets/images/no_country.png"; 12 | 13 | import "@styles/teams.scss"; 14 | import Link from "@components/Link"; 15 | 16 | @inject("store") 17 | @observer 18 | export class TeamsPage extends React.Component { 19 | 20 | async componentDidMount() { 21 | await this.props.store.ctf.fetchTeams(); 22 | } 23 | 24 | render( ) { 25 | return ( 26 |
27 |
28 |

Teams

29 | 30 | {this.props.store.ctf.teamsState === "pending" && } 31 | {this.props.store.ctf.teamsState === "error" && } 32 | {this.props.store.ctf.teamsState === "done" && (
33 | 34 | 35 | 36 | 37 | 38 | {this.props.store && Array.from(this.props.store.ctf.teams.values()).map((row, index) => ( 39 | 40 | 41 | 42 | 47 | 48 | 56 | ))} 57 | 58 |
#AvatarTeam nameLinkAffiliationCountry
{row.id} 43 | {row.api.website && ( 44 | 45 | )} 46 | {row.api.affiliation && row.api.affiliation.length ? row.api.affiliation : "---"} 49 | {row.api.country && ( 50 | 51 | )} 52 | {!row.api.country && ( 53 | 54 | )} 55 |
59 |
)} 60 | 61 |
62 |
63 | 64 |
65 | ); 66 | } 67 | 68 | trunc( text: string, length: number ) { 69 | return (text.length > length) ? text.substr(0, length-1) + '...' : text; 70 | } 71 | } 72 | 73 | interface ITeamsPageProps { 74 | store: IRootStore 75 | } -------------------------------------------------------------------------------- /frontend/src/pages/UnavailablePage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export class UnavailablePage extends React.Component<{}, {}> { 4 | render() { 5 | return ( 6 |
7 |
8 |

Unavailable yet

9 |
10 |
11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./Announcements"; 2 | export * from "./Challenges"; 3 | export * from "./Home"; 4 | export * from "./Login"; 5 | export * from "./NotFound"; 6 | export * from "./Register"; 7 | export * from "./Rules"; 8 | export * from "./Scoreboard"; 9 | export * from "./Settings"; 10 | export * from "./Team"; 11 | export * from "./Teams"; -------------------------------------------------------------------------------- /frontend/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { CtfStore } from "@store/CtfStore"; 2 | import { flow, Instance, types } from "mobx-state-tree"; 3 | 4 | export const RootStore = types 5 | .model({ 6 | ctf: types.optional(CtfStore, {}), 7 | }); 8 | 9 | export type IRootStore = Instance; 10 | -------------------------------------------------------------------------------- /frontend/src/styles/announcements.scss: -------------------------------------------------------------------------------- 1 | $width: 650px; 2 | $padding: 40px; 3 | 4 | .page.announcements { 5 | .inner { 6 | width: 100%; 7 | max-width: $width + $padding * 2; 8 | margin-left: auto; 9 | margin-right: auto; 10 | padding: 0 $padding; 11 | 12 | @media only screen and (max-width: 420px) { 13 | padding: 0 ($padding / 2); 14 | } 15 | 16 | .list { 17 | .announcement { 18 | &:not(:first-child) { 19 | &:before { 20 | content: ""; 21 | height: 1px; 22 | background: #3a3a3a; 23 | display: block; 24 | margin: 24px 0; 25 | } 26 | } 27 | 28 | header { 29 | display: flex; 30 | align-items: center; 31 | margin-bottom: 9px; 32 | 33 | h2 { 34 | font-size: 17px; 35 | font-weight: 500; 36 | letter-spacing: 0; 37 | line-height: 41px; 38 | } 39 | 40 | .label { 41 | border: 1px solid #fff; 42 | border-radius: 3px; 43 | padding: 4px 13px; 44 | opacity: .5; 45 | font-size: 8.4px; 46 | letter-spacing: .1px; 47 | text-transform: uppercase; 48 | margin-left: 12px; 49 | } 50 | 51 | .date { 52 | margin-left: auto; 53 | font-size: 13px; 54 | font-weight: 500; 55 | letter-spacing: 0; 56 | } 57 | } 58 | 59 | > .description { 60 | margin-top: 20px; 61 | font-size: 15px; 62 | line-height: 23px; 63 | 64 | * { 65 | margin: initial; 66 | padding: initial; 67 | } 68 | 69 | img { 70 | max-width: 100%; 71 | display: block; 72 | } 73 | 74 | code { 75 | background: #434343; 76 | border-radius: 1px; 77 | letter-spacing: 0.5px; 78 | padding: 2px 6px; 79 | } 80 | 81 | pre { 82 | padding: 16px; 83 | background: #434343; 84 | border-radius: 1px; 85 | 86 | code { 87 | padding: 0; 88 | line-height: 23px; 89 | } 90 | } 91 | 92 | p { 93 | margin-top: 1em; 94 | margin-bottom: 1em; 95 | } 96 | 97 | ul, ol { 98 | margin-top: 1em; 99 | margin-bottom: 1em; 100 | padding-left: 40px; 101 | } 102 | 103 | a { 104 | color: #3aadd4; 105 | text-decoration: underline; 106 | } 107 | 108 | > :last-child { 109 | margin-bottom: 0 !important; 110 | padding-bottom: 0 !important; 111 | } 112 | } 113 | 114 | @media only screen and (max-width: 720px) { 115 | header { 116 | flex-direction: column; 117 | align-items: center; 118 | margin-bottom: 20px; 119 | 120 | h2 { 121 | order: 1; 122 | } 123 | 124 | .label { 125 | order: 0; 126 | margin-left: 0; 127 | } 128 | 129 | .date { 130 | order: 2; 131 | margin-left: initial; 132 | } 133 | } 134 | } 135 | } 136 | } 137 | } 138 | } -------------------------------------------------------------------------------- /frontend/src/styles/login.scss: -------------------------------------------------------------------------------- 1 | $formWidth: 340px; 2 | $padding: 40px; 3 | 4 | .page.login, .page.register { 5 | 6 | .inner { 7 | width: 100%; 8 | max-width: $formWidth + $padding * 2; 9 | margin-left: auto; 10 | margin-right: auto; 11 | padding: 0 $padding; 12 | 13 | @media only screen and (max-width: 420px) { 14 | padding: 0 ($padding / 2); 15 | } 16 | 17 | .reset, .register { 18 | font-weight: 500; 19 | display: block; 20 | margin-top: 20px; 21 | text-align: center; 22 | text-transform: uppercase; 23 | } 24 | 25 | .reset { 26 | color: #3aadd4; 27 | } 28 | 29 | .register { 30 | > span { 31 | color: #3aadd4; 32 | } 33 | } 34 | 35 | .customFile { 36 | cursor: pointer; 37 | 38 | > input[type="file"] { 39 | display: none; 40 | 41 | & + label { 42 | .button { 43 | border: 1px solid #2194bb; 44 | border-radius: 1.8px; 45 | background: transparent; 46 | padding: 7px 12px; 47 | color: #fff; 48 | font-family: 'Rubik', sans-serif; 49 | font-size: 13px; 50 | font-weight: 500; 51 | text-transform: uppercase; 52 | margin-right: 13px; 53 | } 54 | 55 | span { 56 | font-weight: 500; 57 | text-transform: none; 58 | display: inline-block; 59 | } 60 | } 61 | } 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /frontend/src/styles/main.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Rubik:300,400,500,700&display=swap'); 2 | @import url('https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap'); 3 | 4 | @font-face { 5 | font-family: "Downtown"; 6 | src: url("../assets/fonts/Downtown.ttf") format("truetype"), url("../assets/fonts/Downtown.otf") format("opentype"); 7 | } 8 | 9 | * { 10 | margin: 0; 11 | padding: 0; 12 | border: 0; 13 | box-sizing: border-box; 14 | } 15 | 16 | body { 17 | background: #1d1d1d; 18 | font-family: 'Rubik', sans-serif; 19 | font-size: 13px; 20 | color: #eaeaea; 21 | } 22 | 23 | a { 24 | text-decoration: none; 25 | color: inherit; 26 | } 27 | 28 | .mainTitle { 29 | font-family: Roboto, sans-serif; 30 | font-weight: 500; 31 | font-size: 30px; 32 | color: #fff; 33 | text-transform: uppercase; 34 | text-align: center; 35 | margin-bottom: 25px; 36 | 37 | &.normal { 38 | text-transform: none; 39 | } 40 | 41 | &.center { 42 | margin-top: auto; 43 | margin-bottom: auto; 44 | } 45 | 46 | &:before, &:after { 47 | color: #2194bb; 48 | } 49 | 50 | &:before { 51 | content: "<"; 52 | margin-right: 5px; 53 | } 54 | 55 | &:after { 56 | content: ">"; 57 | margin-left: 5px; 58 | } 59 | 60 | @media only screen and (max-width: 490px) { 61 | font-size: 25px; 62 | } 63 | } 64 | 65 | #root { 66 | min-height: 100vh; 67 | 68 | display: flex; 69 | flex-direction: column; 70 | } 71 | 72 | .page { 73 | padding-top: 40px; 74 | flex-grow: 1; 75 | display: flex; 76 | flex-direction: column; 77 | overflow: hidden; 78 | 79 | &:not(.homepage) { 80 | position: relative; 81 | 82 | &:before, &:after { 83 | position: absolute; 84 | top: 0; 85 | content: ""; 86 | height: 100%; 87 | z-index: 1; 88 | } 89 | 90 | &:before { 91 | left: 0; 92 | width: 445px; 93 | 94 | background: url(../assets/images/full_background_LEFT.svg) no-repeat; 95 | background-position-x: -207px; 96 | background-position-y: -100px; 97 | } 98 | 99 | &:after { 100 | right: 0; 101 | width: 385px; 102 | 103 | background: url(../assets/images/full_background_RIGHT.svg) no-repeat; 104 | background-position-x: 82px; 105 | background-position-y: -175px; 106 | } 107 | 108 | @media only screen and (max-width: 1280px) { 109 | &:before, &:after { 110 | display: none; 111 | } 112 | } 113 | } 114 | 115 | 116 | .inner { 117 | flex-grow: 1; 118 | display: flex; 119 | flex-direction: column; 120 | position: relative; 121 | z-index: 5; 122 | } 123 | 124 | .errorMessage, .successMessage { 125 | width: 100%; 126 | padding: 15px; 127 | text-align: center; 128 | margin-bottom: 20px; 129 | border-radius: 5px; 130 | } 131 | 132 | .errorMessage { 133 | background: #c3311f; 134 | } 135 | 136 | .successMessage { 137 | background: #2e9249; 138 | } 139 | 140 | .form-group { 141 | & + .form-group { 142 | margin-top: 15px; 143 | } 144 | 145 | > label { 146 | font-size: 13px; 147 | font-weight: 500; 148 | display: block; 149 | text-transform: uppercase; 150 | margin-bottom: 9px; 151 | 152 | & + input:not([type="file"]), & + select { 153 | width: 100%; 154 | font-family: 'Rubik', sans-serif; 155 | font-size: 15px; 156 | font-weight: 300; 157 | background: #434343; 158 | border-radius: 5px; 159 | padding: 12px; 160 | color: #fff; 161 | opacity: .5; 162 | -webkit-appearance: initial; 163 | } 164 | } 165 | } 166 | 167 | .submitButton { 168 | margin: 25px auto 0; 169 | background: #2194bb; 170 | border-radius: 3px; 171 | color: #fff; 172 | font-family: 'Rubik', sans-serif; 173 | font-size: 15px; 174 | font-weight: 500; 175 | text-transform: uppercase; 176 | padding: 16px 65px 13px; 177 | display: block; 178 | cursor: pointer; 179 | } 180 | 181 | footer.mainFooter { 182 | padding: 40px 0; 183 | font-size: 11px; 184 | font-weight: 400; 185 | opacity: .5; 186 | text-align: center; 187 | 188 | &.sticky { 189 | flex-shrink: 0; 190 | margin-top: auto; 191 | } 192 | } 193 | } 194 | 195 | .loaderBackground { 196 | position: absolute; 197 | top: 0; 198 | left: 0; 199 | width: 100%; 200 | height: 100%; 201 | z-index: 99999; 202 | background: #161616; 203 | opacity: .8; 204 | } 205 | 206 | .loader { 207 | position: absolute; 208 | user-select: none; 209 | overflow-x: hidden; 210 | 211 | &.inline { 212 | position: static; 213 | 214 | &.center { 215 | margin-left: auto; 216 | margin-right: auto; 217 | } 218 | } 219 | 220 | &:not(.inline).center { 221 | top: 50%; 222 | left: 50%; 223 | 224 | transform: translate(-50%, -50%); 225 | } 226 | 227 | &.small { 228 | &:before { 229 | width: 52px; 230 | height: 52px; 231 | } 232 | 233 | > p { 234 | font-size: 11px; 235 | } 236 | } 237 | 238 | &:before { 239 | content: ""; 240 | display: block; 241 | margin: 0 auto; 242 | width: 64px; 243 | height: 64px; 244 | background: url(../assets/images/loader_logo.png) no-repeat center / cover; 245 | animation: beat 1.2s infinite cubic-bezier(0.215, 0.61, 0.355, 1); 246 | } 247 | 248 | > p { 249 | font-size: 17px; 250 | font-weight: 500; 251 | color: #fff; 252 | text-align: center; 253 | text-transform: uppercase; 254 | margin-top: 10px; 255 | margin-bottom: 0; 256 | 257 | &.small { 258 | font-size: 11px; 259 | } 260 | } 261 | } 262 | 263 | @keyframes beat { 264 | 0% { 265 | transform: scale(0.95); 266 | } 267 | 5% { 268 | transform: scale(1.1); 269 | } 270 | 39% { 271 | transform: scale(0.85); 272 | } 273 | 45% { 274 | transform: scale(1); 275 | } 276 | 60% { 277 | transform: scale(0.95); 278 | } 279 | 100% { 280 | transform: scale(0.9); 281 | } 282 | } -------------------------------------------------------------------------------- /frontend/src/styles/modal.scss: -------------------------------------------------------------------------------- 1 | 2 | .modalBackground { 3 | position: fixed; 4 | top: 0; 5 | left: 0; 6 | right: 0; 7 | bottom: 0; 8 | background: rgba(0,0,0, .7); 9 | z-index: 9998; 10 | display: flex; 11 | flex-direction: column; 12 | align-items: center; 13 | justify-content: center; 14 | 15 | .modal { 16 | max-height: 90%; 17 | overflow: hidden; 18 | width: 100%; 19 | max-width: 650px; 20 | padding: 20px 20px 40px; 21 | background: #252525; 22 | box-shadow: 0 2px 16px 0 rgba(0,0,0,0.5); 23 | 24 | position: relative; 25 | 26 | .close { 27 | position: absolute; 28 | top: 10px; 29 | right: 4px; 30 | cursor: pointer; 31 | opacity: .5; 32 | width: 20px; 33 | height: 20px; 34 | 35 | &:before { 36 | content: "X"; 37 | font-family: Roboto, sans-serif; 38 | font-size: 17px; 39 | font-weight: 700; 40 | } 41 | } 42 | 43 | .body { 44 | height: 100%; 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /frontend/src/styles/navbar.scss: -------------------------------------------------------------------------------- 1 | $padding: 40px; 2 | 3 | .mainNavbar { 4 | padding: 0 $padding; 5 | color: #fff; 6 | font-size: 13px; 7 | font-weight: 500; 8 | text-transform: uppercase; 9 | border: 1px solid #3c3c3c; 10 | z-index: 10; 11 | position: relative; 12 | 13 | @media only screen and (max-width: 420px) { 14 | padding: 0 ($padding / 2); 15 | } 16 | 17 | .inner { 18 | max-width: 1280px; 19 | margin-left: auto; 20 | margin-right: auto; 21 | 22 | display: flex; 23 | justify-content: space-between; 24 | align-items: center; 25 | 26 | .rwdLogo, .rwdMenu { 27 | display: none; 28 | } 29 | 30 | ul { 31 | list-style: none; 32 | display: flex; 33 | align-items: center; 34 | padding: 16px; 35 | 36 | li { 37 | position: relative; 38 | 39 | &:not(:first-child) { 40 | margin-left: 20px; 41 | } 42 | 43 | &.logo { 44 | a { 45 | width: 58px; 46 | height: 33px; 47 | background: url(../assets/images/logo.svg) no-repeat; 48 | } 49 | } 50 | 51 | &.active { 52 | color: #3aadd4 53 | } 54 | 55 | a { 56 | display: block; 57 | } 58 | 59 | .badge { 60 | position: absolute; 61 | top: -8px; 62 | right: -18px; 63 | background: #3aadd4; 64 | border-radius: 3px; 65 | padding: 4px 4px 2px; 66 | font-size: 8px; 67 | font-weight: 600; 68 | color: #161616; 69 | letter-spacing: 0.1px; 70 | text-align: center; 71 | min-width: 15px; 72 | } 73 | } 74 | } 75 | } 76 | 77 | @media only screen and (max-width: 980px) { 78 | .inner { 79 | height: 65px; 80 | justify-content: center; 81 | 82 | .rwdLogo { 83 | display: block; 84 | width: 58px; 85 | height: 33px; 86 | background: url(../assets/images/logo.svg) no-repeat center/cover; 87 | margin-left: auto; 88 | } 89 | 90 | .rwdMenu { 91 | display: block; 92 | width: 40px; 93 | height: 40px; 94 | background: url(../assets/images/rwd_icon.png) no-repeat center/cover; 95 | margin-left: auto; 96 | cursor: pointer; 97 | } 98 | 99 | ul { 100 | display: none; 101 | } 102 | 103 | ul.main.active { 104 | display: flex; 105 | flex-direction: column; 106 | position: absolute; 107 | right: 0; 108 | top: 65px; 109 | padding: 0; 110 | 111 | li { 112 | &.logo { display: none;} 113 | 114 | background: #1d2124; 115 | padding: 10px 15px; 116 | margin-left: 0; 117 | width: 100%; 118 | } 119 | } 120 | } 121 | } 122 | 123 | @media only screen and (max-width: 520px) { 124 | ul.main.active { 125 | width: 100%; 126 | left: 0; 127 | right: auto; 128 | } 129 | } 130 | } -------------------------------------------------------------------------------- /frontend/src/styles/rules.scss: -------------------------------------------------------------------------------- 1 | $width: 712px; 2 | $padding: 40px; 3 | 4 | .page.rules { 5 | .inner { 6 | width: 100%; 7 | max-width: $width + $padding * 2; 8 | margin-left: auto; 9 | margin-right: auto; 10 | padding: 0 $padding; 11 | 12 | @media only screen and (max-width: 420px) { 13 | padding: 0 ($padding / 2); 14 | } 15 | 16 | ol { 17 | font-size: 17px; 18 | line-height: 21px; 19 | margin-left: 1em; 20 | 21 | li { 22 | &:not(:first-child) { 23 | margin-top: 20px; 24 | } 25 | 26 | padding-left: 10px; 27 | } 28 | } 29 | 30 | a { 31 | color: #3aadd4; 32 | font-weight: 500; 33 | } 34 | 35 | b { 36 | font-weight: 500; 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /frontend/src/styles/scoreboard.scss: -------------------------------------------------------------------------------- 1 | $width: 1210px; 2 | $padding: 40px; 3 | 4 | .page.scoreboard { 5 | .inner { 6 | width: 100%; 7 | max-width: $width + $padding * 2; 8 | margin-left: auto; 9 | margin-right: auto; 10 | padding: 0 $padding; 11 | align-items: center; 12 | 13 | @media only screen and (max-width: 420px) { 14 | padding: 0 ($padding / 2); 15 | } 16 | 17 | h4 { 18 | color: #3aadd4; 19 | font-size: 17px; 20 | font-weight: 500; 21 | text-align: center; 22 | margin-bottom: 20px; 23 | margin-top: -20px; 24 | } 25 | 26 | .unfreeze { 27 | color: grey; 28 | } 29 | 30 | .time{ 31 | transform: scale(0.8); 32 | margin-bottom: 20px; 33 | } 34 | 35 | .time > div:not(.spacer) { 36 | font-size: 30px !important; 37 | } 38 | .time .spacer { 39 | font-size: 30px !important; 40 | margin-left: 0 !important; 41 | margin-right: 0 !important; 42 | } 43 | 44 | @media only screen and (max-width: 420px) { 45 | padding: 0 $padding / 2; 46 | } 47 | 48 | .table { 49 | width: 100%; 50 | overflow: auto; 51 | 52 | table { 53 | width: $width - 2; 54 | border-collapse: collapse; 55 | 56 | thead { 57 | tr:nth-child(1) { 58 | th { 59 | padding: 8px 0; 60 | font-size: 13px; 61 | font-weight: 400; 62 | 63 | > div { 64 | margin-bottom: -15px; 65 | margin-left: -50%; 66 | } 67 | } 68 | } 69 | 70 | tr:nth-child(2) { 71 | th { 72 | height: 80px; 73 | white-space: nowrap; 74 | font-size: 10px; 75 | width: 40px; 76 | font-weight: 400; 77 | color: #eaeaea; 78 | 79 | &.light { 80 | > div > span { 81 | &:after { 82 | background: #8e8e8e !important; 83 | } 84 | } 85 | } 86 | 87 | > div { 88 | transform: translate(-43px, -3px) rotate(45deg); 89 | width: 40px; 90 | position: relative; 91 | 92 | &:before { 93 | content: ""; 94 | position: absolute; 95 | width: 1px; 96 | height: 40px; 97 | background: #535353; 98 | transform: translate(-19px, -7px) rotate(45deg); 99 | } 100 | 101 | > span { 102 | padding: 8px 4px; 103 | display: block; 104 | width: 80px; 105 | position: relative; 106 | } 107 | } 108 | 109 | &:nth-child(n + 2) { 110 | > div > span { 111 | &:after { 112 | content: ""; 113 | position: absolute; 114 | bottom: 0; 115 | left: -12px; 116 | height: 1px; 117 | width: 100%; 118 | background: #535353; 119 | } 120 | } 121 | } 122 | 123 | &:last-child { 124 | > div > span { 125 | &:before { 126 | content: ""; 127 | position: absolute; 128 | top: -1px; 129 | right: -15px; 130 | height: 1px; 131 | width: 100%; 132 | background: #535353; 133 | } 134 | } 135 | } 136 | } 137 | } 138 | 139 | tr:nth-child(3) { 140 | th { 141 | border-top: 1px solid #8e8e8e; 142 | border-bottom: 1px solid #8e8e8e; 143 | border-left: 1px solid #535353; 144 | text-align: center; 145 | font-size: 11px; 146 | font-weight: 500; 147 | color: #3aadd4; 148 | padding: 4px 8.5px; 149 | 150 | &.light { 151 | border-left-color: #8e8e8e; 152 | } 153 | 154 | &:nth-child(1) { 155 | width: 45px; 156 | border-left: none; 157 | } 158 | 159 | &:nth-child(3) { 160 | width: 60px; 161 | } 162 | 163 | &:nth-child(-n+4) { 164 | font-size: 10px; 165 | color: #eaeaea; 166 | } 167 | 168 | &:nth-child(4) { 169 | width: 53px; 170 | } 171 | 172 | &:nth-child(5n) { 173 | width: 40px; 174 | } 175 | 176 | &:last-child { 177 | border-right: 1px solid #535353; 178 | } 179 | } 180 | } 181 | 182 | } 183 | 184 | tbody { 185 | tr { 186 | td { 187 | font-size: 11px; 188 | color: #eaeaea; 189 | text-align: center; 190 | border-left: 1px solid #535353; 191 | border-bottom: 1px solid #535353; 192 | height: 30px; 193 | padding: 7px 10px; 194 | 195 | img:not(.avatar) { 196 | max-width: 15px; 197 | max-height: 20px; 198 | } 199 | 200 | .avatar { 201 | max-height: 35px; 202 | display: inline-block; 203 | vertical-align: middle; 204 | margin-right: 8px; 205 | } 206 | 207 | &.left { 208 | text-align: left; 209 | } 210 | 211 | &.light { 212 | border-left-color: #8e8e8e;; 213 | } 214 | 215 | &:first-child { 216 | border-left: none; 217 | } 218 | 219 | &:last-child { 220 | border-right: 1px solid #535353; 221 | } 222 | } 223 | 224 | } 225 | } 226 | } 227 | } 228 | 229 | .more { 230 | cursor: pointer; 231 | padding: 16px 48px; 232 | text-transform: uppercase; 233 | font-size: 15px; 234 | font-weight: 500; 235 | border: 2px solid #3aadd4; 236 | border-radius: 3px; 237 | margin-top: 40px; 238 | } 239 | } 240 | } -------------------------------------------------------------------------------- /frontend/src/styles/settings.scss: -------------------------------------------------------------------------------- 1 | $formWidth: 340px; 2 | $padding: 40px; 3 | 4 | .page.settings { 5 | .inner { 6 | width: 100%; 7 | max-width: $formWidth + $padding * 2; 8 | margin-left: auto; 9 | margin-right: auto; 10 | padding: 0 $padding; 11 | 12 | @media only screen and (max-width: 420px) { 13 | padding: 0 ($padding / 2); 14 | } 15 | 16 | .avatar { 17 | text-align: center; 18 | 19 | .customFile { 20 | > input[type="file"] { 21 | display: none; 22 | 23 | & + label { 24 | img { 25 | cursor: pointer; 26 | max-width: 128px; 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /frontend/src/styles/team.scss: -------------------------------------------------------------------------------- 1 | $tableWidth: 420px; 2 | $padding: 40px; 3 | 4 | .page.team { 5 | .inner { 6 | width: 100%; 7 | max-width: $tableWidth + $padding * 2; 8 | margin-left: auto; 9 | margin-right: auto; 10 | padding: 0 $padding; 11 | 12 | @media only screen and (max-width: 420px) { 13 | padding: 0 ($padding / 2); 14 | } 15 | 16 | header { 17 | display: flex; 18 | justify-content: space-between; 19 | align-items: flex-start; 20 | 21 | > img { 22 | max-width: 160px; 23 | } 24 | 25 | > ul { 26 | flex: 0 0 220px; 27 | display: flex; 28 | flex-wrap: wrap; 29 | list-style: none; 30 | 31 | li { 32 | flex: 1; 33 | margin-bottom: 20px; 34 | 35 | h2 { 36 | font-size: 13px; 37 | font-weight: 500; 38 | color: #fff; 39 | margin-bottom: 3px; 40 | text-transform: uppercase; 41 | } 42 | 43 | p { 44 | color: #fff; 45 | font-size: 15px; 46 | display: flex; 47 | align-items: center; 48 | min-height: 27px; 49 | 50 | &.blue { 51 | color: #3aadd4; 52 | } 53 | 54 | img { 55 | width: 50px; 56 | height: 27px; 57 | margin-left: 15px; 58 | } 59 | 60 | a { 61 | text-decoration: underline; 62 | color: inherit; 63 | } 64 | } 65 | 66 | &.ranking, &.score { 67 | flex: 40%; 68 | } 69 | 70 | &.url, &.country { 71 | flex: 60%; 72 | } 73 | 74 | &.affiliation { 75 | flex: 100%; 76 | margin-bottom: 0; 77 | } 78 | } 79 | } 80 | 81 | @media only screen and (max-width: 480px) { 82 | flex-direction: column; 83 | align-items: center; 84 | 85 | > img { 86 | margin-bottom: 20px; 87 | } 88 | 89 | > ul { 90 | li:not(.affiliation) { 91 | flex-basis: 50% !important; 92 | } 93 | } 94 | } 95 | 96 | @media only screen and (max-width: 380px) { 97 | > ul { 98 | li:not(.affiliation) { 99 | flex-basis: 100% !important; 100 | } 101 | } 102 | } 103 | } 104 | 105 | .result { 106 | margin-top: 20px; 107 | 108 | h2, h4 { 109 | font-size: 17px; 110 | font-weight: 500; 111 | text-align: center; 112 | } 113 | 114 | h2 { 115 | color: #3aadd4; 116 | margin-bottom: 10px; 117 | } 118 | 119 | h4 { 120 | span { 121 | color: #3aadd4; 122 | } 123 | } 124 | } 125 | 126 | .table { 127 | margin-top: 20px; 128 | width: 100%; 129 | overflow: auto; 130 | 131 | table { 132 | margin: 0 auto; 133 | 134 | thead { 135 | th { 136 | font-size: 12px; 137 | font-weight: 500; 138 | color: #fff; 139 | text-align: left; 140 | padding-bottom: 8px; 141 | 142 | &:not(:first-child) { 143 | border-left: 1px solid #3aadd4; 144 | } 145 | 146 | &:nth-child(1) { 147 | padding-right: 24.5px; 148 | } 149 | 150 | &:nth-child(2) { 151 | width: 75px; 152 | text-align: center; 153 | } 154 | 155 | &:nth-child(3) { 156 | padding-left: 24.5px; 157 | } 158 | } 159 | } 160 | 161 | tbody { 162 | td { 163 | border-top: 1px solid #3aadd4; 164 | padding: 8px 0; 165 | 166 | &:not(:first-child) { 167 | border-left: 1px solid #3aadd4; 168 | } 169 | 170 | &:nth-child(1) { 171 | padding-right: 24.5px; 172 | } 173 | 174 | &:nth-child(2) { 175 | width: 75px; 176 | text-align: center; 177 | } 178 | 179 | &:nth-child(3) { 180 | padding-left: 24.5px; 181 | } 182 | } 183 | } 184 | } 185 | } 186 | } 187 | } -------------------------------------------------------------------------------- /frontend/src/styles/teams.scss: -------------------------------------------------------------------------------- 1 | $tableWidth: 800px; 2 | $padding: 40px; 3 | 4 | .page.teams { 5 | .inner { 6 | width: 100%; 7 | max-width: $tableWidth + $padding * 2; 8 | margin-left: auto; 9 | margin-right: auto; 10 | padding: 0 $padding; 11 | 12 | @media only screen and (max-width: 420px) { 13 | padding: 0 ($padding / 2); 14 | } 15 | 16 | .table { 17 | width: 100%; 18 | overflow: auto; 19 | 20 | table { 21 | width: $tableWidth; 22 | 23 | thead { 24 | th { 25 | font-size: 17px; 26 | font-weight: 500; 27 | padding: 11px 0; 28 | } 29 | } 30 | 31 | tbody { 32 | td { 33 | font-size: 17px; 34 | font-weight: 500; 35 | padding: 11px 0; 36 | text-align: center; 37 | 38 | .avatar { 39 | max-height: 45px; 40 | } 41 | 42 | .flag { 43 | width: 50px; 44 | height: 27px; 45 | } 46 | } 47 | } 48 | } 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /frontend/src/styles/timer.scss: -------------------------------------------------------------------------------- 1 | .time { 2 | display: flex; 3 | justify-content: center; 4 | align-items: flex-start; 5 | 6 | > div:not(.spacer) { 7 | display: flex; 8 | flex-direction: column; 9 | align-items: center; 10 | text-align: center; 11 | font-weight: 700; 12 | font-size: 50px; 13 | min-width: 70px; 14 | 15 | &:after { 16 | content: attr(data-type); 17 | text-align: justify; 18 | font-size: 13px; 19 | font-weight: 500; 20 | text-transform: uppercase; 21 | } 22 | } 23 | 24 | .spacer { 25 | font-weight: 700; 26 | font-size: 50px; 27 | margin-left: 7px; 28 | margin-right: 7px; 29 | 30 | &:before { 31 | display: block; 32 | content: ":"; 33 | } 34 | } 35 | 36 | @media only screen and (max-width: 430px) { 37 | > div { 38 | &:not(.spacer) { 39 | font-size: 32px; 40 | 41 | &:after { 42 | font-size: 11px; 43 | } 44 | 45 | min-width: initial; 46 | } 47 | 48 | &.spacer { 49 | margin-left: 0; 50 | margin-right: 0; 51 | font-size: 32px; 52 | } 53 | 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /frontend/src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | // webpack Define plugin 2 | // tslint:disable 3 | declare const __IS_DEV__: boolean; 4 | -------------------------------------------------------------------------------- /frontend/src/types/import-png.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.png" { 2 | const value: any; 3 | export default value; 4 | } -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "lib": ["dom", "es2016", "es2017.object", "es2019"], 5 | "experimentalDecorators": true, 6 | "sourceMap": true, 7 | "strict": true, 8 | "strictNullChecks": true, // https://github.com/mobxjs/mobx-state-tree/issues/1208 9 | "strictFunctionTypes": true, 10 | "noImplicitAny": true, 11 | "noImplicitReturns": true, 12 | "noImplicitThis": true, 13 | "moduleResolution": "node", 14 | "module": "es6", 15 | "target": "es6", 16 | "jsx": "react", 17 | "baseUrl": "src", 18 | "paths": { 19 | "@styles/*": ["styles/*"], 20 | "@store/*": ["store/*"], 21 | "@models/*": ["models/*"], 22 | "@consts/*": ["consts/*"], 23 | "@components/*": ["components/*"], 24 | "@containers/*": ["containers/*"], 25 | "@libs/*": ["libs/*"] 26 | }, 27 | "allowSyntheticDefaultImports": true 28 | }, 29 | "include": [ 30 | "./src/**/*" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /frontend/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rulesDirectory": ["./src/"], 4 | "rules": { 5 | "max-line-length": { 6 | "options": [120] 7 | }, 8 | "max-classes-per-file": false, 9 | "new-parens": true, 10 | "no-arg": true, 11 | "no-bitwise": true, 12 | "object-literal-sort-keys": false, 13 | "no-conditional-assignment": true, 14 | "no-consecutive-blank-lines": false, 15 | "no-unused-expression": [true, "allow-fast-null-checks"], 16 | "no-console": { 17 | "severity": "warning", 18 | "options": ["debug", "info", "log", "time", "timeEnd", "trace"] 19 | } 20 | }, 21 | "jsRules": { 22 | "max-line-length": { 23 | "options": [150] 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /frontend/webpack.config.js/index.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | 4 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 5 | const MiniCSSExtractPlugin = require("mini-css-extract-plugin"); 6 | const TsConfigPathsPlugin = require("tsconfig-paths-webpack-plugin"); 7 | const CopyPlugin = require('copy-webpack-plugin'); 8 | 9 | const config = { 10 | entry: { 11 | desktop: "./src/index.tsx", 12 | }, 13 | // 14 | // output: { 15 | // filename: "bundle.js", 16 | // path: __dirname + "/dist" 17 | // }, 18 | 19 | // output: { 20 | // path: __dirname + "/dist", 21 | // filename: `[name]${!IS_DEV ? '.[hash].min' : ''}.js`, 22 | // // publicPath: paths.publicPath, 23 | // chunkFilename: `${IS_DEV ? '[name].' : ''}[chunkhash]${!IS_DEV ? '.min' : ''}.js`, 24 | // }, 25 | 26 | output: { 27 | path: path.join(__dirname, '..', 'dist'), 28 | filename: `[name].[hash].js`, 29 | publicPath: '/static/', 30 | chunkFilename: `[name].[chunkhash].js`, 31 | }, 32 | 33 | devServer: { 34 | contentBase: "./public", 35 | compress: true, 36 | port: 9000, 37 | historyApiFallback: true 38 | }, 39 | 40 | plugins: [ 41 | new HtmlWebpackPlugin({ 42 | inject: true, 43 | template: "./public/index.html" 44 | }), 45 | new MiniCSSExtractPlugin({ 46 | filename: "[name].[hash].css", 47 | chunkFilename: "[id].[hash].css" 48 | }), 49 | new CopyPlugin([ 50 | { from: './public/favicons', to: 'favicons' }, 51 | ]), 52 | ], 53 | 54 | // Enable sourcemaps for debugging webpack's output. 55 | devtool: "source-map", 56 | 57 | resolve: { 58 | // Add '.ts' and '.tsx' as resolvable extensions. 59 | extensions: [".ts", ".tsx", ".js", ".json"], 60 | plugins: [ 61 | new TsConfigPathsPlugin({configFile: path.join(__dirname, '..', 'tsconfig.json')}), 62 | ] 63 | }, 64 | 65 | module: { 66 | rules: [ 67 | // All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'. 68 | { 69 | test: /\.tsx?$/, 70 | loader: "ts-loader", 71 | }, 72 | { 73 | test: /\.(sa|sc|c)ss$/, 74 | use: [ 75 | { loader: MiniCSSExtractPlugin.loader }, 76 | { loader: "css-loader" }, 77 | { loader: "sass-loader" } 78 | ] 79 | }, 80 | { 81 | loader: "file-loader", 82 | exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/, /\.scss$/, /\.css$/ ], 83 | options: { 84 | name: 'media/[hash].[ext]', 85 | }, 86 | }, 87 | 88 | // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'. 89 | { enforce: "pre", test: /\.js$/, loader: "source-map-loader" } 90 | ] 91 | }, 92 | 93 | // // When importing a module whose path matches one of the following, just 94 | // // assume a corresponding global variable exists and use that instead. 95 | // // This is important because it allows us to avoid bundling all of our 96 | // // dependencies, which allows browsers to cache those libraries between builds. 97 | // externals: { 98 | // "react": "React", 99 | // "react-dom": "ReactDOM" 100 | // } 101 | 102 | optimization: { 103 | namedModules: true, 104 | noEmitOnErrors: true, 105 | splitChunks: { 106 | cacheGroups: { 107 | vendors: { 108 | test(module) { 109 | return /[\\/]node_modules[\\/]/.test(module.context); 110 | }, 111 | name: 'vendors', 112 | chunks: 'all', 113 | priority: -10, 114 | enforce: true, 115 | }, 116 | }, 117 | } 118 | } 119 | }; 120 | 121 | module.exports = (env, argv) => { 122 | const IS_DEV = argv.mode === 'development'; 123 | const plugins = []; 124 | 125 | plugins.push(new webpack.DefinePlugin({ 126 | __IS_DEV__: IS_DEV ? 'true' : 'false', 127 | })); 128 | 129 | config.plugins = config.plugins.concat(plugins); 130 | 131 | return config; 132 | }; 133 | -------------------------------------------------------------------------------- /nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:13 as builder 2 | WORKDIR /frontend 3 | COPY ./frontend/package.json /frontend/package.json 4 | COPY ./frontend/yarn.lock /frontend/yarn.lock 5 | RUN yarn 6 | COPY ./frontend /frontend 7 | RUN yarn run build:prod 8 | 9 | 10 | FROM openresty/openresty:alpine 11 | RUN rm /etc/nginx/conf.d/default.conf 12 | COPY ./nginx/web.conf /etc/nginx/conf.d/web.conf 13 | COPY ./nginx/admin.conf /etc/nginx/conf.d/admin.conf 14 | COPY --from=builder /frontend/dist /frontend 15 | -------------------------------------------------------------------------------- /nginx/admin.conf: -------------------------------------------------------------------------------- 1 | 2 | log_format admin_log '$remote_addr - $upstream_cache_status [$time_local] "$request" ' 3 | '$status $request_time $body_bytes_sent ' 4 | '"$http_user_agent" "$http_x_forwarded_for"'; 5 | 6 | upstream admin { 7 | keepalive 100; 8 | server admin:8080; 9 | } 10 | 11 | server { 12 | listen 82; 13 | server_name _; 14 | 15 | gzip on; 16 | gzip_min_length 1000; 17 | gunzip on; 18 | 19 | access_log /dev/stdout admin_log; 20 | error_log /dev/stdout warn; 21 | 22 | location / { 23 | try_files $uri @backend; 24 | } 25 | 26 | # TODO: static django for prod 27 | # location /static/ { 28 | # alias /admin/static/; 29 | # try_files $uri =404; 30 | # } 31 | 32 | location @backend { 33 | proxy_read_timeout 30; 34 | proxy_connect_timeout 30; 35 | 36 | proxy_http_version 1.1; 37 | proxy_set_header Connection ""; 38 | proxy_set_header X-Forwarded-For $remote_addr; 39 | proxy_pass http://admin; 40 | } 41 | } -------------------------------------------------------------------------------- /nginx/web.conf: -------------------------------------------------------------------------------- 1 | 2 | upstream backend { 3 | keepalive 100; 4 | server web:8080; 5 | } 6 | 7 | map $uri $cache_enable { 8 | default "off"; 9 | /api/v1/tasks "mycache"; 10 | /api/v1/scoreboard "mycache"; 11 | /api/v1/announcements "mycache"; 12 | /api/v1/info "mycache"; 13 | # /api/v1/team "mycache"; 14 | /api/v1/teams "mycache"; 15 | '~^/api/v1/team_info/[0-9]+$' "mycache"; 16 | '~^/api/v1/task_solvers/[0-9]+$' "mycache"; 17 | '~^/avatar/' "mycache"; 18 | '~^/api/v1/team/avatar/' "mycache"; 19 | } 20 | 21 | map $uri $cache_key { 22 | default "$uri"; 23 | /api/v1/tasks "$uri"; 24 | /api/v1/scoreboard "$uri"; 25 | /api/v1/announcements "$uri"; 26 | /api/v1/info "$uri"; 27 | # /api/v1/team "$uri$cookie_session"; 28 | /api/v1/teams "$uri"; 29 | '~^/api/v1/team_info/[0-9]+$' "$uri"; 30 | '~^/api/v1/task_solvers/[0-9]+$' "$uri"; 31 | '~^/avatar/' "$uri"; 32 | '~^/api/v1/team/avatar/' "$uri"; 33 | } 34 | 35 | map $uri $cache_in_browser { 36 | default "no-cache"; 37 | /api/v1/tasks "public, max-age=30"; 38 | /api/v1/ranking "public, max-age=30"; 39 | /api/v1/announcements "public, max-age=30"; 40 | /api/v1/info "public, max-age=30"; 41 | /api/v1/team "public, max-age=1"; 42 | /api/v1/teams "public, max-age=30"; 43 | '~^/api/v1/team_info/[0-9]+$' "public, max-age=30"; 44 | '~^/api/v1/task_solvers/[0-9]+$' "public, max-age=30"; 45 | '~^/avatar/' ""; # this header is coming from app :) 46 | '~^/api/v1/team/avatar/' ""; # this header is coming from app :) 47 | } 48 | 49 | map $uri $limit_by_uri { 50 | default $binary_remote_addr; 51 | '~^/api/v1/team/avatar/' ""; 52 | # default ""; 53 | # /api/v1/team/login $binary_remote_addr; 54 | # /api/v1/team/register $binary_remote_addr; 55 | } 56 | 57 | limit_req_zone $limit_by_uri zone=req_zone:10m rate=5r/s; 58 | 59 | proxy_cache_path /tmp/cache levels=1:2 keys_zone=mycache:10m max_size=10g inactive=60m use_temp_path=off; 60 | 61 | log_format main_log '$remote_addr - $upstream_cache_status [$time_local] "$request" ' 62 | '$status $request_time $body_bytes_sent ' 63 | '"$http_user_agent" "$http_x_forwarded_for" "$http_cf_connecting_ip"'; 64 | 65 | server_tokens off; 66 | 67 | # cloudflare ip 68 | set_real_ip_from 103.21.244.0/22; 69 | set_real_ip_from 103.22.200.0/22; 70 | set_real_ip_from 103.31.4.0/22; 71 | set_real_ip_from 104.16.0.0/12; 72 | set_real_ip_from 108.162.192.0/18; 73 | set_real_ip_from 131.0.72.0/22; 74 | set_real_ip_from 141.101.64.0/18; 75 | set_real_ip_from 162.158.0.0/15; 76 | set_real_ip_from 172.64.0.0/13; 77 | set_real_ip_from 173.245.48.0/20; 78 | set_real_ip_from 188.114.96.0/20; 79 | set_real_ip_from 190.93.240.0/20; 80 | set_real_ip_from 197.234.240.0/22; 81 | set_real_ip_from 198.41.128.0/17; 82 | set_real_ip_from 2400:cb00::/32; 83 | set_real_ip_from 2606:4700::/32; 84 | set_real_ip_from 2803:f800::/32; 85 | set_real_ip_from 2405:b500::/32; 86 | set_real_ip_from 2405:8100::/32; 87 | set_real_ip_from 2c0f:f248::/32; 88 | set_real_ip_from 2a06:98c0::/29; 89 | real_ip_header CF-Connecting-IP; 90 | 91 | server { 92 | listen 81; 93 | server_name _; 94 | 95 | gzip on; 96 | gzip_min_length 1000; 97 | gunzip on; 98 | if_modified_since off; 99 | 100 | access_log /dev/stdout main_log; 101 | error_log /dev/stdout warn; 102 | 103 | proxy_cache_methods GET HEAD; 104 | proxy_cache_valid 200 30s; 105 | proxy_cache_valid 404 1m; 106 | proxy_cache_valid 500 502 503 401 403 1s; 107 | proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504; 108 | 109 | location = /android-chrome-192x192.png { 110 | rewrite (.*) /static/favicons/$1 last; 111 | } 112 | location = /android-chrome-512x512.png { 113 | rewrite (.*) /static/favicons/$1 last; 114 | } 115 | location = /apple-touch-icon.png { 116 | rewrite (.*) /static/favicons/$1 last; 117 | } 118 | location = /favicon.ico { 119 | rewrite (.*) /static/favicons/$1 last; 120 | } 121 | location = /favicon-16x16.png { 122 | rewrite (.*) /static/favicons/$1 last; 123 | } 124 | location = /favicon-32x32.png { 125 | rewrite (.*) /static/favicons/$1 last; 126 | } 127 | location = /site.webmanifest { 128 | rewrite (.*) /static/favicons/$1 last; 129 | } 130 | 131 | location / { 132 | add_header Cache-Control "public, max-age=600" always; 133 | add_header Last-Modified ""; 134 | etag off; 135 | root /frontend; 136 | try_files /index.html =404; 137 | } 138 | 139 | location /static/ { 140 | add_header Cache-Control "public, max-age=3600" always; 141 | add_header Last-Modified ""; 142 | etag off; 143 | alias /frontend/; 144 | try_files $uri =404; 145 | } 146 | 147 | location /avatar/ { 148 | rewrite /avatar/(.*) /api/v1/team/avatar/$1 break; 149 | try_files $uri @backend; 150 | } 151 | 152 | location = /api/v1/announcements { 153 | try_files $uri @backend; 154 | } 155 | location = /api/v1/scoreboard { 156 | try_files $uri @backend; 157 | } 158 | location = /api/v1/tasks { 159 | try_files $uri @backend; 160 | } 161 | location = /api/v1/info { 162 | try_files $uri @backend; 163 | } 164 | location = /api/v1/teams { 165 | try_files $uri @backend; 166 | } 167 | location ~ ^/api/v1/team_info/[0-9]+$ { 168 | try_files $uri @backend; 169 | } 170 | location ~ ^/api/v1/task_solvers/[0-9]+$ { 171 | try_files $uri @backend; 172 | } 173 | location = /api/v1/team/register { 174 | try_files $uri @backend; 175 | } 176 | location = /api/v1/team/login { 177 | try_files $uri @backend; 178 | } 179 | location = /api/v1/team/logout { 180 | try_files $uri @backend; 181 | } 182 | location = /api/v1/team { 183 | try_files $uri @backend; 184 | } 185 | location = /api/v1/team/settings { 186 | try_files $uri @backend; 187 | } 188 | location = /api/v1/flag/submit { 189 | try_files $uri @backend; 190 | } 191 | 192 | error_page 555 = @rate_limit; 193 | location @rate_limit { 194 | return 429 'too_many_requests'; 195 | } 196 | 197 | location @backend { 198 | proxy_cache $cache_enable; 199 | proxy_cache_key "$cache_key"; 200 | if ($cache_in_browser != "") { 201 | add_header Cache-Control "$cache_in_browser" always; 202 | } 203 | 204 | limit_req zone=req_zone burst=10 nodelay; 205 | limit_req_status 555; 206 | 207 | proxy_read_timeout 10; 208 | proxy_connect_timeout 10; 209 | 210 | proxy_http_version 1.1; 211 | proxy_set_header Connection ""; 212 | proxy_set_header Accept-Encoding "gzip"; 213 | proxy_set_header X-Real-IP $remote_addr; 214 | proxy_pass http://backend; 215 | } 216 | } -------------------------------------------------------------------------------- /web/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.13.4-alpine3.10 as builder 2 | WORKDIR /code/ 3 | COPY . . 4 | RUN go build -v ./cmd/main/ 5 | 6 | FROM alpine:3.10 7 | RUN apk add --no-cache curl 8 | 9 | WORKDIR /root/ 10 | COPY --from=builder /code/main . 11 | CMD ["./main"] 12 | -------------------------------------------------------------------------------- /web/actions/country.go: -------------------------------------------------------------------------------- 1 | package actions 2 | 3 | var countryCodes = map[string]string{ 4 | "": "", 5 | "AF":"Afghanistan", 6 | "AX":"Åland Islands", 7 | "AL":"Albania", 8 | "DZ":"Algeria", 9 | "AS":"American Samoa", 10 | "AD":"Andorra", 11 | "AO":"Angola", 12 | "AI":"Anguilla", 13 | "AQ":"Antarctica", 14 | "AG":"Antigua and Barbuda", 15 | "AR":"Argentina", 16 | "AM":"Armenia", 17 | "AW":"Aruba", 18 | "AU":"Australia", 19 | "AT":"Austria", 20 | "AZ":"Azerbaijan", 21 | "BS":"The Bahamas", 22 | "BH":"Bahrain", 23 | "BD":"Bangladesh", 24 | "BB":"Barbados", 25 | "BY":"Belarus", 26 | "BE":"Belgium", 27 | "BZ":"Belize", 28 | "BJ":"Benin", 29 | "BM":"Bermuda", 30 | "BT":"Bhutan", 31 | "BO":"Bolivia", 32 | "BQ":"Bonaire", 33 | "BA":"Bosnia and Herzegovina", 34 | "BW":"Botswana", 35 | "BV":"Bouvet Island", 36 | "BR":"Brazil", 37 | "IO":"British Indian Ocean Territory", 38 | "UM":"United States Minor Outlying Islands", 39 | "VG":"Virgin Islands (British)", 40 | "VI":"Virgin Islands (U.S.)", 41 | "BN":"Brunei", 42 | "BG":"Bulgaria", 43 | "BF":"Burkina Faso", 44 | "BI":"Burundi", 45 | "KH":"Cambodia", 46 | "CM":"Cameroon", 47 | "CA":"Canada", 48 | "CV":"Cape Verde", 49 | "KY":"Cayman Islands", 50 | "CF":"Central African Republic", 51 | "TD":"Chad", 52 | "CL":"Chile", 53 | "CN":"China", 54 | "CX":"Christmas Island", 55 | "CC":"Cocos (Keeling) Islands", 56 | "CO":"Colombia", 57 | "KM":"Comoros", 58 | "CG":"Republic of the Congo", 59 | "CD":"Democratic Republic of the Congo", 60 | "CK":"Cook Islands", 61 | "CR":"Costa Rica", 62 | "HR":"Croatia", 63 | "CU":"Cuba", 64 | "CW":"Curaçao", 65 | "CY":"Cyprus", 66 | "CZ":"Czech Republic", 67 | "DK":"Denmark", 68 | "DJ":"Djibouti", 69 | "DM":"Dominica", 70 | "DO":"Dominican Republic", 71 | "EC":"Ecuador", 72 | "EG":"Egypt", 73 | "SV":"El Salvador", 74 | "GQ":"Equatorial Guinea", 75 | "ER":"Eritrea", 76 | "EE":"Estonia", 77 | "ET":"Ethiopia", 78 | "FK":"Falkland Islands", 79 | "FO":"Faroe Islands", 80 | "FJ":"Fiji", 81 | "FI":"Finland", 82 | "FR":"France", 83 | "GF":"French Guiana", 84 | "PF":"French Polynesia", 85 | "TF":"French Southern and Antarctic Lands", 86 | "GA":"Gabon", 87 | "GM":"The Gambia", 88 | "GE":"Georgia", 89 | "DE":"Germany", 90 | "GH":"Ghana", 91 | "GI":"Gibraltar", 92 | "GR":"Greece", 93 | "GL":"Greenland", 94 | "GD":"Grenada", 95 | "GP":"Guadeloupe", 96 | "GU":"Guam", 97 | "GT":"Guatemala", 98 | "GG":"Guernsey", 99 | "GW":"Guinea-Bissau", 100 | "GY":"Guyana", 101 | "HT":"Haiti", 102 | "HM":"Heard Island and McDonald Islands", 103 | "VA":"Holy See", 104 | "HN":"Honduras", 105 | "HK":"Hong Kong", 106 | "HU":"Hungary", 107 | "IS":"Iceland", 108 | "IN":"India", 109 | "ID":"Indonesia", 110 | "CI":"Ivory Coast", 111 | "IR":"Iran", 112 | "IQ":"Iraq", 113 | "IE":"Republic of Ireland", 114 | "IM":"Isle of Man", 115 | "IL":"Israel", 116 | "IT":"Italy", 117 | "JM":"Jamaica", 118 | "JP":"Japan", 119 | "JE":"Jersey", 120 | "JO":"Jordan", 121 | "KZ":"Kazakhstan", 122 | "KE":"Kenya", 123 | "KI":"Kiribati", 124 | "KW":"Kuwait", 125 | "KG":"Kyrgyzstan", 126 | "LA":"Laos", 127 | "LV":"Latvia", 128 | "LB":"Lebanon", 129 | "LS":"Lesotho", 130 | "LR":"Liberia", 131 | "LY":"Libya", 132 | "LI":"Liechtenstein", 133 | "LT":"Lithuania", 134 | "LU":"Luxembourg", 135 | "MO":"Macau", 136 | "MK":"Republic of Macedonia", 137 | "MG":"Madagascar", 138 | "MW":"Malawi", 139 | "MY":"Malaysia", 140 | "MV":"Maldives", 141 | "ML":"Mali", 142 | "MT":"Malta", 143 | "MH":"Marshall Islands", 144 | "MQ":"Martinique", 145 | "MR":"Mauritania", 146 | "MU":"Mauritius", 147 | "YT":"Mayotte", 148 | "MX":"Mexico", 149 | "FM":"Federated States of Micronesia", 150 | "MD":"Moldova", 151 | "MC":"Monaco", 152 | "MN":"Mongolia", 153 | "ME":"Montenegro", 154 | "MS":"Montserrat", 155 | "MA":"Morocco", 156 | "MZ":"Mozambique", 157 | "MM":"Myanmar", 158 | "NA":"Namibia", 159 | "NR":"Nauru", 160 | "NP":"Nepal", 161 | "NL":"Netherlands", 162 | "NC":"New Caledonia", 163 | "NZ":"New Zealand", 164 | "NI":"Nicaragua", 165 | "NE":"Niger", 166 | "NG":"Nigeria", 167 | "NU":"Niue", 168 | "NF":"Norfolk Island", 169 | "KP":"North Korea", 170 | "MP":"Northern Mariana Islands", 171 | "NO":"Norway", 172 | "OM":"Oman", 173 | "PK":"Pakistan", 174 | "PW":"Palau", 175 | "PS":"Palestine", 176 | "PA":"Panama", 177 | "PG":"Papua New Guinea", 178 | "PY":"Paraguay", 179 | "PE":"Peru", 180 | "PH":"Philippines", 181 | "PN":"Pitcairn Islands", 182 | "PL":"Poland", 183 | "PT":"Portugal", 184 | "PR":"Puerto Rico", 185 | "QA":"Qatar", 186 | "RE":"Réunion", 187 | "RO":"Romania", 188 | "RU":"Russia", 189 | "RW":"Rwanda", 190 | "BL":"Saint Barthélemy", 191 | "SH":"Saint Helena", 192 | "KN":"Saint Kitts and Nevis", 193 | "LC":"Saint Lucia", 194 | "MF":"Saint Martin", 195 | "PM":"Saint Pierre and Miquelon", 196 | "VC":"Saint Vincent and the Grenadines", 197 | "WS":"Samoa", 198 | "SM":"San Marino", 199 | "ST":"São Tomé and Príncipe", 200 | "SA":"Saudi Arabia", 201 | "SN":"Senegal", 202 | "RS":"Serbia", 203 | "SC":"Seychelles", 204 | "SL":"Sierra Leone", 205 | "SG":"Singapore", 206 | "SX":"Sint Maarten", 207 | "SK":"Slovakia", 208 | "SI":"Slovenia", 209 | "SB":"Solomon Islands", 210 | "SO":"Somalia", 211 | "ZA":"South Africa", 212 | "GS":"South Georgia", 213 | "KR":"South Korea", 214 | "SS":"South Sudan", 215 | "ES":"Spain", 216 | "LK":"Sri Lanka", 217 | "SD":"Sudan", 218 | "SR":"Surinae", 219 | "SJ":"Svalbard and Jan Mayen", 220 | "SZ":"Swaziland", 221 | "SE":"Sweden", 222 | "CH":"Switzerland", 223 | "SY":"Syria", 224 | "TW":"Taiwan", 225 | "TJ":"Tajikistan", 226 | "TZ":"Tanzania", 227 | "TH":"Thailand", 228 | "TL":"East Timor", 229 | "TG":"Togo", 230 | "TK":"Tokelau", 231 | "TO":"Tonga", 232 | "TT":"Trinidad and Tobago", 233 | "TN":"Tunisia", 234 | "TR":"Turkey", 235 | "TM":"Turkmenistan", 236 | "TC":"Turks and Caicos Islands", 237 | "TV":"Tuvalu", 238 | "UG":"Uganda", 239 | "UA":"Ukraine", 240 | "AE":"United Arab Emirates", 241 | "GB":"United Kingdom", 242 | "US":"United States", 243 | "UY":"Uruguay", 244 | "UZ":"Uzbekistan", 245 | "VU":"Vanuatu", 246 | "VE":"Venezuela", 247 | "VN":"Vietnam", 248 | "WF":"Wallis and Futuna", 249 | "EH":"Western Sahara", 250 | "YE":"Yemen", 251 | "ZM":"Zambia", 252 | "ZW":"Zimbabwe", 253 | } 254 | 255 | func ValidCountry(isoCode string) bool { 256 | _, ok := countryCodes[isoCode] 257 | return ok 258 | } 259 | -------------------------------------------------------------------------------- /web/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/kelseyhightower/envconfig" 5 | "log" 6 | "time" 7 | ) 8 | 9 | type DateTimeParser time.Time 10 | 11 | func (dt *DateTimeParser) Decode(value string) error { 12 | out, err := time.Parse(time.RFC3339, value) 13 | if err != nil { 14 | return err 15 | } 16 | *dt = DateTimeParser(out) 17 | return nil 18 | } 19 | 20 | var Config *config 21 | 22 | func init() { 23 | var s config 24 | err := envconfig.Process("", &s) 25 | if err != nil { 26 | // WTF :) 27 | log.Fatal(err.Error()) 28 | } 29 | Config = &s 30 | } 31 | 32 | type config struct { 33 | EnableSecureCookies bool `default:"true" split_words:"true"` 34 | 35 | StartCompetition DateTimeParser `default:"2019-12-20T20:00:00+00:00" split_words:"true"` 36 | EndCompetition DateTimeParser `default:"2019-12-22T09:00:00+00:00" split_words:"true"` 37 | 38 | FreezeStartCompetition DateTimeParser `default:"2019-12-22T08:00:00+00:00" split_words:"true"` 39 | FreezeEndCompetition DateTimeParser `default:"2019-12-22T09:00:00+00:00" split_words:"true"` 40 | 41 | HmacSecretKey []byte `required:"true" split_words:"true"` 42 | AesSecretKey []byte `required:"true" split_words:"true"` 43 | CaptchaSecret string `required:"true" split_words:"true"` 44 | SentryDsn string `default:"" split_words:"true"` 45 | 46 | RequestTimeout time.Duration `default:"4s" split_words:"true"` 47 | Listen string `default:":8080" split_words:"true"` 48 | 49 | MysqlDsn string `required:"true" split_words:"true"` 50 | AvatarPublicWebPath string `default:"/avatar/" split_words:"true"` 51 | } 52 | 53 | func IsFreezeNow() bool { 54 | now := time.Now() 55 | //start < now && end > now 56 | return time.Time(Config.FreezeStartCompetition).Before(now) && time.Time(Config.FreezeEndCompetition).After(now) 57 | } 58 | 59 | func IsBetweenFreeze(now time.Time) bool { 60 | //start < now && end > now 61 | return IsFreezeNow() && time.Time(Config.FreezeStartCompetition).Before(now) && time.Time(Config.FreezeEndCompetition).After(now) 62 | } 63 | -------------------------------------------------------------------------------- /web/db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "errors" 7 | "github.com/go-sql-driver/mysql" 8 | _ "github.com/go-sql-driver/mysql" 9 | ) 10 | 11 | var ErrAlreadyExistsDB = errors.New("already exists in db") 12 | 13 | type DatabaseInternal struct { 14 | db *sql.DB 15 | } 16 | 17 | func NewDB(mysqlDSN string) (*DatabaseInternal, error) { 18 | db, err := sql.Open("mysql", mysqlDSN) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | return &DatabaseInternal{ 24 | db: db, 25 | }, nil 26 | } 27 | 28 | func (s *DatabaseInternal) Ping(ctx context.Context) error { 29 | return s.db.PingContext(ctx) 30 | } 31 | 32 | func (s *DatabaseInternal) Exec(ctx context.Context, query string, args ...interface{}) (sql.Result, error) { 33 | result, err := s.db.ExecContext(ctx, query, args...) 34 | return result, wrapErr(err) 35 | } 36 | 37 | func (s *DatabaseInternal) QueryRow(ctx context.Context, query string, args ...interface{}) *Row { 38 | rows, err := s.db.QueryContext(ctx, query, args...) 39 | return &Row{Err: wrapErr(err), rows: rows} 40 | } 41 | 42 | func (s *DatabaseInternal) Query(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) { 43 | rows, err := s.db.QueryContext(ctx, query, args...) 44 | return rows, wrapErr(err) 45 | } 46 | 47 | func (s *DatabaseInternal) Close(ctx context.Context) error { 48 | if s.db != nil { 49 | return nil 50 | } 51 | return s.db.Close() 52 | } 53 | 54 | func wrapErr(err error) error { 55 | me, ok := err.(*mysql.MySQLError) 56 | if !ok { 57 | return err 58 | } 59 | if me.Number == 1062 { 60 | return ErrAlreadyExistsDB 61 | } 62 | return err 63 | } 64 | 65 | // forked shit, no public Err in stdlib :'( 66 | type Row struct { 67 | Err error 68 | rows *sql.Rows 69 | } 70 | 71 | func (r *Row) Scan(dest ...interface{}) error { 72 | if r.Err != nil { 73 | return r.Err 74 | } 75 | defer r.rows.Close() 76 | for _, dp := range dest { 77 | if _, ok := dp.(*sql.RawBytes); ok { 78 | return errors.New("sql: RawBytes isn't allowed on Row.Scan") 79 | } 80 | } 81 | 82 | if !r.rows.Next() { 83 | if err := r.rows.Err(); err != nil { 84 | return err 85 | } 86 | return sql.ErrNoRows 87 | } 88 | err := r.rows.Scan(dest...) 89 | if err != nil { 90 | return err 91 | } 92 | return r.rows.Close() 93 | } 94 | -------------------------------------------------------------------------------- /web/go.mod: -------------------------------------------------------------------------------- 1 | module ctfplatform 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/fasthttp/router v0.5.2 7 | github.com/getsentry/sentry-go v0.3.1 8 | github.com/go-sql-driver/mysql v1.4.1 9 | github.com/goware/emailx v0.2.0 10 | github.com/kelseyhightower/envconfig v1.4.0 11 | github.com/sirupsen/logrus v1.4.2 12 | github.com/valyala/fasthttp v1.6.0 13 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 14 | google.golang.org/appengine v1.6.5 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /web/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import "github.com/sirupsen/logrus" 4 | 5 | var Log *logrus.Logger 6 | 7 | func init() { 8 | Log = logrus.New() 9 | } 10 | -------------------------------------------------------------------------------- /web/models/task.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "context" 5 | "ctfplatform/config" 6 | "ctfplatform/db" 7 | "ctfplatform/log" 8 | "errors" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | type TaskXXX struct { 14 | ID int 15 | Name string 16 | Points int 17 | Solvers int 18 | Category string 19 | Description string 20 | Difficult string 21 | } 22 | 23 | type TaskInternal struct { 24 | db *db.DatabaseInternal 25 | 26 | flagsMu sync.RWMutex 27 | flagsToTask map[string]int 28 | } 29 | 30 | func NewTaskDB(db *db.DatabaseInternal) *TaskInternal { 31 | s := &TaskInternal{ 32 | db: db, 33 | flagsToTask: make(map[string]int), 34 | } 35 | return s 36 | } 37 | 38 | func (s *TaskInternal) Run(ctx context.Context) error { 39 | for { 40 | s.updateWorker(ctx) 41 | 42 | select { 43 | case <-ctx.Done(): 44 | return ctx.Err() 45 | case <-time.After(time.Second * 10): 46 | } 47 | } 48 | return nil 49 | } 50 | 51 | func (s *TaskInternal) updateWorker(ctx context.Context) { 52 | newFlags, err := s.GetFlags(ctx) 53 | if err != nil { 54 | log.Log.WithError(err).Error("not updating flags") 55 | return 56 | } 57 | s.flagsMu.Lock() 58 | s.flagsToTask = newFlags 59 | s.flagsMu.Unlock() 60 | } 61 | 62 | func (s *TaskInternal) GetFlags(ctx context.Context) (map[string]int, error) { 63 | query := ` 64 | SELECT 65 | task_flags.task_id, 66 | task_flags.flag 67 | FROM 68 | task_flags 69 | INNER JOIN task ON (task.id = task_flags.task_id) 70 | WHERE 71 | task.started_at < NOW() 72 | ` 73 | rows, err := s.db.Query(ctx, query) 74 | if err != nil { 75 | return nil, err 76 | } 77 | defer rows.Close() 78 | 79 | out := make(map[string]int) 80 | var ( 81 | taskID int 82 | flag string 83 | ) 84 | for rows.Next() { 85 | if err := rows.Scan(&taskID, &flag); err != nil { 86 | return nil, err 87 | } 88 | out[flag] = taskID 89 | } 90 | return out, nil 91 | } 92 | 93 | func (s *TaskInternal) GetByFlag(ctx context.Context, flag string) (int, error) { 94 | s.flagsMu.RLock() 95 | defer s.flagsMu.RUnlock() 96 | taskID, exists := s.flagsToTask[flag] 97 | if !exists { 98 | return 0, errors.New("not exists") 99 | } 100 | return taskID, nil 101 | } 102 | 103 | func (s *TaskInternal) All(ctx context.Context) ([]*TaskXXX, error) { 104 | query := ` 105 | WITH 106 | audit_fixed AS ( 107 | SELECT 108 | audit.id, 109 | audit.team_id, 110 | audit.task_id, 111 | audit.created_at 112 | FROM 113 | audit 114 | WHERE 115 | audit.created_at BETWEEN ? AND ? 116 | ), 117 | task_calculated_points AS ( 118 | SELECT 119 | audit_fixed.task_id, 120 | COUNT(1) as team_solved, 121 | GREATEST( 122 | 50, 123 | FLOOR( 124 | 500 - (80 * LOG2( 125 | ( 126 | GREATEST( 127 | 1, 128 | COUNT(1) 129 | ) + 3 130 | ) / (1 + 3) 131 | )) 132 | ) 133 | ) as points 134 | FROM 135 | audit_fixed 136 | GROUP BY audit_fixed.task_id 137 | ) 138 | SELECT 139 | task.id, 140 | task.name, 141 | task.description, 142 | task.category, 143 | task.difficult, 144 | COALESCE(task_calculated_points.points, 500) as points, 145 | COALESCE(task_calculated_points.team_solved, 0) as solvers 146 | FROM 147 | task 148 | LEFT JOIN task_calculated_points ON (task_calculated_points.task_id = task.id) 149 | WHERE 150 | task.started_at < NOW() 151 | ` 152 | rows, err := s.db.Query(ctx, query, time.Time(config.Config.StartCompetition), time.Time(config.Config.EndCompetition)) 153 | if err != nil { 154 | return nil, err 155 | } 156 | defer rows.Close() 157 | 158 | out := make([]*TaskXXX, 0) 159 | for rows.Next() { 160 | var row TaskXXX 161 | if err := rows.Scan(&row.ID, &row.Name, &row.Description, &row.Category, &row.Difficult, &row.Points, &row.Solvers); err != nil { 162 | return nil, err 163 | } 164 | out = append(out, &row) 165 | } 166 | return out, nil 167 | } 168 | -------------------------------------------------------------------------------- /web/models/team.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "context" 5 | "ctfplatform/db" 6 | "ctfplatform/rand" 7 | "fmt" 8 | "golang.org/x/crypto/bcrypt" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | type TeamXXX struct { 14 | ID int 15 | Name string 16 | Email string 17 | Password string 18 | AvatarPath string 19 | Country string 20 | CreatedAt time.Time 21 | Affiliation string 22 | Website string 23 | } 24 | 25 | func GenAvatarFilename() string { 26 | return fmt.Sprintf("%s.png", rand.RandStringRunes(32)) 27 | } 28 | 29 | func (t *TeamXXX) SetAvatar(ctx context.Context, db *TeamInternal, avatarPayload []byte) error { 30 | if len(avatarPayload) == 0 { 31 | t.AvatarPath = "" 32 | return nil 33 | } 34 | oldAvatar := t.AvatarPath 35 | newAvatar := GenAvatarFilename() 36 | t.AvatarPath = newAvatar 37 | 38 | if err := db.UpdateAvatar(ctx, *t, avatarPayload); err != nil { 39 | t.AvatarPath = oldAvatar 40 | return err 41 | } 42 | //if err := ioutil.WriteFile(filepath.Join(config.Config.AvatarPath, newAvatar), avatarPayload, 0644); err != nil { 43 | // return err 44 | //} 45 | //if len(t.AvatarPath) != 0 { 46 | // if err := os.Remove(filepath.Join(config.Config.AvatarPath, t.AvatarPath)); err != nil { 47 | // log.Log.WithError(err).Warningf("os.Remove avatar: %v", t.AvatarPath) 48 | // } 49 | //} 50 | return nil 51 | } 52 | 53 | func (t *TeamXXX) SetPassword(plainPassword string) error { 54 | b, err := bcrypt.GenerateFromPassword([]byte(plainPassword), 12) 55 | if err != nil { 56 | return err 57 | } 58 | t.Password = string(b) 59 | return nil 60 | } 61 | 62 | func (t *TeamXXX) EqualPassword(plainPassword string) bool { 63 | err := bcrypt.CompareHashAndPassword([]byte(t.Password), []byte(plainPassword)) 64 | return err == nil 65 | } 66 | 67 | type TeamInternal struct { 68 | db *db.DatabaseInternal 69 | } 70 | 71 | func NewTeamDB(db *db.DatabaseInternal) *TeamInternal { 72 | return &TeamInternal{ 73 | db: db, 74 | } 75 | } 76 | 77 | func (s *TeamInternal) All(ctx context.Context) ([]*TeamXXX, error) { 78 | query := ` 79 | SELECT 80 | id, 81 | name, 82 | created_at, 83 | avatar, 84 | country, 85 | affiliation, 86 | website 87 | FROM team 88 | ORDER BY id ASC 89 | ` 90 | rows, err := s.db.Query(ctx, query) 91 | if err != nil { 92 | return nil, err 93 | } 94 | defer rows.Close() 95 | 96 | var result []*TeamXXX 97 | for rows.Next() { 98 | var out TeamXXX 99 | if err := rows.Scan(&out.ID, &out.Name, &out.CreatedAt, &out.AvatarPath, &out.Country, &out.Affiliation, &out.Website); err != nil { 100 | return nil, err 101 | } 102 | result = append(result, &out) 103 | } 104 | return result, nil 105 | } 106 | 107 | func (s *TeamInternal) GetByID(ctx context.Context, id int) (*TeamXXX, error) { 108 | query := ` 109 | SELECT 110 | id, 111 | name, 112 | email, 113 | password, 114 | created_at, 115 | avatar, 116 | country, 117 | affiliation, 118 | website 119 | FROM team 120 | WHERE 121 | id = ? 122 | ` 123 | var out TeamXXX 124 | err := s.db.QueryRow(ctx, query, id).Scan(&out.ID, &out.Name, &out.Email, &out.Password, &out.CreatedAt, &out.AvatarPath, &out.Country, &out.Affiliation, &out.Website) 125 | if err != nil { 126 | return nil, err 127 | } 128 | return &out, nil 129 | } 130 | 131 | // sad golang :( 132 | func arrayIntToInterface(v []int) []interface{} { 133 | r := make([]interface{}, len(v)) 134 | for i, a := range v { 135 | r[i] = a 136 | } 137 | return r 138 | } 139 | 140 | func sqlInComma(count int) string { 141 | if count == 0 { 142 | return "" 143 | } 144 | if count == 1 { 145 | return "?" 146 | } 147 | return "?" + strings.Repeat(",?", count-1) 148 | } 149 | 150 | func (s *TeamInternal) GetByIDs(ctx context.Context, ids []int) (map[int]*TeamXXX, error) { 151 | query := fmt.Sprintf(` 152 | SELECT 153 | id, 154 | name, 155 | email, 156 | password, 157 | created_at, 158 | avatar, 159 | country, 160 | affiliation, 161 | website 162 | FROM team 163 | WHERE 164 | id IN (%s) 165 | `, sqlInComma(len(ids))) 166 | rows, err := s.db.Query(ctx, query, arrayIntToInterface(ids)...) 167 | if err != nil { 168 | return nil, err 169 | } 170 | defer rows.Close() 171 | 172 | result := make(map[int]*TeamXXX) 173 | for rows.Next() { 174 | var out TeamXXX 175 | if err := rows.Scan(&out.ID, &out.Name, &out.Email, &out.Password, &out.CreatedAt, &out.AvatarPath, &out.Country, &out.Affiliation, &out.Website); err != nil { 176 | return nil, err 177 | } 178 | result[out.ID] = &out 179 | } 180 | return result, nil 181 | } 182 | 183 | func (s *TeamInternal) GetByLogin(ctx context.Context, email string) (*TeamXXX, error) { 184 | query := ` 185 | SELECT 186 | id, 187 | name, 188 | email, 189 | password, 190 | created_at, 191 | avatar, 192 | country, 193 | affiliation, 194 | website 195 | FROM team 196 | WHERE 197 | email = ? OR name = ? 198 | LIMIT 1 199 | ` 200 | var out TeamXXX 201 | err := s.db.QueryRow(ctx, query, email, email).Scan(&out.ID, &out.Name, &out.Email, &out.Password, &out.CreatedAt, &out.AvatarPath, &out.Country, &out.Affiliation, &out.Website) 202 | if err != nil { 203 | return nil, err 204 | } 205 | return &out, nil 206 | } 207 | 208 | func (s *TeamInternal) AddTeam(ctx context.Context, team TeamXXX) error { 209 | query := ` 210 | INSERT INTO team (id, name, email, password, created_at, active, avatar, country) VALUES (NULL, ?, ?, ?, NOW(), 1, ?, ?) 211 | ` 212 | _, err := s.db.Exec(ctx, query, team.Name, team.Email, team.Password, team.AvatarPath, team.Country) 213 | return err 214 | } 215 | 216 | func (s *TeamInternal) UpdateTeam(ctx context.Context, team TeamXXX) error { 217 | query := ` 218 | UPDATE team SET name = ?, password = ?, avatar = ?, country = ?, affiliation = ?, website = ? WHERE id = ? 219 | ` 220 | _, err := s.db.Exec(ctx, query, team.Name, team.Password, team.AvatarPath, team.Country, team.Affiliation, team.Website, team.ID) 221 | return err 222 | } 223 | 224 | // TODO: other model 225 | func (s *TeamInternal) UpdateAvatar(ctx context.Context, team TeamXXX, avatarPayload []byte) error { 226 | query := ` 227 | INSERT INTO team_avatar (id, team_id, avatar_path, avatar) VALUES (NULL, ?, ?, ?) 228 | ON DUPLICATE KEY 229 | UPDATE avatar_path=VALUES(avatar_path), avatar=VALUES(avatar) 230 | ` 231 | _, err := s.db.Exec(ctx, query, team.ID, team.AvatarPath, avatarPayload) 232 | return err 233 | } 234 | 235 | func (s *TeamInternal) GetAvatar(ctx context.Context, avatarPath string) ([]byte, error) { 236 | query := ` 237 | SELECT avatar FROM team_avatar WHERE avatar_path = ? 238 | ` 239 | var out []byte 240 | err := s.db.QueryRow(ctx, query, avatarPath).Scan(&out) 241 | if err != nil { 242 | return nil, err 243 | } 244 | return out, nil 245 | } 246 | -------------------------------------------------------------------------------- /web/rand/rand.go: -------------------------------------------------------------------------------- 1 | package rand 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | func init() { 9 | rand.Seed(time.Now().UnixNano()) 10 | } 11 | 12 | var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 13 | 14 | func RandStringRunes(n int) string { 15 | b := make([]rune, n) 16 | for i := range b { 17 | b[i] = letterRunes[rand.Intn(len(letterRunes))] 18 | } 19 | return string(b) 20 | } 21 | 22 | func RandInt() int { 23 | return rand.Intn(0xffffffff) 24 | } 25 | -------------------------------------------------------------------------------- /web/sentry/hook.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "fmt" 5 | "github.com/getsentry/sentry-go" 6 | "github.com/sirupsen/logrus" 7 | "reflect" 8 | ) 9 | 10 | type SentryHook struct { 11 | client *sentry.Hub 12 | } 13 | 14 | func NewSentryHook(dsn string) (*SentryHook, error) { 15 | err := sentry.Init(sentry.ClientOptions{ 16 | Dsn: dsn, 17 | }) 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | return &SentryHook{ 23 | client: sentry.CurrentHub(), 24 | }, nil 25 | } 26 | 27 | func (hook *SentryHook) Fire(entry *logrus.Entry) error { 28 | var err error 29 | if err2, ok := entry.Data[logrus.ErrorKey]; ok { 30 | if err3, ok := err2.(error); ok { 31 | err = err3 32 | } else { 33 | err = fmt.Errorf("not err: %v", err2) 34 | } 35 | } 36 | 37 | event := sentry.NewEvent() 38 | if err != nil { 39 | stacktrace := sentry.ExtractStacktrace(err) 40 | if stacktrace == nil { 41 | stacktrace = sentry.NewStacktrace() 42 | } 43 | event.Exception = []sentry.Exception{{ 44 | Value: err.Error(), 45 | Type: reflect.TypeOf(err).String(), 46 | Stacktrace: stacktrace, 47 | }} 48 | } 49 | event.Message = entry.Message 50 | event.Timestamp = entry.Time.Unix() 51 | tags := make(map[string]string) 52 | for key, value := range entry.Data { 53 | if key == logrus.ErrorKey { 54 | continue 55 | } 56 | if v, ok := value.(string); ok { 57 | tags[key] = v 58 | } else { 59 | tags[key] = fmt.Sprintf("%v", value) 60 | } 61 | } 62 | event.Tags = tags 63 | 64 | switch entry.Level { 65 | case logrus.PanicLevel: 66 | event.Level = sentry.LevelFatal 67 | case logrus.FatalLevel: 68 | event.Level = sentry.LevelFatal 69 | case logrus.ErrorLevel: 70 | event.Level = sentry.LevelError 71 | case logrus.WarnLevel: 72 | event.Level = sentry.LevelWarning 73 | case logrus.InfoLevel: 74 | event.Level = sentry.LevelInfo 75 | case logrus.DebugLevel, logrus.TraceLevel: 76 | event.Level = sentry.LevelDebug 77 | default: 78 | return nil 79 | } 80 | 81 | hook.client.CaptureEvent(event) 82 | return nil 83 | } 84 | 85 | func (hook *SentryHook) Levels() []logrus.Level { 86 | return []logrus.Level{ 87 | logrus.PanicLevel, 88 | logrus.FatalLevel, 89 | logrus.ErrorLevel, 90 | logrus.WarnLevel, 91 | //logrus.InfoLevel, 92 | //logrus.DebugLevel, 93 | //logrus.TraceLevel, 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /web/session/session.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "bytes" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "crypto/hmac" 8 | "crypto/rand" 9 | "crypto/sha256" 10 | "encoding/base64" 11 | "encoding/json" 12 | "errors" 13 | "io" 14 | ) 15 | 16 | func PKCS5Padding(ciphertext []byte, blockSize int) []byte { 17 | padding := blockSize - len(ciphertext)%blockSize 18 | padtext := bytes.Repeat([]byte{byte(padding)}, padding) 19 | return append(ciphertext, padtext...) 20 | } 21 | 22 | func PKCS5Trimming(encrypt []byte) []byte { 23 | padding := encrypt[len(encrypt)-1] 24 | return encrypt[:len(encrypt)-int(padding)] 25 | } 26 | 27 | func MarshalSession(key1, key2 []byte, in interface{}) ([]byte, error) { 28 | plaintext, err := json.Marshal(in) 29 | if err != nil { 30 | return nil, err 31 | } 32 | plaintext = PKCS5Padding(plaintext, aes.BlockSize) 33 | 34 | if len(plaintext)%aes.BlockSize != 0 { 35 | return nil, errors.New("plaintext is not a multiple of the block size") 36 | } 37 | 38 | block, err := aes.NewCipher(key1) 39 | if err != nil { 40 | return nil, err 41 | } 42 | block.BlockSize() 43 | 44 | ciphertext := make([]byte, aes.BlockSize+len(plaintext)) 45 | iv := ciphertext[:aes.BlockSize] 46 | if _, err := io.ReadFull(rand.Reader, iv); err != nil { 47 | return nil, err 48 | } 49 | 50 | mode := cipher.NewCBCEncrypter(block, iv) 51 | mode.CryptBlocks(ciphertext[aes.BlockSize:], plaintext) 52 | 53 | // hmac 54 | h := hmac.New(sha256.New, key2) 55 | _, err = h.Write(ciphertext) 56 | if err != nil { 57 | return nil, err 58 | } 59 | hashSum := h.Sum(nil) 60 | 61 | // base64 ciphertext 62 | ciphertextBase64 := base64.RawStdEncoding.EncodeToString(ciphertext) 63 | // base64 hash 64 | hashSumBase64 := base64.RawStdEncoding.EncodeToString(hashSum) 65 | 66 | return []byte(ciphertextBase64 + "." + hashSumBase64), nil 67 | } 68 | 69 | func UnmarshalSession(key1, key2 []byte, in []byte, out interface{}) error { 70 | arrSplit := bytes.SplitN(in, []byte("."), 2) 71 | if len(arrSplit) != 2 { 72 | return errors.New("should have two dots") 73 | } 74 | ciphertextBase64, hashSumBase64 := string(arrSplit[0]), string(arrSplit[1]) 75 | 76 | hashSum, err := base64.RawStdEncoding.DecodeString(hashSumBase64) 77 | if err != nil { 78 | return err 79 | } 80 | 81 | ciphertext, err := base64.RawStdEncoding.DecodeString(ciphertextBase64) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | // hmac 87 | h := hmac.New(sha256.New, key2) 88 | _, err = h.Write(ciphertext) 89 | if err != nil { 90 | return err 91 | } 92 | hashSumNew := h.Sum(nil) 93 | if !hmac.Equal(hashSum, hashSumNew) { 94 | return errors.New("hmac not same") 95 | } 96 | 97 | block, err := aes.NewCipher(key1) 98 | if len(ciphertext) < aes.BlockSize { 99 | return errors.New("ciphertext too short") 100 | } 101 | iv := ciphertext[:aes.BlockSize] 102 | ciphertext = ciphertext[aes.BlockSize:] 103 | 104 | if len(ciphertext)%aes.BlockSize != 0 { 105 | return errors.New("ciphertext is not a multiple of the block size") 106 | } 107 | 108 | mode := cipher.NewCBCDecrypter(block, iv) 109 | mode.CryptBlocks(ciphertext, ciphertext) 110 | 111 | plaintext := PKCS5Trimming(ciphertext) 112 | return json.Unmarshal(plaintext, out) 113 | } 114 | --------------------------------------------------------------------------------