Skip to content

Commit

Permalink
feat: captcha (#1083)
Browse files Browse the repository at this point in the history
* feat: captcha wip

* feat: captcha

* feat: cleanup

* fix: remove console.logs

* feat: only pass accountId instead of full userInfo

* feat: add feedback to user

* chore: remove stray comment

* chore: remove stray comment

* chore: remove intl-tel

* feat: update alpha env with hcaptcha

* fix: console logs everywhere

* fix: better error handling

* fix: error when signing up for a second time without a refresh
  • Loading branch information
chrispanag authored Jul 20, 2022
1 parent 8170325 commit 8e6ec36
Show file tree
Hide file tree
Showing 10 changed files with 95 additions and 261 deletions.
1 change: 1 addition & 0 deletions .github/configs/alpha.env
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ TORUS_GOOGLE_CLIENTID="653379121360-j8t9ua763vfvd86d1qjguonhrgqvkigo.apps.google
TORUS_DISCORD_VERIFIER="capsule-social-test-v2-discord"
TORUS_DISCORD_CLIENTID="906210984396468275"
STRIPE_PUBLISHABLE_KEY=pk_live_51I81pBCPCJ3FaYLGK7rT7mqLsvvFCAOOxdN2RDLt868Oc2Tvrp6MYuyOoPyT6GkZNXvbGY02T5q6IsB8Z6HpE1PN00aopEjYBi
HCAPTCHA_SITE_KEY="4c2a59d5-cedc-4e81-a646-b769bf50e52c"
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@
"browser-image-compression": "^2.0.0",
"dompurify": "^2.3.9",
"highlight.js": "^11.6.0",
"intl-tel-input": "^17.0.18",
"ipfs-core": "0.14.3",
"libsodium-wrappers": "^0.7.10",
"lodash": "^4.17.21",
Expand All @@ -83,12 +82,12 @@
"@commitlint/cli": "^17.0.3",
"@commitlint/config-conventional": "^17.0.3",
"@nuxt/image": "^0.7.1",
"@hcaptcha/types": "^1.0.2",
"@nuxt/types": "^2.15.8",
"@nuxt/typescript-build": "^2.1.0",
"@nuxtjs/eslint-config-typescript": "^10.0.0",
"@nuxtjs/tailwindcss": "^4.2.1",
"@types/dompurify": "^2.3.3",
"@types/intl-tel-input": "^17.0.4",
"@types/libsodium-wrappers": "^0.7.9",
"@types/lodash": "^4.14.182",
"@types/marked": "^4.0.3",
Expand Down
12 changes: 2 additions & 10 deletions src/backend/funder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,6 @@ import axios from 'axios'
import { checkAccountStatus, getIsAccountIdOnboarded } from './near'
import { capsuleServer, sufficientFunds } from './utilities/config'

export async function requestOTP(phoneNumber: string) {
const response = await axios.post(`${capsuleServer}/sendOtp`, {
phoneNumber,
})
return response.data.data
}

export async function getFundTransferStatus(accountId: string): Promise<`PROCESSING` | `SENT` | `FAILED`> {
const response = await axios.get(`${capsuleServer}/onboard/sponsor/status?accountId=${accountId}`)
return response.data.data
Expand Down Expand Up @@ -55,10 +48,9 @@ export function waitForFunds(accountId: string) {
})
}

