diff --git a/client/src/api/auth.js b/client/src/api/auth.js index 661fa6f58..f206a4522 100644 --- a/client/src/api/auth.js +++ b/client/src/api/auth.js @@ -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' @@ -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) { @@ -80,3 +87,9 @@ export const register = ({ email, name, division }) => { } }) } + +export const ctftimeCallback = ({ ctftimeCode }) => { + return request('POST', '/integrations/ctftime/callback', { + ctftimeCode + }) +} diff --git a/client/src/components/ctftime-additional.js b/client/src/components/ctftime-additional.js new file mode 100644 index 000000000..b1e51dd76 --- /dev/null +++ b/client/src/components/ctftime-additional.js @@ -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 ( +
+

Finish registration

+
+ + {showName && ( + } name='name' placeholder='Team Name' type='text' value={name} onChange={this.linkState('name')} /> + )} +
+
+ ) + } + + 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 + }) + }) + } +}) diff --git a/client/src/components/ctftime-button.js b/client/src/components/ctftime-button.js new file mode 100644 index 000000000..b3306ff44 --- /dev/null +++ b/client/src/components/ctftime-button.js @@ -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 ( +
+
+
or
+
+ +
+ ) + } +})) diff --git a/client/src/components/form.js b/client/src/components/form.js index 7878bb7b8..5c322564d 100644 --- a/client/src/components/form.js +++ b/client/src/components/form.js @@ -21,6 +21,9 @@ export default withStyles({
{ [].concat(children).map(input => { + if (input.props === undefined) { + return + } let { icon, error, name } = input.props if (errors !== undefined && name !== undefined) error = error || errors[name] diff --git a/client/src/icons/ctftime.svg b/client/src/icons/ctftime.svg new file mode 100644 index 000000000..0aec8653b --- /dev/null +++ b/client/src/icons/ctftime.svg @@ -0,0 +1 @@ + diff --git a/client/src/index.js b/client/src/index.js index 2c325a6f2..fe80fbd08 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -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' @@ -48,6 +49,7 @@ function App () { let allPaths = [ , , + , ] allPaths = allPaths.concat(loggedInPaths).concat(loggedOutPaths) diff --git a/client/src/routes/ctftime-callback.js b/client/src/routes/ctftime-callback.js new file mode 100644 index 000000000..534b3c7e4 --- /dev/null +++ b/client/src/routes/ctftime-callback.js @@ -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 + } +} diff --git a/client/src/routes/login.js b/client/src/routes/login.js index c2ceb8cd4..d3317f677 100644 --- a/client/src/routes/login.js +++ b/client/src/routes/login.js @@ -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: { @@ -20,7 +22,8 @@ export default withStyles({ state = { teamToken: '', errors: {}, - disabledButton: false + disabledButton: false, + ctftimeToken: undefined } componentDidMount () { @@ -42,17 +45,34 @@ export default withStyles({ } } - render (props, { teamToken, errors, disabledButton }) { - const { classes } = props + render ({ classes }, { teamToken, errors, disabledButton, ctftimeToken }) { + if (ctftimeToken) { + return + } return (
} placeholder='Team Token' type='text' value={teamToken} onChange={this.linkState('teamToken')} /> +
) } + handleCtftimeDone = async (ctftimeToken) => { + this.setState({ + disabledButton: true + }) + const loginRes = await login({ + ctftimeToken + }) + if (loginRes && loginRes.badUnknownUser) { + this.setState({ + ctftimeToken + }) + } + } + handleSubmit = e => { e.preventDefault() diff --git a/client/src/routes/registration.js b/client/src/routes/registration.js index 812a7c18c..cb84de829 100644 --- a/client/src/routes/registration.js +++ b/client/src/routes/registration.js @@ -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: { @@ -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: {} } @@ -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 + } return (
@@ -44,10 +53,25 @@ export default withStyles({ }
+
) } + handleCtftimeDone = async (ctftimeToken) => { + this.setState({ + disabledButton: true + }) + const loginRes = await login({ + ctftimeToken + }) + if (loginRes && loginRes.badUnknownUser) { + this.setState({ + ctftimeToken + }) + } + } + handleSubmit = e => { e.preventDefault() @@ -57,6 +81,10 @@ export default withStyles({ register(this.state) .then(errors => { + if (!errors) { + return + } + this.setState({ errors, disabledButton: false diff --git a/client/src/util/ctftime.js b/client/src/util/ctftime.js new file mode 100644 index 000000000..7425b59c2 --- /dev/null +++ b/client/src/util/ctftime.js @@ -0,0 +1,34 @@ +import config from '../../../config/client' + +const openPopup = ({ url, title, w, h }) => { + const systemZoom = window.innerWidth / window.screen.availWidth + const left = (window.innerWidth - w) / 2 / systemZoom + window.screenLeft + const top = (window.innerHeight - h) / 2 / systemZoom + window.screenTop + const popup = window.open(url, title, [ + 'scrollbars', + 'resizable', + `width=${w / systemZoom}`, + `height=${h / systemZoom}`, + `top=${top}`, + `left=${left}` + ].join(',')) + popup.focus() +} + +const getState = () => Array.from(crypto.getRandomValues(new Uint8Array(16))) + .map(v => v.toString(16).padStart(2, '0')).join('') + +export default () => { + const state = getState() + openPopup({ + url: 'https://oauth.ctftime.org/authorize' + + `?scope=${encodeURIComponent('team:read')}` + + `&client_id=${encodeURIComponent(config.ctftimeClientId)}` + + `&redirect_uri=${encodeURIComponent(`${location.origin}/integrations/ctftime/callback`)}` + + `&state=${encodeURIComponent(state)}`, + title: 'CTFtime', + w: 500, + h: 500 + }) + return state +} diff --git a/server/api/auth/register.js b/server/api/auth/register.js index d9d48f2c7..73305b045 100644 --- a/server/api/auth/register.js +++ b/server/api/auth/register.js @@ -67,11 +67,6 @@ module.exports = { }) } - const conflictError = await util.auth.getRegisterConflict({ name, email, ctftimeId }) - if (conflictError !== undefined) { - return conflictError - } - if (req.body.ctftimeToken !== undefined) { return auth.register.register({ division: req.body.division, diff --git a/server/util/auth.js b/server/util/auth.js deleted file mode 100644 index e86e831e3..000000000 --- a/server/util/auth.js +++ /dev/null @@ -1,23 +0,0 @@ -const { responses } = require('../responses') -const database = require('../database') - -module.exports = { - getRegisterConflict: async ({ name, email, ctftimeId }) => { - let dbResult - if (ctftimeId === undefined) { - dbResult = await database.auth.getUserByNameOrEmail({ name, email }) - } else { - dbResult = await database.auth.getUserByNameOrCtftimeId({ name, ctftimeId }) - } - if (dbResult === undefined) { - return - } - if (ctftimeId !== undefined && dbResult.ctftime_id === ctftimeId) { - return responses.badKnownCtftimeId - } - if (ctftimeId !== undefined && dbResult.email === email) { - return responses.badKnownEmail - } - return responses.badKnownName - } -} diff --git a/server/util/index.js b/server/util/index.js index 49cb03080..27cab0c3b 100644 --- a/server/util/index.js +++ b/server/util/index.js @@ -8,7 +8,6 @@ const normalize = require('./normalize') module.exports = { scores: require('./scores'), email: require('./email'), - auth: require('./auth'), normalize, notStarted: () => { return [