authorize button feature

This commit is contained in:
Alexey 2020-02-08 21:47:21 +03:00
parent 7fb6395dd7
commit 431b06ca52
7 changed files with 155 additions and 29 deletions

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="ceremony"> <div class="ceremony">
<h1 class="title is-size-1 is-size-2-mobile is-spaced"> <h1 class="title is-size-1 is-size-2-mobile is-spaced">
Hello, <span>@{{ userHandle }}</span> Hello, <span>@{{ handle }}</span>
</h1> </h1>
<h2 class="subtitle"> <h2 class="subtitle">
Do you want to authorize your contribution #{{ contributionIndex }}? Please sign in. Do you want to authorize your contribution #{{ contributionIndex }}? Please sign in.
@ -10,15 +10,34 @@
<Form /> <Form />
</fieldset> </fieldset>
<div class="buttons is-centered"> <div class="buttons is-centered">
<b-button v-if="isLoggedIn" :disabled="hasErrorName.invalid" type="is-primary" outlined> <b-button
v-if="isLoggedIn && !hideSaveBtn"
@click="authorize"
:disabled="hasErrorName.invalid"
type="is-primary"
outlined
>
Save information Save information
</b-button> </b-button>
<b-button
v-if="status.type === 'is-success'"
@click="makeTweet"
type="is-primary"
tag="a"
target="_blank"
outlined
>
Tweet about your contribution
</b-button>
</div>
<div v-show="status.type === 'is-danger' || status.type === 'is-success'" class="status">
<div :class="status.type" class="status-message">{{ status.msg }}</div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { mapGetters, mapActions } from 'vuex' import { mapGetters, mapActions, mapState } from 'vuex'
import Form from '@/components/Form' import Form from '@/components/Form'
export default { export default {
@ -27,24 +46,60 @@ export default {
}, },
data() { data() {
return { return {
contributionIndex: 1 contributionIndex: 1,
token: null,
status: {
type: '',
msg: ''
},
hideSaveBtn: false
} }
}, },
computed: { computed: {
...mapGetters('user', ['isLoggedIn', 'hasErrorName']), ...mapState('user', ['name', 'handle', 'company']),
userHandle: { ...mapGetters('user', ['isLoggedIn', 'hasErrorName'])
get() {
return this.$store.state.user.handle
}
}
}, },
async mounted() { async mounted() {
await this.getUserData() await this.getUserData()
// TODO. parse href to take token (it's supposed to be after #) this.token = this.$route.query.token
// then you need to store it in localstorage OR pass to server (to `/connect`) so after the authorization redirect server can put it in url if (!this.token) {
window.location.replace(window.location.origin)
} else {
// TODO try to load contribution data. May be it's already authorized
// also set `contributionIndex`
}
}, },
methods: { methods: {
...mapActions('user', ['getUserData']) ...mapActions('user', ['getUserData', 'makeTweet']),
async authorize() {
const body = {
token: this.token,
name: this.name,
company: this.company
}
try {
const response = await fetch('/api/authorize_contribution', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
})
if (response.ok) {
this.status.msg = `Your contribution is verified and authorized. Thank you.`
this.status.type = 'is-success'
this.hideSaveBtn = true
} else {
const error = await response.text()
this.status.msg = error
this.status.type = 'is-danger'
}
} catch (e) {
this.status.msg = 'Something went wrong. Please contact support'
this.status.type = 'is-danger'
}
}
} }
} }
</script> </script>

View File

