├── .gitignore
├── LICENSE
├── README.md
├── app.js
├── gulpfile.js
├── icons
├── app-icon.icns
├── app-icon.ico
└── app-icon.png
├── index.html
├── lepus-ctf.desktop
├── package.json
├── res
├── alternative-icon.png
└── lepus.svg
└── src
├── api
├── api.jsx
└── error-handler.jsx
├── data
└── store.jsx
├── index.jsx
└── pages
├── announcements.jsx
├── dashboard.jsx
├── login.jsx
├── main.jsx
├── problem.jsx
├── problems.jsx
├── ranking.jsx
└── top.jsx
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | js
3 | font
4 | release
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Yuki MIZUNO
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Lepus-CTF frontend application
2 |
3 | CTF frontent application made with Electron
4 |
5 | ## Usage
6 |
7 | ### Install dependened modules
8 |
9 | ```
10 | $ npm install
11 | ```
12 |
13 | ### Build and run
14 |
15 | ```
16 | $ npm run compile
17 | $ npm start
18 | ```
19 |
20 | > NOTE: If you want to set a specific score-server url, use `--host=` option like
21 | > `npm run compile -- --host=https://score.example.ctf`
22 |
23 | ### Packaging
24 | ```
25 | $ npm run package:all
26 | ```
27 |
28 |
29 | ## License
30 | MIT
31 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var app = require('app');
4 | var BrowserWindow = require('browser-window');
5 | var Menu = require('menu');
6 |
7 | require('crash-reporter').start();
8 |
9 | var mainWindow = null;
10 |
11 | app.on('window-all-closed', function() {
12 | if (process.platform != 'darwin')
13 | app.quit();
14 | });
15 |
16 | app.on('ready', function() {
17 |
18 | mainWindow = new BrowserWindow({width: 1024, height: 600});
19 | mainWindow.loadUrl('file://' + __dirname + '/index.html');
20 |
21 | mainWindow.on('closed', function() {
22 | mainWindow = null;
23 | });
24 |
25 | var menu = Menu.buildFromTemplate([
26 | {
27 | label: 'Lepus-CTF',
28 | submenu: [
29 | {
30 | label: 'Settings',
31 | accelerator: 'CmdOrCtrl+,',
32 | },
33 | {
34 | type: 'separator'
35 | },
36 | {
37 | label: 'Quit',
38 | accelerator: 'CmdOrCtrl+Q',
39 | click: function () {
40 | app.quit();
41 | }
42 | }
43 | ]
44 | },
45 | {
46 | label: 'View',
47 | submenu: [
48 | {
49 | label: 'Toggle Full Screen',
50 | accelerator: 'CmdOrCtrl+Shift+F',
51 | click: function() {
52 | mainWindow.setFullScreen(!mainWindow.isFullScreen());
53 | }
54 | },
55 | {
56 | label: 'Toggle developer tool',
57 | accelerator: 'CmdOrCtrl+Alt+I',
58 | click: function() {
59 | mainWindow.toggleDevTools();
60 | }
61 | }
62 | ]
63 | },
64 | {
65 | label: 'Help',
66 | submenu: [
67 | {label: 'About'},
68 | {label: 'Contact to admin'},
69 | ]
70 | }
71 | ]);
72 | Menu.setApplicationMenu(menu);
73 | });
74 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | var gulp = require('gulp');
2 | var $ = require('gulp-load-plugins')();
3 | var packager = require('electron-packager');
4 |
5 | gulp.task('download:font', function (done) {
6 | var urls = [
7 | "http://mplus-fonts.osdn.jp/webfonts/mplus-2m-regular.ttf",
8 | "http://mplus-fonts.osdn.jp/webfonts/mplus-2p-regular.ttf",
9 | ];
10 |
11 | var fs = require('fs');
12 | var path = require('path');
13 | var dirname = "font"
14 |
15 | if (fs.existsSync(dirname)) {
16 | // Already exists. Skip downloading font.
17 | return done();
18 | }
19 | fs.mkdirSync(dirname);
20 |
21 | console.log('downloading web font.');
22 |
23 | process.on('download', function(i) {
24 | var url = urls[i];
25 | if (!url) {
26 | return done();
27 | }
28 | console.log(url);
29 | var http = require(url.split(':')[0]);
30 | http.get(url, function(res) {
31 | var filename = path.join(dirname, path.basename(url));
32 | var output = fs.createWriteStream(filename);
33 | res.pipe(output);
34 | res.on('end', function() {
35 | process.nextTick(function() {
36 | process.emit('download', ++i);
37 | })
38 | });
39 | }).on('error', function(err) {
40 | console.log('error', err);
41 | });
42 | })
43 | process.emit('download', 0);
44 | });
45 |
46 | gulp.task('compile', function(){
47 | var host = process.argv[3] ? process.argv[3].split('--host=').pop() : '';
48 | if (!host) {
49 | host = "https://score.sakura.tductf.org";
50 | }
51 | return gulp.src('src/**/*.{js,jsx}')
52 | .pipe($.replace('SCORE_SERVER_URL', host))
53 | .pipe(
54 | $.babel({
55 | stage: 0
56 | })
57 | )
58 | .pipe(gulp.dest('js'));
59 | });
60 |
61 | var commonOption = {
62 | dir: './',
63 | out: 'release',
64 | name: 'Lepus-CTF',
65 | arch: 'all',
66 | platform: 'all',
67 | asar: true,
68 | ignore: [
69 | './node_modules/electron*',
70 | './node_modules/.bin',
71 | './release/',
72 | './src/',
73 | './.git*'
74 | ],
75 | version: '0.30.6'
76 | }
77 |
78 | gulp.task('package:darwin', ['compile'], function (done) {
79 | var option = commonOption;
80 | option.platform = 'darwin';
81 | option.arch = 'x64';
82 | packager(option, function (err, path) {
83 | done();
84 | });
85 | });
86 |
87 | gulp.task('package:linux:ia32', ['compile'], function (done) {
88 | var option = commonOption;
89 | option.platform = 'linux';
90 | option.arch = 'ia32';
91 | packager(option, function (err, path) {
92 | done();
93 | });
94 | });
95 |
96 | gulp.task('package:linux:x64', ['compile'], function (done) {
97 | var option = commonOption;
98 | option.platform = 'linux';
99 | option.arch = 'x64';
100 | packager(option, function (err, path) {
101 | done();
102 | });
103 | });
104 |
105 | gulp.task('package:win32:ia32', ['compile'], function (done) {
106 | var option = commonOption;
107 | option.platform = 'win32';
108 | option.arch = 'ia32';
109 | packager(option, function (err, path) {
110 | done();
111 | });
112 | });
113 |
114 | gulp.task('package:win32:x64', ['compile'], function (done) {
115 | var option = commonOption;
116 | option.platform = 'win32';
117 | option.arch = 'x64';
118 | packager(option, function (err, path) {
119 | done();
120 | });
121 | });
122 |
123 | gulp.task('package:all', [
124 | 'package:darwin',
125 | 'package:linux:ia32',
126 | 'package:linux:x64',
127 | 'package:win32:ia32',
128 | 'package:win32:x64'], function (done) {
129 | done();
130 | });
131 |
132 |
--------------------------------------------------------------------------------
/icons/app-icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lepus-ctf/lepus-frontend/2f7e05737a72a72b2f3f3d7758c3148653b785ca/icons/app-icon.icns
--------------------------------------------------------------------------------
/icons/app-icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lepus-ctf/lepus-frontend/2f7e05737a72a72b2f3f3d7758c3148653b785ca/icons/app-icon.ico
--------------------------------------------------------------------------------
/icons/app-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lepus-ctf/lepus-frontend/2f7e05737a72a72b2f3f3d7758c3148653b785ca/icons/app-icon.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Lepus-CTF
6 |
7 |
53 |
54 |
55 |
56 |
57 |
58 |
125 |
126 |
--------------------------------------------------------------------------------
/lepus-ctf.desktop:
--------------------------------------------------------------------------------
1 | [Desktop Entry]
2 | Name=Lepus-CTF
3 | Comment=Lepus-CTF frontend application
4 | Exec=Lepus-CTF
5 | Icon=app-icon.png
6 | Type=Application
7 | Terminal=false
8 | Categories=Game;
9 | Keywords=CTF;
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lepus-frontend",
3 | "version": "0.1.0",
4 | "description": "Lepus-CTF front-end application",
5 | "main": "app.js",
6 | "desktopName": "lepus-ctf.desktop",
7 | "scripts": {
8 | "test": "echo \"Error: no test specified\" && exit 1",
9 | "compile": "gulp compile",
10 | "start": "electron .",
11 | "package:darwin": "gulp package:darwin",
12 | "package:linux:x64": "gulp package:linux:x64",
13 | "package:linux:ia32": "gulp package:linux:ia32",
14 | "package:win32:x64": "gulp package:win32:x64",
15 | "package:win32:ia32": "gulp package:win32:ia32",
16 | "package:all": "gulp package:all",
17 | "postinstall": "gulp download:font"
18 | },
19 | "repository": {
20 | "type": "git",
21 | "url": "https://github.com/lepus-ctf/lepus-frontend.git"
22 | },
23 | "keywords": [
24 | "lepus-ctf",
25 | "ctf",
26 | "electron"
27 | ],
28 | "author": "mzyy94",
29 | "license": "MIT",
30 | "bugs": {
31 | "url": "https://github.com/lepus-ctf/lepus-frontend/issues"
32 | },
33 | "homepage": "https://github.com/lepus-ctf/lepus-frontend",
34 | "devDependencies": {
35 | "electron-packager": "^5.0.2",
36 | "electron-prebuilt": "^0.30.6",
37 | "gulp-babel": "^5.2.0",
38 | "gulp-load-plugins": "^1.0.0-rc.1"
39 | },
40 | "dependencies": {
41 | "better-console": "^0.2.4",
42 | "del": "^1.2.1",
43 | "extend": "^3.0.0",
44 | "gulp": "^3.9.0",
45 | "gulp-autoprefixer": "^2.3.1",
46 | "gulp-chmod": "^1.2.0",
47 | "gulp-clone": "^1.0.0",
48 | "gulp-concat": "^2.6.0",
49 | "gulp-concat-css": "^2.2.0",
50 | "gulp-copy": "0.0.2",
51 | "gulp-flatten": "^0.1.1",
52 | "gulp-header": "^1.2.2",
53 | "gulp-help": "^1.6.0",
54 | "gulp-if": "^1.2.5",
55 | "gulp-less": "^3.0.3",
56 | "gulp-minify-css": "^1.2.0",
57 | "gulp-notify": "^2.2.0",
58 | "gulp-plumber": "^1.0.1",
59 | "gulp-print": "^1.1.0",
60 | "gulp-rename": "^1.2.2",
61 | "gulp-replace": "^0.5.4",
62 | "gulp-rtlcss": "^0.1.4",
63 | "gulp-uglify": "^1.2.0",
64 | "gulp-util": "^3.0.6",
65 | "gulp-watch": "^4.3.5",
66 | "map-stream": "0.0.6",
67 | "md2react": "^0.8.8",
68 | "object-assign": "^4.0.0",
69 | "particles.js": "^2.0.0",
70 | "qs": "^4.0.0",
71 | "react": "^0.13.3",
72 | "react-redux": "^1.0.1",
73 | "react-router": "^0.13.3",
74 | "redux": "^1.0.1",
75 | "require-dot-file": "^0.4.0",
76 | "semantic-ui-css": "^2.0.8",
77 | "socket.io-client": "^1.3.6",
78 | "superagent": "^1.3.0",
79 | "yamljs": "^0.2.3"
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/res/alternative-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lepus-ctf/lepus-frontend/2f7e05737a72a72b2f3f3d7758c3148653b785ca/res/alternative-icon.png
--------------------------------------------------------------------------------
/res/lepus.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
46 |
--------------------------------------------------------------------------------
/src/api/api.jsx:
--------------------------------------------------------------------------------
1 | import ErrorHandler from './error-handler'
2 | export class Api {
3 | constructor() {
4 | this.superagent = require('superagent');
5 | this.serverUrl = "SCORE_SERVER_URL";
6 | this.apiEndpoint = this.serverUrl + "/api"
7 | this.token = "";
8 | this.errorHandler = new ErrorHandler();
9 |
10 | var https = require('https');
11 | var _addRequest = https.Agent.prototype.addRequest;
12 | var self = this;
13 | https.Agent.prototype.addRequest = function() {
14 | var args = arguments;
15 | var cookies = self.agent.jar.getCookies(args[0]).map((cookie) => cookie.toValueString()).join('; ');;
16 | var old = args[0]._headers.cookie;
17 | args[0]._headers.cookie = cookies + (old ? '; ' + old : '');
18 | args[0]._headerNames['cookie'] = 'Cookie';
19 | return _addRequest.apply(this, args);
20 | };
21 | this.https = https;
22 | }
23 | setCriticalAction(func) {
24 | this.errorHandler.criticalAction = func || (() => {});
25 | }
26 | login(username, password, success, failure) {
27 | this.agent = this.superagent.agent();
28 | this.agent
29 | .post(this.apiEndpoint + '/auth.json')
30 | .send({username: username, password: password})
31 | .end((err, res) => {
32 | if (err || username != res.body.username) {
33 | const error = this.errorHandler.parseError(err, res);
34 | failure(error);
35 | } else {
36 | this.agent.saveCookies(res);
37 | this.token = this.agent.jar.getCookie("csrftoken", res.req).value;
38 | success(res.body);
39 | }
40 | });
41 | }
42 |
43 | signup(username, password, success, failure) {
44 | this.agent
45 | .post(this.apiEndpoint + '/users.json')
46 | .send({username: username, password: password, team_name: username, team_password: password})
47 | .end((err, res) => {
48 | if (err) {
49 | const error = this.errorHandler.parseError(err, res);
50 | failure(error);
51 | } else {
52 | success();
53 | }
54 | });
55 | }
56 |
57 | configurations(success, failure) {
58 | this.agent
59 | .get(this.apiEndpoint + '/configurations.json')
60 | .end((err, res) => {
61 | if (err) {
62 | const error = this.errorHandler.parseError(err, res);
63 | failure(error);
64 | } else {
65 | this.agent.saveCookies(res);
66 | success(res.body);
67 | }
68 | });
69 | }
70 |
71 | problems(success, failure) {
72 | this.agent
73 | .get(this.apiEndpoint + '/questions.json')
74 | .query({include: '1'})
75 | .end((err, res) => {
76 | if (err) {
77 | const error = this.errorHandler.parseError(err, res);
78 | failure(error);
79 | } else {
80 | this.agent.saveCookies(res);
81 | success(res.body);
82 | }
83 | });
84 | }
85 |
86 | problem(id, success, failure) {
87 | this.agent
88 | .get(this.apiEndpoint + '/questions/' + id + '.json')
89 | .query({include: '1'})
90 | .end((err, res) => {
91 | if (err) {
92 | const error = this.errorHandler.parseError(err, res);
93 | failure(error);
94 | } else {
95 | this.agent.saveCookies(res);
96 | success(res.body);
97 | }
98 | });
99 | }
100 |
101 | announcement(id, success, failure) {
102 | this.agent
103 | .get(this.apiEndpoint + '/notices/' + id + '.json')
104 | .end((err, res) => {
105 | if (err) {
106 | const error = this.errorHandler.parseError(err, res);
107 | failure(error);
108 | } else {
109 | this.agent.saveCookies(res);
110 | success(res.body);
111 | }
112 | });
113 | }
114 |
115 | announcements(success, failure) {
116 | this.agent
117 | .get(this.apiEndpoint + '/notices.json')
118 | .end((err, res) => {
119 | if (err) {
120 | const error = this.errorHandler.parseError(err, res);
121 | failure(error);
122 | } else {
123 | this.agent.saveCookies(res);
124 | success(res.body);
125 | }
126 | });
127 | }
128 |
129 | submitFlag(id, flag, success, failure) {
130 | this.agent
131 | .post(this.apiEndpoint + '/answers.json')
132 | .set('X-CSRFToken', this.token)
133 | .send({question: id, answer: flag})
134 | .end((err, res) => {
135 | if (err) {
136 | const error = this.errorHandler.parseError(err, res);
137 | failure(error);
138 | } else if (res.body.is_correct) {
139 | this.agent.saveCookies(res);
140 | success();
141 | } else {
142 | console.error(err);
143 | console.error(res);
144 | failure(err, res);
145 | }
146 | });
147 | }
148 |
149 | teamlist(success, failure) {
150 | this.agent
151 | .get(this.apiEndpoint + '/teams.json')
152 | .set('X-CSRFToken', this.token)
153 | .end((err, res) => {
154 | if (err) {
155 | const error = this.errorHandler.parseError(err, res);
156 | failure(error);
157 | } else {
158 | success(res.body);
159 | }
160 | });
161 | }
162 |
163 | team(id, success, failure) {
164 | this.agent
165 | .get(this.apiEndpoint + '/teams/' + id + '.json')
166 | .set('X-CSRFToken', this.token)
167 | .end((err, res) => {
168 | if (err) {
169 | const error = this.errorHandler.parseError(err, res);
170 | failure(error);
171 | } else {
172 | success(res.body);
173 | }
174 | });
175 | }
176 |
177 | downloadFile(filepath, savepath, success, failure) {
178 | this.https
179 | .get(this.serverUrl + filepath, (res) => {
180 | if (res.statusCode >= 200 && res.statusCode < 300) {
181 | var fs = require('fs');
182 | var stream = fs.createWriteStream(savepath);
183 | stream.on('error', (err) => {
184 | const error = this.errorHandler.parseError(err, null);
185 | failure(error);
186 | });
187 | res.pipe(stream);
188 | stream.on('finish', () => {
189 | stream.close((err) => {
190 | if (err) {
191 | const error = this.errorHandler.parseError(err, null);
192 | failure(error);
193 | } else {
194 | success();
195 | }
196 | });
197 | })
198 | } else {
199 | const error = this.errorHandler.parseError(res.statusMessage, res);
200 | failure(error);
201 | }
202 | }).on('error', (err) => {
203 | console.log(err);
204 | const error = this.errorHandler.parseError(err, null);
205 | failure(error);
206 | });
207 | }
208 |
209 | };
210 | export default new Api();
211 |
--------------------------------------------------------------------------------
/src/api/error-handler.jsx:
--------------------------------------------------------------------------------
1 | export default class ErrorHandler {
2 | constructor() {
3 | this.criticalAction = (() => {});
4 | this.errorCodes = [
5 | "REQUIRED",
6 | "TOO_LONG",
7 | "TOO_SHORT",
8 | "TOO_SMALL",
9 | "TOO_BIG",
10 | "INVALID",
11 | "DUPLICATED",
12 | "NOT_FOUND",
13 | "NUMERIC_IS_REQUIRED",
14 | "UNAUTHORIZED",
15 | "PERMISSION_DENIED",
16 | "INVALID_CREDENTIALS",
17 | "INCORRECT_ANSWER",
18 | "ALREADY_SUBMITTED",
19 | "MAX_FAILUER",
20 | "MAX_ANSWERS",
21 | "NOT_STARTED",
22 | "CLOSED",
23 | ];
24 | this.errorMessagesJP = {
25 | "REQUIRED": "入力が必須です。",
26 | "TOO_LONG": "入力文字が長過ぎます。",
27 | "TOO_SHORT": "入力文字が短すぎます。",
28 | "TOO_SMALL": "入力値が小さすぎます。",
29 | "TOO_BIG": "入力値が大きすぎます。",
30 | "INVALID": "入力値が不正です。",
31 | "DUPLICATED": "入力値はすでに存在しています。",
32 | "NOT_FOUND": "項目が見つかりません。",
33 | "NUMERIC_IS_REQUIRED": "数値で入力する必要があります。",
34 | "UNAUTHORIZED": "アクセス認証がされていません。",
35 | "PERMISSION_DENIED": "あなたにはアクセス権限がありません。",
36 | "INVALID_CREDENTIALS": "認証情報が不正です。",
37 | "INCORRECT_ANSWER": "回答が間違っています。",
38 | "ALREADY_SUBMITTED": "すでに回答しています",
39 | "MAX_FAILUER": "回答回数制限に達したため、これ以上回答できません。",
40 | "MAX_ANSWERS": "正答者数が上限に達したため、回答を受け付けられません。",
41 | "NOT_STARTED": "まだ開始していません。",
42 | "CLOSED": "終了しました。",
43 | };
44 | }
45 |
46 | parseError(err, res) {
47 | try {
48 | console.error("Path:" + res.req.path);
49 | } catch(e) {
50 | console.error("Client side error");
51 | }
52 | console.error(err);
53 | var errors;
54 | if (!!res && res.text) {
55 | try {
56 | var json = JSON.parse(res.text);
57 | errors = json.errors;
58 | console.error("REST error: " + json.message);
59 | } catch(e) {
60 | console.error("Error object parsing failed.");
61 | }
62 | } else {
63 | console.error("Can't parse error object.");
64 | console.error("Unknown error.")
65 | }
66 |
67 | var messages = [];
68 | if (errors && errors.length > 0) {
69 | errors.forEach((error) => {
70 | console.log(JSON.stringify(error));
71 | const code = error.error.toUpperCase();
72 | if (this.errorCodes.indexOf(code) === -1) {
73 | console.error("Unlisted error!");
74 | messages.push(code);
75 | } else {
76 | if (error.field) {
77 | messages.push(error.field + 'の' + this.errorMessagesJP[code]);
78 | } else {
79 | messages.push(this.errorMessagesJP[code]);
80 | }
81 | }
82 | }.bind(this));
83 | } else {
84 | try {
85 | messages.push(err.toString());
86 | } catch(e) {
87 | messages.push("REALLY_UNKNOWN_ERROR");
88 | }
89 | }
90 | if (!!res && res.status === 403) {
91 | this.criticalAction();
92 | }
93 | console.log(JSON.stringify(messages));
94 | return messages;
95 | }
96 |
97 | };
98 |
--------------------------------------------------------------------------------
/src/data/store.jsx:
--------------------------------------------------------------------------------
1 | import {createStore} from 'redux';
2 |
3 | export const UPDATE_USERINFO = 'updateUserinfo';
4 | export const UPDATE_TEAMINFO = 'updateTeaminfo';
5 | export const UPDATE_PROBLEMS = 'updateProblems';
6 | export const UPDATE_A_PROBLEM = 'updateAProblem';
7 | export const UPDATE_TEAMLIST = 'updateTeamlist';
8 | export const UPDATE_ANNOUNCEMENTS = 'updateAnnouncements';
9 | export const UPDATE_SERVEREVENT = 'updateServerEvent';
10 | export const UPDATE_CTFCONF = 'updateCTFConfigurations';
11 | export const UPDATE_COUNTDOWN = 'updateCountdown';
12 | export const SET_VISIBLE_CATEGORY = 'setVisibleCategory';
13 | export const SET_VISIBLE_LEVEL = 'setVisibleLevel';
14 | export const SET_HIDDEN_SOLVED = 'setHiddenSolved';
15 | export const RESET_EVENTS = 'resetEvents';
16 | export const EE = 'kkjjhlhlba';
17 |
18 | const initialState = {
19 | point: 0,
20 | solved: 0,
21 | ranking: 0,
22 | easteregg: false,
23 | hiddenSolved: false,
24 | visibleCategory: -1,
25 | visibleLevel: -1,
26 | userInfo: {},
27 | teamInfo: {},
28 | problems: {},
29 | solvedTeams: {},
30 | teamList: [],
31 | announcements: [],
32 | config: {},
33 | countdown: {},
34 | events: {}
35 | };
36 |
37 | const dataStore = (state=initialState, action) => {
38 | switch(action.type){
39 | case UPDATE_USERINFO:
40 | state["userInfo"] = action.data;
41 | return state;
42 | case UPDATE_TEAMINFO:
43 | var solved = 0;
44 | action.data.questions.forEach((question) => {
45 | solved += question.flags;
46 | });
47 | state["teamInfo"] = action.data;
48 | state["point"] = action.data.points;
49 | state["solved"] = solved;
50 | return state;
51 | case UPDATE_PROBLEMS:
52 | if (action.data.length) {
53 | action.data.forEach((problem) => {
54 | state["problems"][problem.id] = problem;
55 | })
56 | }
57 | return state;
58 | case UPDATE_A_PROBLEM:
59 | if (action.data.id != undefined) {
60 | state["problems"][action.data.id] = action.data;
61 | }
62 | return state;
63 | case UPDATE_TEAMLIST:
64 | const teamlist = action.data.sort((current, next) => {
65 | if (current.points < next.points)
66 | return 1;
67 | if (current.points > next.points)
68 | return -1;
69 | if (new Date(current.last_score_time) > new Date(next.last_score_time))
70 | return 1;
71 | return -1;
72 | })
73 | state["teamList"] = teamlist;
74 | state["solvedTeams"] = {};
75 | teamlist.forEach((team, index) => {
76 | if (team.id == state["userInfo"].team) {
77 | var solved = 0;
78 | team.questions.forEach((question) => {
79 | solved += question.flags;
80 | });
81 | state["teamInfo"] = team;
82 | state["point"] = team.points;
83 | state["solved"] = solved;
84 | state["ranking"] = index + 1;
85 | }
86 | team.questions.forEach((question) => {
87 | state["solvedTeams"][question.id] = ~~state["solvedTeams"][question.id] + 1;
88 | });
89 | });
90 | return state;
91 | case UPDATE_ANNOUNCEMENTS:
92 | const announcements = action.data.sort((current, next) => {
93 | const day_current = new Date(current["updated_at"]);
94 | const day_next = new Date(next["updated_at"]);
95 | if (day_current < day_next)
96 | return 1;
97 | if (day_current > day_next)
98 | return -1;
99 | return 0;
100 | })
101 | state["announcements"] = announcements;
102 | return state;
103 | case UPDATE_SERVEREVENT:
104 | switch (action.data.type) {
105 | case "update":
106 | switch (action.data.model) {
107 | case "notice":
108 | state["events"]["announcements"] = ~~state["events"]["announcements"] + 1;
109 | break;
110 | case "question":
111 | state["events"]["problems"] = ~~state["events"]["problems"] + 1;
112 | break;
113 | default:
114 | console.log(JSON.stringify(action.data));
115 | }
116 | break;
117 | default:
118 | console.log(JSON.stringify(action.data));
119 | }
120 | return state;
121 | case UPDATE_CTFCONF:
122 | action.data.forEach((config) => {
123 | switch (config.id) {
124 | case "start_at":
125 | state["config"]["start"] = new Date(config.value);
126 | break;
127 | case "end_at":
128 | state["config"]["end"] = new Date(config.value);
129 | break;
130 | }
131 | });
132 | return state;
133 | case UPDATE_COUNTDOWN:
134 | state["countdown"] = action.data;
135 | return state;
136 | case SET_VISIBLE_CATEGORY:
137 | state["visibleLevel"] = -1;
138 | state["visibleCategory"] = action.data;
139 | return state;
140 | case SET_VISIBLE_LEVEL:
141 | state["visibleCategory"] = -1;
142 | state["visibleLevel"] = action.data;
143 | return state;
144 | case SET_HIDDEN_SOLVED:
145 | state["hiddenSolved"] = action.data;
146 | return state;
147 | case RESET_EVENTS:
148 | switch (action.data) {
149 | case "problems":
150 | state["events"]["problems"] = 0;
151 | break;
152 | case "announcements":
153 | state["events"]["announcements"] = 0;
154 | break;
155 | default:
156 | console.log(JSON.stringify(action.data));
157 | }
158 | return state;
159 | case EE:
160 | state["easteregg"] = action.data;
161 | return state;
162 | default:
163 | console.log(JSON.stringify(action));
164 | return state;
165 | }
166 | }
167 |
168 | export default createStore(dataStore);
169 |
--------------------------------------------------------------------------------
/src/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Router from 'react-router';
3 | import {Provider} from 'react-redux';
4 | import Store from './js/data/store';
5 | import Login from './js/pages/login';
6 | import Dashboard from './js/pages/dashboard';
7 | import Problems from './js/pages/problems';
8 | import Problem from './js/pages/problem';
9 | import Ranking from './js/pages/ranking';
10 | import Announcements from './js/pages/announcements';
11 | import Main from './js/pages/main';
12 | import {Top} from './js/pages/top';
13 | var remote = require('remote');
14 | var clipboard = remote.require('clipboard');
15 | var DefaultRoute = Router.DefaultRoute;
16 | var Link = Router.Link;
17 | var Route = Router.Route;
18 |
19 | var routes = (
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | );
34 |
35 | // Patch for Electron on OS X Copy/Paste bug
36 | function CopyPasteFunc(e){
37 | // Command key presses
38 | if (!e.ctrlKey && e.metaKey && !e.altKey && !e.shiftKey) {
39 | if (e.keyCode === 67) {
40 | // and key 'C' pressed
41 | var selectedText = window.getSelection().toString();
42 | if (selectedText) {
43 | clipboard.writeText(selectedText);
44 | }
45 | } else if (e.keyCode === 86){
46 | // and key 'V' pressed
47 | var activeElement = document.activeElement;
48 | var clipboardText = clipboard.readText();
49 | if (clipboardText && activeElement && activeElement.type == "text") {
50 | var currentText = activeElement.value;
51 | var cursorPosition = activeElement.selectionStart;
52 | if (cursorPosition != activeElement.selectionEnd) {
53 | currentText = currentText.slice(0, cursorPosition) + currentText.slice(activeElement.selectionEnd);
54 | }
55 | activeElement.value = currentText.slice(0, cursorPosition) + clipboardText + currentText.slice(cursorPosition);
56 | activeElement.selectionStart = activeElement.selectionEnd = cursorPosition + clipboardText.length;
57 | }
58 | }
59 | }
60 | }
61 | document.addEventListener("keydown", CopyPasteFunc);
62 |
63 | Router.run(routes, (Handler, routerState) => {
64 | React.render((
65 |
66 | {() => }
67 |
68 | ), document.getElementById("content"));
69 | })
70 |
--------------------------------------------------------------------------------
/src/pages/announcements.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Api from '../api/api'
3 | import {connect} from 'react-redux';
4 | import {UPDATE_ANNOUNCEMENTS, RESET_EVENTS} from '../data/store'
5 | global.React = React;
6 | var md2react = require('md2react');
7 |
8 | class Announcements extends React.Component {
9 | constructor(props) {
10 | super(props);
11 | this.state = {
12 | updated: null,
13 | error: null
14 | };
15 | }
16 | componentWillMount() {
17 | // Tap tap API server
18 | Api.announcements((json) => {
19 | this.props.updateAnnouncements(json);
20 | this.setState({
21 | updated: Date()
22 | });
23 | }, (mes) => {
24 | this.setState({
25 | error: mes
26 | });
27 | })
28 | this.props.resetEvents();
29 | }
30 | clearError() {
31 | this.setState({
32 | error: null
33 | });
34 | }
35 | render() {
36 | const {announcements} = this.props;
37 | var contents = announcements.map((announcement, index) => {
38 | return (
39 |
40 |
41 |
42 |
43 |
44 |
45 | {announcement["title"]}
46 |
47 | {new Date(announcement["updated_at"]).toTimeString()}
48 |
49 |
50 |
51 | {md2react(announcement["body"], {gfm: true, tables: true})}
52 |
53 |
54 |
55 |
56 |
57 | );
58 | });
59 | if (!contents) contents = No announcement
60 | var errorMessage;
61 | if (this.state.error) {
62 | errorMessage = (
63 |
64 |
65 |
66 | Error
67 |
68 |
{this.state.error[0]}
69 |
70 | );
71 | }
72 | return (
73 |
74 |
Last updated: {this.state.updated}
75 |
76 | {contents}
77 |
78 |
79 |
80 | {errorMessage}
81 |
82 | );
83 | }
84 | };
85 |
86 | export default connect(
87 | (state) => ({
88 | announcements: state.announcements
89 | }),
90 | (dispatch) => ({
91 | updateAnnouncements: (data) => dispatch({type: UPDATE_ANNOUNCEMENTS, data: data}),
92 | resetEvents: () => dispatch({type: RESET_EVENTS, data: "announcements"})
93 | })
94 | )(Announcements);
95 |
--------------------------------------------------------------------------------
/src/pages/dashboard.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Api from '../api/api'
3 | import {connect} from 'react-redux';
4 | import {UPDATE_TEAMLIST} from '../data/store'
5 |
6 | class Dashboard extends React.Component {
7 | constructor(props) {
8 | super(props);
9 | this.state = {
10 | error: null
11 | };
12 | }
13 | componentWillMount() {
14 | // Tap tap API server
15 | Api.teamlist((json) => {
16 | this.props.updateTeamlist(json);
17 | }, (mes) => {
18 | this.setState({
19 | error: mes
20 | });
21 | })
22 | this.timer = setInterval(() => {
23 | this.setState({now: new Date()})
24 | }.bind(this), 1000);
25 | }
26 | componentWillUnmount() {
27 | clearInterval(this.timer);
28 | }
29 | clearError() {
30 | this.setState({
31 | error: null
32 | });
33 | }
34 | render() {
35 | const {point, solved, teamlist, ranking, countdown} = this.props;
36 | var errorMessage;
37 | if (this.state.error) {
38 | errorMessage = (
39 |
40 |
41 |
42 | Error
43 |
44 |
{this.state.error[0]}
45 |
46 | );
47 | }
48 | return (
49 |
50 |
Countdown
51 |
52 |
53 |
54 |
55 | {countdown.h || 'n/a'}
56 |
57 |
58 | Hours
59 |
60 |
61 |
62 |
63 | {countdown.m || 'n/a'}
64 |
65 |
66 | Minutes
67 |
68 |
69 |
70 |
71 | {countdown.s || 'n/a'}
72 |
73 |
74 | Seconds
75 |
76 |
77 |
78 |
79 |
Statistics
80 |
81 |
82 |
83 |
84 |
Ranking
85 |
86 | {ranking}
87 |
88 |
89 | of {teamlist.length}
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
Score
98 |
99 | {point}
100 |
101 |
102 | points
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
Flags
111 |
112 | {solved}
113 |
114 |
115 | captured
116 |
117 |
118 |
119 |
120 |
121 | {errorMessage}
122 |
123 | );
124 | }
125 | };
126 |
127 | export default connect(
128 | (state) => ({
129 | solved: state.solved,
130 | point: state.point,
131 | teamlist: state.teamList,
132 | ranking: state.ranking,
133 | start: state.config.start,
134 | end: state.config.end,
135 | countdown: state.countdown
136 | }),
137 | (dispatch) => ({updateTeamlist: (data) => dispatch({type: UPDATE_TEAMLIST, data: data})})
138 | )(Dashboard);
139 |
--------------------------------------------------------------------------------
/src/pages/login.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Api from '../api/api'
3 | import {connect} from 'react-redux';
4 | import {UPDATE_USERINFO} from '../data/store'
5 |
6 | class Login extends React.Component {
7 | constructor(props) {
8 | super(props);
9 | this.state = {
10 | username: "",
11 | password: "",
12 | login_pending: false,
13 | signup_pending: false,
14 | error: false,
15 | message: "",
16 | success: false
17 | };
18 | }
19 | componentWillMount() {
20 | document.body.style.backgroundColor = "#f7c5d2";
21 | if (this.props && this.props.query && this.props.query.message) {
22 | this.setState({
23 | error: true,
24 | message: this.props.query.message
25 | });
26 | }
27 | }
28 | componentWillUnmount() {
29 | document.body.style.backgroundColor = null;
30 | }
31 | changeText(e) {
32 | var value = {}
33 | value[e.target.name] = e.target.value;
34 | this.setState(value);
35 | }
36 | tryLogin() {
37 | this.setState({
38 | login_pending: true,
39 | error: false,
40 | success: false
41 | });
42 |
43 | Api.login(this.state.username, this.state.password, (userinfo) => {
44 | this.props.saveUserinfo(userinfo);
45 | this.context.router.transitionTo("dashboard");
46 | }, (mes) => {
47 | this.setState({
48 | login_pending: false,
49 | error: true,
50 | message: mes[0]
51 | });
52 | })
53 | }
54 | signUp() {
55 | this.setState({
56 | signup_pending: true,
57 | error: false,
58 | success: false
59 | });
60 |
61 | Api.signup(this.state.username, this.state.password, (userinfo) => {
62 | this.setState({
63 | signup_pending: false,
64 | success: true
65 | });
66 | React.render(New account created
Press "Login" to start CTF!
, document.querySelector('.ui.success.message'));
67 | }, (mes) => {
68 | this.setState({
69 | signup_pending: false,
70 | error: true,
71 | message: mes[0]
72 | });
73 | })
74 | return true
75 | }
76 | render() {
77 | var size400 = {
78 | paddingTop: "20px",
79 | paddingBottom: "10px",
80 | maxWidth: "400px",
81 | maxHeight: "400px"
82 | };
83 | var fullHeight = {
84 | height: "100%"
85 | };
86 | return (
87 |
88 |
89 |

90 |
91 |
92 | Welcome to Lepus-CTF
93 |
94 |
95 |
119 |
120 |
121 | );
122 | }
123 | };
124 | Login.contextTypes = {
125 | router: React.PropTypes.func.isRequired
126 | };
127 |
128 | export default connect(
129 | (state) => ({}),
130 | (dispatch) => ({saveUserinfo: (data) => dispatch({type: UPDATE_USERINFO, data: data})})
131 | )(Login);
132 |
--------------------------------------------------------------------------------
/src/pages/main.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Api from '../api/api'
3 | import {connect} from 'react-redux';
4 | import {UPDATE_TEAMINFO, UPDATE_SERVEREVENT, UPDATE_CTFCONF, UPDATE_COUNTDOWN, EE} from '../data/store'
5 | import Router from 'react-router';
6 | var DefaultRoute = Router.DefaultRoute;
7 | var Link = Router.Link;
8 | var Route = Router.Route;
9 | var RouteHandler = Router.RouteHandler;
10 |
11 | class Main extends React.Component {
12 | constructor(props) {
13 | super(props);
14 | this.state = {url: "", breakingnews: true, playOnce: false, error: null};
15 | this.watchServerEvent();
16 | this.getCTFConfigurations();
17 | }
18 | static willTransitionTo(transition) {
19 | //TODO: check logon status
20 | var logon = true;
21 | if (!logon){
22 | transition.redirect('/login');
23 | }
24 | }
25 | updateTeaminfo() {
26 | Api.team(this.props.userinfo.team, (json) => {
27 | this.props.updateTeaminfo(json);
28 | }, (mes) => {
29 | this.setState({
30 | error: mes
31 | });
32 | })
33 | }
34 | getCTFConfigurations() {
35 | Api.configurations((json) => {
36 | this.props.updateCTFConfigurations(json);
37 | }, (mes) => {
38 | this.setState({
39 | error: mes
40 | });
41 | })
42 | }
43 | watchServerEvent() {
44 | var socket = require('socket.io-client');
45 | var io = socket('SCORE_SERVER_URL', {
46 | secure: true,
47 | transports: ["websocket"]
48 | });
49 | io.on('connect', () => {
50 | console.log("Socket.io connected!")
51 | });
52 | io.on('event', (data) => {
53 | switch (data.type) {
54 | case "update":
55 | this.props.onReceiveServerEvent(data);
56 | switch (data.model) {
57 | case "notice":
58 | setTimeout(function() {
59 | Api.announcement(data.id, (json) => {
60 | var n = new Notification('Lepus-CTF - Announcements', {
61 | body: json.title
62 | });
63 | n.onclick = () => {
64 | this.context.router.transitionTo("announcements");
65 | }.bind(this);
66 | }.bind(this), (mes) => {
67 | var n = new Notification('Lepus-CTF - Announcements', {
68 | body: 'New 1 announcement'
69 | });
70 | n.onclick = () => {
71 | this.context.router.transitionTo("announcements");
72 | }.bind(this);
73 | }.bind(this))
74 | }.bind(this), ~~(Math.random() * 1000));
75 | break;
76 | case "question":
77 | console.log(JSON.stringify(data));
78 | setTimeout(function() {
79 | Api.problem(data.id, (json) => {
80 | var n = new Notification('Lepus-CTF - Problems', {
81 | body: json.title + ' updated'
82 | });
83 | n.onclick = () => {
84 | this.context.router.transitionTo("problems");
85 | }.bind(this);
86 | }.bind(this), (mes) => {
87 | var n = new Notification('Lepus-CTF - Problems', {
88 | body: 'New 1 problem update'
89 | });
90 | n.onclick = () => {
91 | this.context.router.transitionTo("problems");
92 | }.bind(this);
93 | }.bind(this))
94 | }.bind(this), ~~(Math.random() * 1000));
95 | break;
96 | }
97 | break;
98 | case "answer":
99 | if (data.team === this.props.userinfo.team) {
100 | this.updateTeaminfo();
101 | }
102 | break;
103 | case "youtube":
104 | this.setState({url: "https://www.youtube.com/embed/" + data["video_id"] + "?autoplay=1&controls=0&fs=0&showinfo=0&rel=0&disablekb=1&modestbranding=1"});
105 | if (!this.state.breakingnews) {
106 | var n = new Notification('Lepus-CTF', {
107 | body: 'Breaking news!'
108 | });
109 | n.onclick = () => {
110 | this.setState({playOnce: true});
111 | };
112 | }
113 | break;
114 | default:
115 | console.log(JSON.stringify(data));
116 | }
117 | this.setState({
118 | render: new Date()
119 | });
120 | }.bind(this));
121 | io.on('disconnect', () => {
122 | console.warn("Socket.io disconnected.");
123 | });
124 | }
125 | componentWillMount() {
126 | document.body.style.backgroundColor = "#1abc9c";
127 | setInterval(() => {
128 | if (!!this.props.start && !!this.props.end) {
129 | const now = new Date();
130 | if (now - this.props.start < 0) {
131 | return;
132 | } else if (this.props.end - now < 0) {
133 | this.props.updateCountdown(null, null, null);
134 | return;
135 | }
136 | const diff = new Date(this.props.end - now);
137 | const time = diff.toUTCString().replace(/^.*(\d\d:\d\d:\d\d).*$/,'$1');
138 | const h = time.replace(/^(\d\d):\d\d:\d\d$/,'$1');
139 | const m = time.replace(/^\d\d:(\d\d):\d\d$/,'$1');
140 | const s = time.replace(/^\d\d:\d\d:(\d\d)$/,'$1');
141 | this.props.updateCountdown(h, m, s);
142 | }
143 | }.bind(this), 1000);
144 | Api.setCriticalAction((() => {this.context.router.transitionTo("login", {}, {message: "You need to re-login."})}.bind(this)));
145 | }
146 | componentWillUnmount() {
147 | document.body.style.backgroundColor = null;
148 | }
149 | unsetBreakingnews() {
150 | this.setState({url: null, breakingnews: false})
151 | }
152 | closeBreakingnewsModal() {
153 | this.setState({url: null, playOnce: false})
154 | }
155 | closeEEModal() {
156 | this.props.resetEE();
157 | }
158 | clearError() {
159 | this.setState({
160 | error: null
161 | });
162 | }
163 | render() {
164 | var mainStyle = {
165 | height: "100%",
166 | padding: "20px",
167 | paddingLeft: "220px",
168 | };
169 | var maxSize = {
170 | maxHeight: "100%",
171 | maxWidth: "100%",
172 | };
173 | var centeredModal = {
174 | height: "70%",
175 | marginTop: "-20%",
176 | }
177 | const {point, solved, events, start, end, countdown} = this.props;
178 | var modal;
179 | if (this.state.url && (this.state.breakingnews || this.state.playOnce)) {
180 | modal = (
181 |
182 |
183 |
184 |
185 |
186 |
187 |
I'll not watch BREAKING NEWS any more.
188 |
189 |
190 |
191 |
192 | );
193 | } else if (this.props.ee) {
194 | modal = (
195 |
196 |
197 |

198 |
199 |
200 | );
201 | }
202 | var count = "Countdown";
203 | const now = new Date();
204 | if (now - start < 0) {
205 | count = start.toString().replace(/^.*(\d\d:\d\d):\d\d.*$/,'$1') + " start."
206 | } else if (end - now < 0) {
207 | count = end.toString().replace(/^.*(\d\d:\d\d):\d\d.*$/,'$1') + " closed."
208 | } else if (countdown.h) {
209 | count = countdown.h + ':' + countdown.m + ':' + countdown.s;
210 | }
211 | var errorMessage;
212 | if (this.state.error) {
213 | errorMessage = (
214 |
215 |
216 |
217 | Error
218 |
219 |
{this.state.error[0]}
220 |
221 | );
222 | }
223 | return (
224 |
225 |
226 |
227 |
228 |
Lepus-CTF
229 |
230 |
231 |
232 | {count}
233 |
234 |
235 |
236 |
237 |
238 |
239 | {point}
240 |
241 |
242 | Points
243 |
244 |
245 |
246 |
247 | {solved}
248 |
249 |
250 | Solved
251 |
252 |
253 |
254 |
255 |
Dashboard
256 |
257 | {events.problems > 0 ?
{events.problems}
: "" }
258 | Problems
259 |
260 |
261 | Ranking
262 |
263 |
264 | {events.announcements > 0 ?
{events.announcements}
: "" }
265 | Announcements
266 |
267 |
268 | {modal}
269 | {errorMessage}
270 |
271 | );
272 | }
273 | };
274 | Main.contextTypes = {
275 | router: React.PropTypes.func.isRequired
276 | };
277 |
278 |
279 | export default connect(
280 | (state) => ({
281 | userinfo: state.userInfo,
282 | point: state.point,
283 | solved: state.solved,
284 | events: state.events,
285 | start: state.config.start,
286 | end: state.config.end,
287 | ee: state.easteregg,
288 | countdown: state.countdown
289 | }),
290 | (dispatch) => ({
291 | updateTeaminfo: (data) => dispatch({type: UPDATE_TEAMINFO, data: data}),
292 | onReceiveServerEvent: (data) => dispatch({type: UPDATE_SERVEREVENT, data: data}),
293 | updateCTFConfigurations: (data) => dispatch({type: UPDATE_CTFCONF, data: data}),
294 | updateCountdown: (h, m, s) => dispatch({type: UPDATE_COUNTDOWN, data: {h, m, s}}),
295 | resetEE: () => dispatch({type: EE, data: false}),
296 | })
297 | )(Main);
298 |
--------------------------------------------------------------------------------
/src/pages/problem.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Router from 'react-router';
3 | import Api from '../api/api'
4 | import {connect} from 'react-redux';
5 | import {UPDATE_A_PROBLEM, EE} from '../data/store'
6 | global.React = React;
7 | var md2react = require('md2react');
8 | var remote = require('remote');
9 | var dialog = remote.require('dialog');
10 | var clipboard = remote.require('clipboard');
11 | var fs = remote.require('fs');
12 | var Link = Router.Link;
13 |
14 | class Problem extends React.Component {
15 | constructor(props) {
16 | super(props);
17 | this.state = {
18 | copiedLink: "",
19 | flagText: "",
20 | downloading: {},
21 | pending: false,
22 | failed: false,
23 | correct: false,
24 | error: null,
25 | answerCount: 0
26 | };
27 | }
28 | componentWillMount() {
29 | // Tap tap API server
30 | Api.problem(this.props.params.id, (json) => {
31 | this.props.updateAProblem(json);
32 | }, (mes) => {
33 | this.setState({
34 | error: mes
35 | });
36 | })
37 | }
38 | changeText(e) {
39 | var value = {}
40 | value[e.target.name] = e.target.value;
41 | this.setState(value);
42 | }
43 | submitFlag() {
44 | this.setState({
45 | pending: true,
46 | failed: false
47 | });
48 |
49 | console.log(this.state.flagText);
50 | Api.submitFlag(this.props.params.id, this.state.flagText, () => {
51 | this.setState({
52 | correct: true
53 | });
54 | React.render(Congratulations
{this.state.flagText} is correct!!
, document.querySelector('.ui.success.message'));
55 | }, (mes) => {
56 | this.setState({
57 | pending: false,
58 | failed: true,
59 | answerCount: this.state.answerCount + 1
60 | });
61 | React.render(, document.querySelector('.ui.error.message'));
62 | if (this.state.answerCount > 5) {
63 | this.props.tooManyWrongAnswer();
64 | this.state.answerCount = 0;
65 | }
66 | })
67 | return false;
68 | }
69 | saveFile(filename, filepath) {
70 | if (this.state.downloading[filename]) {
71 | return;
72 | }
73 | console.log("File saving");
74 | dialog.showSaveDialog({ defaultPath: filename}, (savepath) => {
75 | if (savepath === undefined) {
76 | console.log("File saving canceld");
77 | return;
78 | }
79 | var downloadState = this.state.downloading;
80 | downloadState[filename] = true;
81 | this.setState({downloading: downloadState});
82 | Api.downloadFile(filepath, savepath, () => {
83 | console.log("File saving done");
84 | var downloadState = this.state.downloading;
85 | downloadState[filename] = false;
86 | this.setState({
87 | downloading: downloadState
88 | });
89 | }.bind(this), (err) => {
90 | console.error("File download error");
91 | var downloadState = this.state.downloading;
92 | downloadState[filename] = false;
93 | this.setState({
94 | error: ["File downloading error, please copy link."],
95 | downloading: downloadState
96 | });
97 | }.bind(this));
98 | }.bind(this));
99 | }
100 | clearError() {
101 | this.setState({
102 | error: null
103 | });
104 | }
105 | copyDownloadLink(name, path) {
106 | clipboard.writeText(Api.serverUrl + path);
107 | this.setState({
108 | copiedLink: name
109 | });
110 | setTimeout(function() {
111 | this.setState({
112 | copiedLink: ""
113 | });
114 | }.bind(this), 3000);
115 | }
116 | render() {
117 | const {teaminfo, problems} = this.props;
118 | var problem = problems[this.props.params.id];
119 | if (!problem) return (Cannot find a problem.
);
120 | var problem_status = {};
121 | if (teaminfo && teaminfo.questions) {
122 | teaminfo.questions.forEach((t_state) => {
123 | if (problem.id == t_state.id) {
124 | problem_status = t_state;
125 | }
126 | }.bind(this))
127 | }
128 | var progress = Math.round(~~problem_status.points / problem["points"] * 100);
129 | var progressStyle = {
130 | width: progress + "%"
131 | };
132 | var attachments = problem["files"].map((file) => {
133 | var button;
134 | if (file["size"] > 5 * 1024 * 1024) { // 5MB
135 | button = (
136 |
137 |
141 |
142 | );
143 | } else {
144 | button = (
145 |
146 |
150 |
153 |
154 | );
155 | }
156 | return (
157 |
158 | {button}
159 |
{file["size"]} bytes
160 |
161 | );
162 | }.bind(this));
163 | var errorMessage;
164 | if (this.state.error) {
165 | errorMessage = (
166 |
167 |
168 |
169 | Error
170 |
171 |
{this.state.error[0]}
172 |
173 | );
174 | }
175 | return (
176 |
177 |
178 | Problems
179 |
180 | {problem["title"]}
181 |
182 |
183 |
184 |
185 |
{problem["title"]}
186 |
187 | {problem["points"]} points
188 |
189 |
190 |
191 | {problem["category"]["name"]}
192 | {md2react(problem["sentence"], {gfm: true, tables: true})}
193 |
194 |
195 |
196 | {attachments}
197 |
198 |
199 |
200 |
201 |
214 |
215 |
216 | {errorMessage}
217 |
218 | );
219 | }
220 | };
221 |
222 | export default connect(
223 | (state) => ({
224 | teaminfo: state.teamInfo,
225 | problems: state.problems
226 | }),
227 | (dispatch) => ({
228 | updateAProblem: (data) => dispatch({type: UPDATE_A_PROBLEM, data: data}),
229 | tooManyWrongAnswer: () => dispatch({type: EE, data: true}),
230 | })
231 | )(Problem);
232 |
--------------------------------------------------------------------------------
/src/pages/problems.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Api from '../api/api'
3 | import {connect} from 'react-redux';
4 | import {UPDATE_PROBLEMS, RESET_EVENTS, SET_VISIBLE_CATEGORY, SET_VISIBLE_LEVEL, SET_HIDDEN_SOLVED} from '../data/store'
5 | import Router from 'react-router';
6 | var Link = Router.Link;
7 |
8 | class Problems extends React.Component {
9 | constructor(props) {
10 | super(props);
11 | this.state = {
12 | searchBoxFocused: false,
13 | error: null
14 | };
15 | }
16 | componentWillMount() {
17 | // Tap tap API server
18 | Api.problems((json) => {
19 | this.props.updateProblems(json);
20 | this.setState();
21 | }, (mes) => {
22 | this.setState({
23 | error: mes
24 | });
25 | })
26 | this.props.resetEvents();
27 | }
28 | toggleSolvedVisibleState(e) {
29 | this.props.hideSolved(e.target.checked);
30 | }
31 | onSearchBoxFocus(e) {
32 | this.setState({
33 | searchBoxFocused: true
34 | })
35 | }
36 | onCategoryClick(e) {
37 | this.props.setVisibleCategory(e.target.getAttribute("data-id"));
38 | this.setState({
39 | searchBoxFocused: false
40 | })
41 | }
42 | onLevelClick(e) {
43 | this.props.setVisibleLevel(e.target.getAttribute("data-id"));
44 | this.setState({
45 | searchBoxFocused: false
46 | })
47 | }
48 | clearCatrgory() {
49 | this.props.setVisibleCategory(-1);
50 | this.setState({
51 | searchBoxFocused: false
52 | })
53 | }
54 | clearError() {
55 | this.setState({
56 | error: null
57 | });
58 | }
59 | render() {
60 | const colors = ["red", "orange", "yellow", "olive", "green", "teal", "blue", "violet", "purple", "pink", "brown", "gray"];
61 | const {teaminfo, solvedTeams, problems, hiddenSolved, visibleCategory, visibleLevel} = this.props;
62 | var problem_status = {};
63 | if (teaminfo.questions) {
64 | teaminfo.questions.forEach((problem) => {
65 | problem_status[problem.id] = problem;
66 | })
67 | }
68 | var maxPoint = -1;
69 | var categories = {};
70 | var cards = [];
71 | for (var id in problems) {
72 | var problem = problems[id];
73 | var difficulty = [];
74 | for (var i = 0; i < problem["points"]; i+=100) {
75 | difficulty.push();
76 | }
77 | maxPoint = maxPoint > problem["points"] ? maxPoint : problem["points"];
78 |
79 | categories[problem["category"]["name"]] = problem["category"]["id"];
80 | if (visibleCategory >= 0 && visibleCategory != problem["category"]["id"]) {
81 | continue
82 | } else if (visibleLevel > 0 && ((visibleLevel * 100) < problem["points"] || ((visibleLevel - 1) * 100) >= problem["points"])) {
83 | continue
84 | }
85 | var solved;
86 | if (problem_status[problem["id"]]) {
87 | if (problem_status[problem["id"]].points == problem["points"]) {
88 | if (hiddenSolved) {
89 | continue
90 | }
91 | solved = Solved
92 | } else {
93 | solved = {Math.round(problem_status[problem["id"]].points / problem["points"] * 100)}% Solved
94 | }
95 | }
96 | cards.push(
97 |
98 |
99 |
{problem["title"]}
100 |
{problem["category"]["name"]}
101 |
102 |
103 | {problem["points"]}
104 |
105 |
106 | Points
107 |
108 |
109 | {solved}
110 |
111 |
112 | Difficulty:
113 | {difficulty}
114 |
115 |
116 |
117 | {~~solvedTeams[problem.id]} teams solved
118 |
119 |
122 |
123 | );
124 | }
125 | var searchInput = "";
126 | var categoryList = [
127 |
132 | ];
133 | for (var key in categories) {
134 | categoryList.push(
135 |
140 | );
141 | if (visibleCategory == categories[key]) {
142 | searchInput = "Category: " + key;
143 | }
144 | }
145 | var levelList = [
146 |
151 | ];
152 | for (var i = 100; i <= maxPoint; i += 100) {
153 | var difficulty = [];
154 | for (var j = 0; j < i; j += 100) {
155 | difficulty.push();
156 | }
157 | levelList.push(
158 |
163 | );
164 | if (visibleLevel == ~~(i / 100)) {
165 | searchInput = "Difficulty: " + ~~(i / 100);
166 | }
167 | }
168 | var errorMessage;
169 | if (this.state.error) {
170 | errorMessage = (
171 |
172 |
173 |
174 | Error
175 |
176 |
{this.state.error[0]}
177 |
178 | );
179 | }
180 | return (
181 |
182 |
183 | Problems
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
Category
201 | {categoryList}
202 |
203 |
204 |
Level
205 | {levelList}
206 |
207 |
208 |
209 |
210 |
211 | {cards}
212 |
213 |
214 |
215 | {errorMessage}
216 |
217 | );
218 | }
219 | };
220 |
221 | export default connect(
222 | (state) => ({
223 | teaminfo: state.teamInfo,
224 | solvedTeams: state.solvedTeams,
225 | problems: state.problems,
226 | visibleCategory: state.visibleCategory,
227 | visibleLevel: state.visibleLevel,
228 | hiddenSolved: state.hiddenSolved
229 | }),
230 | (dispatch) => ({
231 | updateProblems: (data) => dispatch({type: UPDATE_PROBLEMS, data: data}),
232 | resetEvents: () => dispatch({type: RESET_EVENTS, data: "problems"}),
233 | setVisibleCategory: (data) => dispatch({type: SET_VISIBLE_CATEGORY, data: data}),
234 | setVisibleLevel: (data) => dispatch({type: SET_VISIBLE_LEVEL, data: data}),
235 | hideSolved: (data) => dispatch({type: SET_HIDDEN_SOLVED, data: data}),
236 | })
237 | )(Problems);
238 |
--------------------------------------------------------------------------------
/src/pages/ranking.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Api from '../api/api'
3 | import {connect} from 'react-redux';
4 | import {UPDATE_TEAMLIST} from '../data/store'
5 |
6 | class Ranking extends React.Component {
7 | constructor(props) {
8 | super(props);
9 | this.state = {
10 | updated: null,
11 | error: null
12 | };
13 | }
14 | componentWillMount() {
15 | // Tap tap API server
16 | Api.teamlist((json) => {
17 | this.props.updateTeamlist(json);
18 | this.setState({
19 | updated: Date()
20 | });
21 | }, (mes) => {
22 | this.setState({
23 | error: mes
24 | });
25 | })
26 | }
27 | clearError() {
28 | this.setState({
29 | error: null
30 | });
31 | }
32 | render() {
33 | const {teaminfo, teamlist} = this.props;
34 | var ranking = teamlist.map((team, index) => {
35 | return (
36 |
37 | {index + 1} |
38 | {team.name} |
39 | {team.points} |
40 |
41 | );
42 | });
43 | var errorMessage;
44 | if (this.state.error) {
45 | errorMessage = (
46 |
47 |
48 |
49 | Error
50 |
51 |
{this.state.error[0]}
52 |
53 | );
54 | }
55 | return (
56 |
57 |
Last updated: {this.state.updated}
58 |
59 |
60 |
61 | Rank |
62 | Team |
63 | Points |
64 |
65 |
66 |
67 | {ranking}
68 |
69 |
70 |
71 |
72 | {errorMessage}
73 |
74 | );
75 | }
76 | };
77 |
78 | export default connect(
79 | (state) => ({
80 | teaminfo: state.teamInfo,
81 | teamlist: state.teamList
82 | }),
83 | (dispatch) => ({updateTeamlist: (data) => dispatch({type: UPDATE_TEAMLIST, data: data})})
84 | )(Ranking);
85 |
--------------------------------------------------------------------------------
/src/pages/top.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Router from 'react-router';
3 | var RouteHandler = Router.RouteHandler;
4 |
5 | export class Top extends React.Component {
6 | render() {
7 | return (
8 |
9 | );
10 | }
11 | };
12 |
--------------------------------------------------------------------------------