export async function requestOnboard(phoneNumber: string, code: string, accountId: string) {
export async function requestOnboard(captchaRes: string, accountId: string) {
const response = await axios.post(`${capsuleServer}/onboard`, {
phoneNumber,
code,
captchaRes,
accountId,
})
return response.data.data
Expand Down
4 changes: 4 additions & 0 deletions src/backend/utilities/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ const torusEnv = {
},
}

export const hcaptchaSiteKey = process.env.HCAPTCHA_SITE_KEY
? process.env.HCAPTCHA_SITE_KEY
: `10000000-ffff-ffff-ffff-000000000001`

export function getNearConfig(): INearConfig | ILocalNetNearConfig {
switch (nearNetwork) {
case `testnet`:
Expand Down
103 changes: 78 additions & 25 deletions src/components/register/SelectID.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,54 +11,83 @@
placeholder=""
class="focus:outline-none focus:border-primary text-primary dark:text-darkPrimaryText bg-gray2 dark:bg-gray7 mt-1 mb-5 w-full rounded-lg px-3 py-2 font-sans text-sm"
/>
<BrandedButton v-show="!isLoading" :text="`Sign Up`" :action="handleRegisterID" class="w-full" />
<h6 v-show="isLoading" class="text-primary text-center">Checking ID...</h6>
<div v-show="!hasEnoughFunds()">
<!-- This is basically a BrandedButton -->
<button
v-if="loadingState === null"
id="hcaptcha"
style="padding: 0.6rem 1.7rem"
class="w-full bg-primary text-lightButtonText focus:outline-none transform rounded-lg font-bold transition duration-500 ease-in-out hover:shadow-lg"
@click="handleRegisterID"
>
<span class="font-sans" style="font-size: 0.95rem"> Sign Up </span>
</button>
<h6 v-else-if="loadingState === 'checking_id'" class="text-primary text-center">Checking ID...</h6>
<h6 v-else-if="loadingState === 'hcaptcha_loading'" class="text-primary text-center">Verifying humanity...</h6>
<h6 v-else-if="loadingState === 'smart_contract'" class="text-primary text-center">Executing smart contract...</h6>
<h6 v-else-if="loadingState === 'transfer_funds'" class="text-primary text-center">Waiting for funds...</h6>
<!-- <div v-show="!hasEnoughFunds()">
<p class="justify-between p-5 font-sans text-sm text-gray7 dark:text-gray3">
Ensure that the NEAR account with ID: "{{ userInfo.accountId }}" has sufficient funds before signing up.
Ensure that the NEAR account with ID: "{{ accountId }}" has sufficient funds before signing up.
</p>
<p class="justify-between p-5 font-sans text-sm text-gray7 dark:text-gray3">Available funds: {{ funds }} yN</p>
<BrandedButton :text="`Re-check funds`" class="w-full" :action="() => $emit(`checkFunds`)" />
</div>
</div> -->
</article>
</template>

<script lang="ts">
import Vue, { PropType } from 'vue'
import Vue from 'vue'
import { mapMutations } from 'vuex'
import BrandedButton from '@/components/BrandedButton.vue'
import { MutationType, namespace as sessionStoreNamespace } from '~/store/session'
import { ValidationError } from '@/errors'
import { IWalletStatus } from '@/backend/auth'
import { hasSufficientFunds } from '@/backend/funder'
import { requestOnboard, waitForFunds } from '@/backend/funder'
import { validateUsernameNEAR } from '@/backend/near'
import { hcaptchaSiteKey } from '@/backend/utilities/config'
interface IData {
id: string
isLoading: boolean
siteKey: string
loadingState: `checking_id` | `hcaptcha_loading` | `smart_contract` | `transfer_funds` | null
captchaID: string | null
}
export default Vue.extend({
components: {
BrandedButton,
},
props: {
funds: {
accountId: {
type: String,
required: true,
},
userInfo: {
type: Object as PropType<IWalletStatus>,
required: true,
},
},
data(): IData {
return {
id: ``,
isLoading: false,
siteKey: hcaptchaSiteKey,
loadingState: null,
captchaID: null,
}
},
head() {
return {
script: [{ src: `https://js.hcaptcha.com/1/api.js?explicit=true`, defer: true, async: true }],
}
},
async mounted() {
const doc = document.getElementById(`hcaptcha`)
if (!doc) {
throw new Error(`Impossible!`)
}
while (true) {
await Promise.resolve()
if (hcaptcha !== undefined && hcaptcha) {
break
}
}
this.captchaID = hcaptcha.render(doc, {
size: `invisible`,
sitekey: hcaptchaSiteKey,
})
},
methods: {
...mapMutations(sessionStoreNamespace, {
changeCID: MutationType.CHANGE_CID,
Expand All @@ -71,23 +100,47 @@ export default Vue.extend({
}),
async handleRegisterID() {
try {
this.isLoading = true
if (!this.captchaID) {
return
}
this.loadingState = `checking_id`
this.id = this.id.toLowerCase()
const idValidity = await validateUsernameNEAR(this.id)
if (idValidity.error) {
this.isLoading = false
this.loadingState = null
throw new ValidationError(idValidity.error)
}
this.loadingState = `hcaptcha_loading`
const res = await hcaptcha.execute(this.captchaID, { async: true })
// eslint-disable-next-line no-console
console.log(res)
if (!res) {
// eslint-disable-next-line no-console
console.log(`captchares`, res)
this.loadingState = null
throw new Error(`Issue on captcha`)
}
this.loadingState = `smart_contract`
await requestOnboard(res.response, this.accountId)
this.loadingState = `transfer_funds`
await waitForFunds(this.accountId)
this.loadingState = null
this.$emit(`verify`, this.id)
} catch (error) {
if (typeof error === `string`) {
if (error === `challenge-closed`) {
return
}
this.$handleError({ message: `Captcha error: ${error}` })
return
}
// eslint-disable-next-line no-console
console.log(`error`, error)
this.$handleError(error)
} finally {
this.isLoading = false
this.loadingState = null
}
},
hasEnoughFunds(): boolean {
return hasSufficientFunds(this.funds)
},
},
})
</script>
53 changes: 3 additions & 50 deletions src/components/register/SignUp.vue
Original file line number Diff line number Diff line change
@@ -1,22 +1,8 @@
<template>
<div class="flex flex-row w-full items-center justify-center">
<article v-if="!downloadKey" class="flex flex-row w-full items-center justify-center">
<VerifyPhone
v-if="!hasEnoughFunds() || !onboarded"
:accountId="userInfo.accountId"
class="w-full h-full xl:w-1/2"
@updateFunds="updateFunds"
@setIsOnboarded="setIsOnboarded"
/>
<!-- Step 3: Choose ID -->
<SelectID
v-else
:funds="funds"
:userInfo="userInfo"
class="w-full h-full xl:w-1/2"
@checkFunds="checkFunds"
@verify="verify"
/>
<!-- Step 2: Choose ID -->
<SelectID :funds="funds" :accountId="userInfo.accountId" class="w-full h-full xl:w-1/2" @verify="verify" />
</article>
<!-- Step 4: Download key -->
<DownloadKey
Expand All @@ -33,34 +19,24 @@ import Vue from 'vue'
import type { PropType } from 'vue'
import { mapMutations } from 'vuex'
import VerifyPhone from './VerifyPhone.vue'
import SelectID from './SelectID.vue'
import DownloadKey from './DownloadKey.vue'
import { hasSufficientFunds } from '@/backend/funder'
import {
checkAccountStatus,
getIsAccountIdOnboarded,
getUsernameNEAR,
removeNearPrivateKey,
walletLogout,
} from '@/backend/near'
import { getUsernameNEAR, removeNearPrivateKey, walletLogout } from '@/backend/near'
import { MutationType, createSessionFromProfile, namespace as sessionStoreNamespace } from '~/store/session'
import { setNearUserFromPrivateKey, login, register, IAuthResult, IWalletStatus } from '@/backend/auth'
import { ValidationError } from '@/errors'
interface IData {
funds: string
onboarded: boolean
username: null | string
isLoading: boolean
downloadKey: boolean
}
export default Vue.extend({
components: {
VerifyPhone,
DownloadKey,
SelectID,
},
Expand All @@ -76,19 +52,13 @@ export default Vue.extend({
username: null,
isLoading: true,
downloadKey: false,
onboarded: false,
}
},
async created() {
this.$emit(`setIsLoading`, true)
try {
const username = await getUsernameNEAR(this.userInfo.accountId)
if (!username) {
const [, onboarded] = await Promise.all([
this.checkFunds(),
await getIsAccountIdOnboarded(this.userInfo.accountId),
])
this.onboarded = onboarded
this.$emit(`setIsLoading`, false)
return
}
Expand Down Expand Up @@ -128,23 +98,6 @@ export default Vue.extend({
changeBio: MutationType.CHANGE_BIO,
changeLocation: MutationType.CHANGE_LOCATION,
}),
hasEnoughFunds(): boolean {
return hasSufficientFunds(this.funds)
},
async checkFunds() {
const accountId = this.userInfo.accountId
if (!accountId) {
return
}
const status = await checkAccountStatus(accountId)
this.funds = status.balance
},
updateFunds(balance: string) {
this.funds = balance
},
setIsOnboarded(onboarded: boolean) {
this.onboarded = onboarded
},
async verify(id: string) {
if (!this.userInfo) {
throw new Error(`Unexpected condition!`)
Expand Down
Loading

0 comments on commit 8e6ec36

Please sign in to comment.