@ -174,6 +174,9 @@ export default {
this.status.type = 'is-success' this.status.type = 'is-success'
const responseData = await resp.json() const responseData = await resp.json()
this.$store.commit('user/SET_CONTRIBUTION_INDEX', responseData.contributionIndex) this.$store.commit('user/SET_CONTRIBUTION_INDEX', responseData.contributionIndex)
console.log(
`${window.location.origin}/authorize-contribution?token=${responseData.token}`
)
} else if (resp.status === 422) { } else if (resp.status === 422) {
if (retry < 3) { if (retry < 3) {
console.log(`Looks like someone else uploaded contribution ahead of us, retrying`) console.log(`Looks like someone else uploaded contribution ahead of us, retrying`)

View File

@ -13,6 +13,7 @@ const {
GITHUB_CALLBACK_URL GITHUB_CALLBACK_URL
} = process.env } = process.env
const providers = ['github', 'twitter'] const providers = ['github', 'twitter']
const signInPages = ['/make-contribution', '/authorize-contribution']
// twitter uses OAuth1 // twitter uses OAuth1
const twitter = new oauth.OAuth( const twitter = new oauth.OAuth(
@ -43,8 +44,31 @@ function validateProvider(req, res, next) {
} }
} }
router.get('/connect/:provider', validateProvider, (req, res) => { function validateRefferer(req, res, next) {
let referrer
try {
referrer = new URL(req.get('Referrer'))
} catch (e) {
res.status(403).send('Access forbidden')
return
}
if (!signInPages.includes(referrer.pathname)) {
res.status(403).send('Access forbidden')
return
}
next()
}
router.get('/connect/:provider', validateProvider, validateRefferer, (req, res) => {
const { provider } = req.params const { provider } = req.params
const referrer = new URL(req.get('Referrer'))
req.session.pageToReturn = referrer.pathname // the page a user will be redirected after signIn
if (referrer.pathname === '/authorize-contribution') {
req.session.pageToReturn += referrer.search // to add `token` parameter for authorize-contribution page
}
if (provider === 'github') { if (provider === 'github') {
const CSRFToken = crypto.randomBytes(32).toString('hex') const CSRFToken = crypto.randomBytes(32).toString('hex')
@ -86,7 +110,7 @@ router.get('/oauth_callback/:provider', validateProvider, (req, res) => {
} else { } else {
req.session.refreshToken = refreshToken req.session.refreshToken = refreshToken
req.session.accessToken = accessToken req.session.accessToken = accessToken
res.redirect('/make-contribution') res.redirect(req.session.pageToReturn)
} }
}) })
} else if (provider === 'twitter') { } else if (provider === 'twitter') {
@ -101,7 +125,7 @@ router.get('/oauth_callback/:provider', validateProvider, (req, res) => {
} else { } else {
req.session.oauthAccessToken = oauthAccessToken req.session.oauthAccessToken = oauthAccessToken
req.session.oauthAccessTokenSecret = oauthAccessTokenSecret req.session.oauthAccessTokenSecret = oauthAccessTokenSecret
res.redirect('/make-contribution') res.redirect(req.session.pageToReturn)
} }
} }
) )

View File

@ -1,3 +1,4 @@
/* eslint-disable no-console */
const fs = require('fs').promises const fs = require('fs').promises
const path = require('path') const path = require('path')
const util = require('util') const util = require('util')
@ -97,7 +98,7 @@ router.post('/response', upload.single('response'), async (req, res) => {
) )
console.log('Finished') console.log('Finished')
res.json({ contributionIndex }) res.json({ contributionIndex, token })
} catch (e) { } catch (e) {
console.error('Got error during save', e) console.error('Got error during save', e)
await fs.unlink(`/tmp/tornado/${req.file.filename}`) await fs.unlink(`/tmp/tornado/${req.file.filename}`)
@ -106,4 +107,43 @@ router.post('/response', upload.single('response'), async (req, res) => {
}) })
}) })
router.post('/authorize_contribution', async (req, res) => {
if (!req.body || !req.body.name || !req.body.token) {
res.status(404).send('Wrong request params')
}
const contribution = await Contribution.findOne({ where: { token: req.body.token } })
if (!contribution) {
res.status(404).send('There is no such contribution')
return
}
if (contribution.dataValues.socialType !== 'anonymous') {
res.status(404).send('The contribution is already authorized')
return
}
if (!req.session.socialType || req.session.socialType === 'anonymous') {
res.status(403).send('Access forbidden')
return
}
try {
await Contribution.update(
{
name: req.body.name,
company: req.body.company,
handle: req.session.handle,
socialType: req.session.socialType
},
{ where: { token: req.body.token }, returning: true }
)
} catch (e) {
console.error('updateError', e)
res.status(404).send('Update error')
}
res.send('OK')
})
module.exports = router module.exports = router

View File

@ -38,12 +38,12 @@ async function start() {
next() next()
}) })
app.use('/api', sessionsController)
app.use('/api', contributionController)
app.use(bodyParser.urlencoded({ extended: true })) app.use(bodyParser.urlencoded({ extended: true }))
app.use(bodyParser.json()) app.use(bodyParser.json())
app.use('/api', sessionsController)
app.use('/api', contributionController)
// Give nuxt middleware to express // Give nuxt middleware to express
app.use(nuxt.render) app.use(nuxt.render)

View File

@ -1,4 +1,15 @@
'use strict' 'use strict'
const validate = (contribution, options) => {
const { name, company, socialType } = contribution.dataValues
if (socialType !== 'anonymous' && (name.length < 4 || name.length > 35)) {
throw new Error('Wrong name')
}
if (company && company.length > 35) {
throw new Error('Wrong company')
}
}
module.exports = (sequelize, DataTypes) => { module.exports = (sequelize, DataTypes) => {
const Contribution = sequelize.define( const Contribution = sequelize.define(
'Contribution', 'Contribution',
@ -12,15 +23,8 @@ module.exports = (sequelize, DataTypes) => {
}, },
{ {
hooks: { hooks: {
beforeCreate: (contribution, options) => { beforeCreate: validate,
const { name, company, socialType } = contribution.dataValues beforeUpdate: validate
if (socialType !== 'anonymous' && (name.length < 4 || name.length > 35)) {
throw new Error('Wrong name')
}
if (company && company.length > 35) {
throw new Error('Wrong company')
}
}
} }
} }
) )

Binary file not shown.