Skip to content

Commit

Permalink
Merge pull request redpwn#200 from redpwn/feature/client-ctftime
Browse files Browse the repository at this point in the history
Add ctftime login and registration frontend
  • Loading branch information
chen-robert authored Apr 15, 2020
2 parents 9521693 + 13976fb commit d709638
Show file tree
Hide file tree
Showing 13 changed files with 276 additions and 39 deletions.
23 changes: 18 additions & 5 deletions client/src/api/auth.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import { request } from './util'
import { route } from 'preact-router'

export const login = ({ teamToken }) => {
export const login = ({ teamToken, ctftimeToken }) => {
return request('POST', '/auth/login', {
teamToken
teamToken,
ctftimeToken
})
.then(resp => {
switch (resp.kind) {
case 'goodLogin':
localStorage.setItem('token', resp.data.authToken)
return route('/challs')
route('/challs')
return
case 'badTokenVerification':
return {
teamToken: resp.message
}
case 'badUnknownUser':
return {
badUnknownUser: true
}
default:
return {
teamToken: 'Unknown response from server, please contact ctf administrator'
Expand Down Expand Up @@ -53,11 +59,12 @@ export const verify = ({ verifyToken }) => {
})
}

export const register = ({ email, name, division }) => {
export const register = ({ email, name, division, ctftimeToken }) => {
return request('POST', '/auth/register', {
email,
name,
division: Number.parseInt(division)
division: Number.parseInt(division),
ctftimeToken
})
.then(resp => {
switch (resp.kind) {
Expand All @@ -80,3 +87,9 @@ export const register = ({ email, name, division }) => {
}
})
}

export const ctftimeCallback = ({ ctftimeCode }) => {
return request('POST', '/integrations/ctftime/callback', {
ctftimeCode
})
}
77 changes: 77 additions & 0 deletions client/src/components/ctftime-additional.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Component } from 'preact'
import Form from '../components/form'
import config from '../../../config/client'
import 'linkstate/polyfill'
import withStyles from '../components/jss'
import { register } from '../api/auth'
import UserCircle from '../icons/user-circle.svg'

export default withStyles({
root: {
padding: '1.5em'
},
submit: {
marginTop: '1.5em'
},
title: {
textAlign: 'center'
}
}, class CtftimeAdditional extends Component {
state = {
showName: false,
disabledButton: false,
division: '',
name: '',
errors: {}
}

render ({ classes }, { showName, disabledButton, division, name, errors }) {
return (
<div class='row u-center'>
<h4 class={`col-12 ${classes.title}`}>Finish registration</h4>
<Form class={`${classes.root} col-6`} onSubmit={this.handleSubmit} disabled={disabledButton} errors={errors} buttonText='Register'>
<select required class='select' name='division' value={division} onChange={this.linkState('division')}>
<option value='' disabled selected>Division</option>
{
Object.entries(config.divisions).map(([name, code]) => {
return <option key={code} value={code}>{name}</option>
})
}
</select>
{showName && (
<input autofocus required icon={<UserCircle />} name='name' placeholder='Team Name' type='text' value={name} onChange={this.linkState('name')} />
)}
</Form>
</div>
)
}

handleSubmit = (e) => {
e.preventDefault()

this.setState({
disabledButton: true
})

register({
ctftimeToken: this.props.ctftimeToken,
division: this.state.division,
name: this.state.name || undefined
})
.then(errors => {
if (!errors) {
return
}
if (errors.name) {
this.setState({
showName: true
})
}

this.setState({
errors,
disabledButton: false
})
})
}
})
72 changes: 72 additions & 0 deletions client/src/components/ctftime-button.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Component } from 'preact'
import Ctftime from '../icons/ctftime.svg'
import openPopup from '../util/ctftime'
import withStyles from '../components/jss'
import { ctftimeCallback } from '../api/auth'
import { withToast } from '../components/toast'

export default withStyles({
ctftimeButton: {
margin: 'auto',
lineHeight: '0',
padding: '10px',
'& svg': {
width: '150px'
}
},
or: {
textAlign: 'center',
display: 'block',
margin: '15px auto'
}
}, withToast(class CtftimeButton extends Component {
componentDidMount () {
window.addEventListener('message', this.handlePostMessage)
}

componentWillUnmount () {
window.removeEventListener('message', this.handlePostMessage)
}

oauthState = null

handlePostMessage = async (evt) => {
if (evt.origin !== location.origin) {
return
}
if (evt.data.kind !== 'ctftimeCallback') {
return
}
if (this.oauthState === null || evt.data.state !== this.oauthState) {
return
}
const { kind, message, data } = await ctftimeCallback({
ctftimeCode: evt.data.ctftimeCode
})
if (kind !== 'goodCtftimeToken') {
this.props.toast({
body: message,
type: 'error'
})
return
}
this.props.onCtftimeDone(data.ctftimeToken)
}

handleClick = () => {
this.oauthState = openPopup()
}

render ({ classes, ...props }) {
return (
<div {...props} >
<div class={classes.or}>
<h5>or</h5>
</div>
<button class={classes.ctftimeButton} onClick={this.handleClick}>
<Ctftime />
</button>
</div>
)
}
}))
3 changes: 3 additions & 0 deletions client/src/components/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export default withStyles({
<form onSubmit={onSubmit} class={props.class}>
{
[].concat(children).map(input => {
if (input.props === undefined) {
return
}
let { icon, error, name } = input.props

if (errors !== undefined && name !== undefined) error = error || errors[name]
Expand Down
1 change: 1 addition & 0 deletions client/src/icons/ctftime.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions client/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import Scoreboard from './routes/scoreboard'
import Error from './routes/error'
import Sponsors from './routes/sponsors'
import Verify from './routes/verify'
import CtftimeCallback from './routes/ctftime-callback'

import { ToastProvider } from './components/toast'

Expand Down Expand Up @@ -48,6 +49,7 @@ function App () {
let allPaths = [
<Profile key='multiProfile' path='/profile/:uuid' />,
<Verify key='verify' path='/verify' />,
<CtftimeCallback key='ctftimeCallback' path='/integrations/ctftime/callback' />,
<Error key='error' error='404' default />
]
allPaths = allPaths.concat(loggedInPaths).concat(loggedOutPaths)
Expand Down
16 changes: 16 additions & 0 deletions client/src/routes/ctftime-callback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Component } from 'preact'

export default class CtftimeCallback extends Component {
componentDidMount () {
window.opener.postMessage({
kind: 'ctftimeCallback',
state: this.props.state,
ctftimeCode: this.props.code
})
window.close()
}

render () {
return null
}
}
26 changes: 23 additions & 3 deletions client/src/routes/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import withStyles from '../components/jss'
import { login } from '../api/auth'
import IdCard from '../icons/id-card.svg'
import { route } from 'preact-router'
import CtftimeButton from '../components/ctftime-button'
import CtftimeAdditional from '../components/ctftime-additional'

export default withStyles({
root: {
Expand All @@ -20,7 +22,8 @@ export default withStyles({
state = {
teamToken: '',
errors: {},
disabledButton: false
disabledButton: false,
ctftimeToken: undefined
}

componentDidMount () {
Expand All @@ -42,17 +45,34 @@ export default withStyles({
}
}

render (props, { teamToken, errors, disabledButton }) {
const { classes } = props
render ({ classes }, { teamToken, errors, disabledButton, ctftimeToken }) {
if (ctftimeToken) {
return <CtftimeAdditional ctftimeToken={ctftimeToken} />
}
return (
<div class='row u-center'>
<Form class={`${classes.root} col-6`} onSubmit={this.handleSubmit} disabled={disabledButton} buttonText='Login' errors={errors}>
<input autofocus name='teamToken' icon={<IdCard />} placeholder='Team Token' type='text' value={teamToken} onChange={this.linkState('teamToken')} />
</Form>
<CtftimeButton class='col-12' onCtftimeDone={this.handleCtftimeDone} />
</div>
)
}

handleCtftimeDone = async (ctftimeToken) => {
this.setState({
disabledButton: true
})
const loginRes = await login({
ctftimeToken
})
if (loginRes && loginRes.badUnknownUser) {
this.setState({
ctftimeToken
})
}
}

handleSubmit = e => {
e.preventDefault()

Expand Down
32 changes: 30 additions & 2 deletions client/src/routes/registration.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import config from '../../../config/client'
import 'linkstate/polyfill'
import withStyles from '../components/jss'

import { register } from '../api/auth'
import { register, login } from '../api/auth'
import UserCircle from '../icons/user-circle.svg'
import EnvelopeOpen from '../icons/envelope-open.svg'
import CtftimeButton from '../components/ctftime-button'
import CtftimeAdditional from '../components/ctftime-additional'

export default withStyles({
root: {
Expand All @@ -15,12 +17,16 @@ export default withStyles({
},
submit: {
marginTop: '1.5em'
},
or: {
textAlign: 'center'
}
}, class Register extends Component {
state = {
name: '',
email: '',
division: '',
ctftimeToken: undefined,
disabledButton: false,
errors: {}
}
Expand All @@ -29,7 +35,10 @@ export default withStyles({
document.title = `Registration${config.ctfTitle}`
}

render ({ classes }, { name, email, division, disabledButton, errors }) {
render ({ classes }, { name, email, division, disabledButton, errors, ctftimeToken }) {
if (ctftimeToken) {
return <CtftimeAdditional ctftimeToken={ctftimeToken} />
}
return (
<div class='row u-center'>
<Form class={`${classes.root} col-6`} onSubmit={this.handleSubmit} disabled={disabledButton} errors={errors} buttonText='Register'>
Expand All @@ -44,10 +53,25 @@ export default withStyles({
}
</select>
</Form>
<CtftimeButton class='col-12' onCtftimeDone={this.handleCtftimeDone} />
</div>
)
}

handleCtftimeDone = async (ctftimeToken) => {
this.setState({
disabledButton: true
})
const loginRes = await login({
ctftimeToken
})
if (loginRes && loginRes.badUnknownUser) {
this.setState({
ctftimeToken
})
}
}

handleSubmit = e => {
e.preventDefault()

Expand All @@ -57,6 +81,10 @@ export default withStyles({

register(this.state)
.then(errors => {
if (!errors) {
return
}

this.setState({
errors,
disabledButton: false
Expand Down
Loading

0 comments on commit d709638

Please sign in to comment.