Skip to content

Commit b0311bd

Browse files
authored
Merge branch 'master' into feat/footer
2 parents 768915f + 9102953 commit b0311bd

File tree

12 files changed

+253
-21
lines changed

12 files changed

+253
-21
lines changed

client/src/api/admin/challs.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { request } from '../util'
1+
import { request, handleResponse } from '../util'
22

33
export const getChallenges = async () => {
44
return (await request('GET', '/admin/challs')).data
@@ -11,3 +11,11 @@ export const updateChallenge = async ({ id, data }) => {
1111
export const deleteChallenge = async ({ id }) => {
1212
return (await request('DELETE', `/admin/challs/${encodeURIComponent(id)}`)).data
1313
}
14+
15+
export const uploadFiles = async ({ files }) => {
16+
const resp = await request('POST', '/admin/upload', {
17+
files
18+
})
19+
20+
return handleResponse({ resp, valid: ['goodFilesUpload'] })
21+
}

client/src/components/admin/problem.js

+108-6
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import withStyles from '../../components/jss'
33
import { useState, useCallback } from 'preact/hooks'
44
import Modal from '../../components/modal'
55

6-
import { updateChallenge, deleteChallenge } from '../../api/admin/challs'
6+
import { updateChallenge, deleteChallenge, uploadFiles } from '../../api/admin/challs'
77
import { useToast } from '../../components/toast'
8+
import { encodeFile } from '../../util'
89

910
const DeleteModal = withStyles({
1011
modalBody: {
@@ -50,7 +51,7 @@ const DeleteModal = withStyles({
5051
)
5152
})
5253

53-
const Problem = ({ classes, problem }) => {
54+
const Problem = ({ classes, problem, update: updateClient }) => {
5455
const { toast } = useToast()
5556

5657
const [flag, setFlag] = useState(problem.flag)
@@ -68,20 +69,93 @@ const Problem = ({ classes, problem }) => {
6869
const [name, setName] = useState(problem.name)
6970
const handleNameChange = useCallback(e => setName(e.target.value), [])
7071

71-
const handleUpdate = useCallback(e => {
72+
const [minPoints, setMinPoints] = useState(problem.points.min)
73+
const handleMinPointsChange = useCallback(e => setMinPoints(e.target.value), [])
74+
75+
const [maxPoints, setMaxPoints] = useState(problem.points.max)
76+
const handleMaxPointsChange = useCallback(e => setMaxPoints(e.target.value), [])
77+
78+
const handleFileUpload = useCallback(async e => {
79+
e.preventDefault()
80+
81+
const fileData = await Promise.all(
82+
Array.from(e.target.files)
83+
.map(async file => {
84+
const data = await encodeFile(file)
85+
86+
return {
87+
data,
88+
name: file.name
89+
}
90+
})
91+
)
92+
93+
const fileUpload = await uploadFiles({
94+
files: fileData
95+
})
96+
97+
if (fileUpload.error) {
98+
toast({ body: fileUpload.error, type: 'error' })
99+
return
100+
}
101+
102+
const data = await updateChallenge({
103+
id: problem.id,
104+
data: {
105+
files: fileUpload.data.concat(problem.files)
106+
}
107+
})
108+
109+
e.target.value = null
110+
111+
updateClient({
112+
problem: data
113+
})
114+
115+
toast({ body: 'Problem successfully updated' })
116+
}, [problem.id, problem.files, updateClient, toast])
117+
118+
const handleRemoveFile = file => async () => {
119+
const newFiles = problem.files.filter(f => f !== file)
120+
121+
const data = await updateChallenge({
122+
id: problem.id,
123+
data: {
124+
files: newFiles
125+
}
126+
})
127+
128+
updateClient({
129+
problem: data
130+
})
131+
132+
toast({ body: 'Problem successfully updated' })
133+
}
134+
135+
const handleUpdate = async e => {
72136
e.preventDefault()
73137

74-
updateChallenge({
138+
const data = await updateChallenge({
75139
id: problem.id,
76140
data: {
77141
flag,
78142
description,
79143
category,
80144
author,
81-
name
145+
name,
146+
points: {
147+
min: minPoints,
148+
max: maxPoints
149+
}
82150
}
83151
})
84-
}, [problem, flag, description, category, author, name])
152+
153+
updateClient({
154+
problem: data
155+
})
156+
157+
toast({ body: 'Problem successfully updated' })
158+
}
85159

86160
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
87161
const openDeleteModal = useCallback(e => {
@@ -117,6 +191,8 @@ const Problem = ({ classes, problem }) => {
117191
</div>
118192
<div class={`col-6 ${classes.header}`}>
119193
<input class='form-group-input input-small' placeholder='Author' value={author} onChange={handleAuthorChange} />
194+
<input class='form-group-input input-small' type='number' value={minPoints} onChange={handleMinPointsChange} />
195+
<input class='form-group-input input-small' type='number' value={maxPoints} onChange={handleMaxPointsChange} />
120196
</div>
121197
</div>
122198

@@ -127,6 +203,32 @@ const Problem = ({ classes, problem }) => {
127203
<input class='form-group-input input-small' placeholder='Flag' value={flag} onChange={handleFlagChange} />
128204
</div>
129205

206+
{
207+
problem.files.length !== 0 &&
208+
<div>
209+
<p class='faded frame__subtitle u-no-margin'>Downloads</p>
210+
<div class='tag-container'>
211+
{
212+
problem.files.map(file => {
213+
return (
214+
<div class='tag' key={file.url}>
215+
<a native download href={file.url}>
216+
{file.name}
217+
</a>
218+
<div class='tag tag--delete' style='margin: 0; margin-left: 3px' onClick={handleRemoveFile(file)} />
219+
</div>
220+
)
221+
})
222+
}
223+
224+
</div>
225+
</div>
226+
}
227+
228+
<div class='input-control'>
229+
<input class='form-group-input input-small' placeholder='Flag' type='file' multiple onChange={handleFileUpload} />
230+
</div>
231+
130232
<div class={`form-section ${classes.controls}`}>
131233
<button class='btn-small btn-info'>Update</button>
132234
<button class='btn-small btn-danger' onClick={openDeleteModal} type='button' >Delete</button>

client/src/routes/admin/challs.js

+32-10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useEffect } from 'preact/hooks'
1+
import { useState, useEffect, useCallback, useMemo } from 'preact/hooks'
22
import { v4 as uuid } from 'uuid'
33

44
import config from '../../config'
@@ -14,16 +14,22 @@ const SAMPLE_PROBLEM = {
1414
author: '',
1515
files: [],
1616
points: {
17-
min: 0,
18-
max: 0
17+
min: 100,
18+
max: 500
1919
}
2020
}
2121

2222
const Challenges = ({ classes }) => {
2323
const [problems, setProblems] = useState([])
24-
const [newId, setNewId] = useState(uuid())
2524

26-
useEffect(() => setNewId(uuid()), [problems])
25+
// newId is the id of the new problem. this allows us to reuse code for problem creation
26+
// eslint-disable-next-line react-hooks/exhaustive-deps
27+
const newId = useMemo(() => uuid(), [problems])
28+
29+
const completeProblems = problems.concat({
30+
...SAMPLE_PROBLEM,
31+
id: newId
32+
})
2733

2834
useEffect(() => {
2935
document.title = `Challenges${config.ctfTitle}`
@@ -36,16 +42,32 @@ const Challenges = ({ classes }) => {
3642
action()
3743
}, [])
3844

45+
const updateProblem = useCallback(({ problem }) => {
46+
let nextProblems = completeProblems
47+
48+
// If we aren't creating new problem, remove sample problem first
49+
if (problem.id !== newId) {
50+
nextProblems = nextProblems.filter(p => p.id !== newId)
51+
}
52+
setProblems(nextProblems.map(p => {
53+
// Perform partial update by merging properties
54+
if (p.id === problem.id) {
55+
return {
56+
...p,
57+
...problem
58+
}
59+
}
60+
return p
61+
}))
62+
}, [newId, completeProblems])
63+
3964
return (
4065
<div class={`row ${classes.row}`}>
4166
<div class='col-9'>
4267
{
43-
problems.concat({
44-
...SAMPLE_PROBLEM,
45-
id: newId
46-
}).map(problem => {
68+
completeProblems.map(problem => {
4769
return (
48-
<Problem key={problem.id} problem={problem} />
70+
<Problem update={updateProblem} key={problem.id} problem={problem} />
4971
)
5072
})
5173
}

client/src/util/index.js

+7
Original file line numberDiff line numberDiff line change
@@ -1 +1,8 @@
11
export * as strings from './strings'
2+
3+
export const encodeFile = file => new Promise((resolve, reject) => {
4+
const reader = new FileReader()
5+
reader.readAsDataURL(file)
6+
reader.addEventListener('load', () => resolve(reader.result))
7+
reader.addEventListener('error', error => reject(error))
8+
})

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"dependencies": {
2929
"@google-cloud/storage": "^5.0.1",
3030
"ajv": "^6.12.2",
31+
"data-uri-to-buffer": "^3.0.0",
3132
"dotenv": "^8.2.0",
3233
"email-validator": "^2.0.4",
3334
"express": "^4.17.1",

server/api/admin/challs/put.js

+18-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,24 @@ export default {
2121
type: 'object',
2222
properties: {
2323
data: {
24-
type: 'object'
24+
type: 'object',
25+
properties: {
26+
files: {
27+
type: 'array',
28+
items: {
29+
type: 'object',
30+
properties: {
31+
name: {
32+
type: 'string'
33+
},
34+
url: {
35+
type: 'string'
36+
}
37+
},
38+
required: ['name', 'url']
39+
}
40+
}
41+
}
2542
}
2643
}
2744
}

server/api/admin/index.js

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export default [
2+
...require('./challs').default,
3+
require('./upload').default
4+
]

server/api/admin/upload.js

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { responses } from '../../responses'
2+
import perms from '../../util/perms'
3+
import { get as getUploadProvider } from '../../uploads'
4+
import toBuffer from 'data-uri-to-buffer'
5+
6+
const itemSchema = {
7+
type: 'object',
8+
properties: {
9+
name: {
10+
type: 'string'
11+
},
12+
data: {
13+
type: 'string'
14+
}
15+
},
16+
required: ['name', 'data']
17+
}
18+
19+
export default {
20+
method: 'post',
21+
path: '/admin/upload',
22+
requireAuth: true,
23+
perms: perms.challsWrite,
24+
schema: {
25+
body: {
26+
type: 'object',
27+
properties: {
28+
files: {
29+
type: 'array',
30+
items: itemSchema
31+
}
32+
},
33+
required: ['files']
34+
}
35+
},
36+
handler: async ({ req }) => {
37+
const uploadProvider = getUploadProvider()
38+
39+
try {
40+
const files = await Promise.all(
41+
req.body.files.map(async ({ name, data }) => {
42+
const url = await uploadProvider.upload(toBuffer(data))
43+
44+
return {
45+
name,
46+
url
47+
}
48+
})
49+
)
50+
51+
return [responses.goodFilesUpload, files]
52+
} catch (e) {
53+
return responses.badFilesUpload
54+
}
55+
}
56+
}

server/api/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const routes = [
1515
require('./integrations-ctftime/callback').default,
1616
...require('./users').default,
1717
...require('./auth').default,
18-
...require('./admin/challs').default
18+
...require('./admin').default
1919
]
2020

2121
const validationParams = ['body', 'params', 'query']

server/providers/challenges/database/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ class DatabaseProvider extends EventEmitter implements Provider {
7676
chall = applyChallengeDefaults(chall)
7777
} else {
7878
chall = {
79-
...originalData,
79+
...originalData.data,
8080
...chall
8181
}
8282
}

server/responses/index.js

+8
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,14 @@ export const responseList = {
103103
status: 200,
104104
message: 'The authorization token is valid'
105105
},
106+
goodFilesUpload: {
107+
status: 200,
108+
message: 'The files were successfully uploaded'
109+
},
110+
badFilesUpload: {
111+
status: 500,
112+
message: 'The message upload failed'
113+
},
106114
badBody: {
107115
status: 400,
108116
message: 'The request body does not meet requirements.'

0 commit comments

Comments
 (0)