Skip to content

Commit

Permalink
feat(webapp): create generic captcha field
Browse files Browse the repository at this point in the history
  • Loading branch information
Rotzbua committed Dec 26, 2023
1 parent 896f495 commit 56aba5d
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 233 deletions.
109 changes: 23 additions & 86 deletions www/webapp/src/components/ActivateAccountActionHandler.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,60 +2,22 @@
<div>
<div class="text-center" v-if="captcha_required && !success">
<v-container class="pa-0">
<v-row dense align="center" class="text-center">
<v-col cols="12" sm="">
<v-text-field
v-model="payload.captcha.solution"
label="Type CAPTCHA text here"
prepend-icon="mdi-account-check"
outlined
required
:disabled="working"
:rules="captcha_rules"
:error-messages="captcha_errors"
@change="captcha_errors=[]"
@keypress="captcha_errors=[]"
class="uppercase"
ref="captchaField"
tabindex="3"
:hint="captcha_kind === 'image' ? 'Can\'t see? Hear an audio CAPTCHA instead.' : 'Trouble hearing? Switch to an image CAPTCHA.'"
/>
</v-col>
<v-col cols="12" sm="auto">
<v-progress-circular
indeterminate
v-if="captchaWorking"
></v-progress-circular>
<img
v-if="captcha && !captchaWorking && captcha_kind === 'image'"
:src="'data:image/png;base64,'+captcha.challenge"
alt="Passwords can also be reset by sending an email to our support."
/>
<audio controls
v-if="captcha && !captchaWorking && captcha_kind === 'audio'"
>
<source :src="'data:audio/wav;base64,'+captcha.challenge" type="audio/wav"/>
</audio>
<br/>
<v-btn-toggle>
<v-btn text outlined @click="getCaptcha(true)" :disabled="captchaWorking"><v-icon>mdi-refresh</v-icon></v-btn>
</v-btn-toggle>
&nbsp;
<v-btn-toggle v-model="captcha_kind">
<v-btn text outlined value="image" aria-label="Switch to Image CAPTCHA" :disabled="captchaWorking"><v-icon>mdi-eye</v-icon></v-btn>
<v-btn text outlined value="audio" aria-label="Switch to Audio CAPTCHA" :disabled="captchaWorking"><v-icon>mdi-ear-hearing</v-icon></v-btn>
</v-btn-toggle>
</v-col>
</v-row>
<generic-captcha
:id="captchaID"
:solution="captchaSolution"
tabindex="3"
ref="captchaField"
/>
</v-container>
<v-btn
depressed
color="primary"
type="submit"
:disabled="working || !valid"
:loading="working"
tabindex="2"
>Submit</v-btn>
<v-btn
depressed
color="primary"
type="submit"
:disabled="working || !valid"
:loading="working"
tabindex="2"
>Submit
</v-btn>
</div>
<v-alert type="success" v-if="success">
<p>{{ this.response.data.detail }}</p>
Expand All @@ -64,60 +26,35 @@
</template>

<script>
import axios from 'axios';
import GenericActionHandler from "./GenericActionHandler.vue"
const HTTP = axios.create({
baseURL: '/api/v1/',
headers: {},
});
import GenericCaptcha from "@/components/Field/GenericCaptcha.vue";
export default {
name: 'ActivateAccountActionHandler',
components: {GenericCaptcha},
extends: GenericActionHandler,
data: () => ({
auto_submit: true,
captchaWorking: false,
LOCAL_PUBLIC_SUFFIXES: import.meta.env.VITE_APP_LOCAL_PUBLIC_SUFFIXES.split(' '),
captcha: null,
captcha_required: false,
/* captcha field */
captcha_required: false,
captchaID: null,
captchaSolution: '',
captcha_rules: [v => !!v || 'Please enter the text displayed in the picture so we are (somewhat) convinced you are human'],
captcha_errors: [],
captcha_kind: 'image',
}),
computed: {
captcha_error: function () {
captcha_error: function() {
return this.error && this.response.data.captcha !== undefined
}
},
methods: {
async getCaptcha() {
this.captchaWorking = true;
this.captchaSolution = "";
try {
this.captcha = (await HTTP.post('captcha/', {kind: this.captcha_kind})).data;
this.payload.captcha.id = this.captcha.id;
this.$refs.captchaField.focus()
} finally {
this.captchaWorking = false;
}
},
},
watch: {
captcha_error(value) {
if(value) {
this.$emit('clearerrors');
this.captcha_required = true;
this.payload.captcha = {};
this.getCaptcha();
}
},
captcha_kind: function (oldKind, newKind) {
if (oldKind !== newKind) {
this.getCaptcha();
this.$refs.captchaField.getCaptcha(true);
this.payload.captcha.id = this.captchaID;
}
},
success(value) {
Expand All @@ -128,7 +65,7 @@
}
if(this.LOCAL_PUBLIC_SUFFIXES.some((suffix) => domain.name.endsWith('.' + suffix))) {
let token = this.response.data.token;
this.$router.push({ name: 'dynSetup', params: { domain: domain.name }, hash: `#${token}` });
this.$router.push({name: 'dynSetup', params: {domain: domain.name}, hash: `#${token}`});
} else {
let ds = domain.keys.map(key => key.ds);
ds = ds.concat.apply([], ds)
Expand Down
146 changes: 146 additions & 0 deletions www/webapp/src/components/Field/GenericCaptcha.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
<template>
<v-row dense align="center" class="text-center">
<v-col cols="12" sm="">
<v-text-field
v-model="inputSolution"
:label="l.inputSolution"
:hint="kind === 'image' ? l.hintImage : l.hintAudio"
:prepend-icon="mdiAccountCheck"
:rules="rules"
:error-messages="errors"
:tabindex="tabindex"
@change="errors=[]"
@keypress="errors=[]"
outlined
required
class="uppercase"
ref="captchaField"
/>
</v-col>
<v-col cols="12" sm="auto">
<v-progress-circular
v-if="working"
indeterminate
></v-progress-circular>
<img
v-if="captcha
&& !working
&& kind === 'image'"
:src="'data:'+mimeImage+';base64,'+captcha.challenge"
:alt="l.altImage"
/>
<audio controls
v-if="captcha
&& !working
&& kind === 'audio'"
>
<source :src="'data:'+mimeAudio+';base64,'+captcha.challenge" :type="mimeAudio"/>
</audio>
<br/>
<v-btn-toggle>
<v-btn text outlined @click="getCaptcha(true)" :aria-label="l.newCaptcha" :disabled="working">
<v-icon>{{ mdiRefresh }}</v-icon>
</v-btn>
</v-btn-toggle>
&nbsp;
<v-btn-toggle v-model="kind">
<v-btn text outlined value="image" :aria-label="l.switchImage" :disabled="working">
<v-icon>{{ mdiEye }}</v-icon>
</v-btn>
<v-btn text outlined value="audio" :aria-label="l.switchAudio" :disabled="working">
<v-icon>{{ mdiEarHearing }}</v-icon>
</v-btn>
</v-btn-toggle>
</v-col>
</v-row>
</template>

<script>
import {mdiAccountCheck, mdiEarHearing, mdiEye, mdiRefresh} from "@mdi/js";
import axios from "axios";
const HTTP = axios.create({
baseURL: '/api/v1/',
headers: {},
});
export default {
name: 'GenericCaptcha',
captcha_kind: '',
props: {
tabindex: {
type: String,
required: true,
},
},
data: () => ({
mdiAccountCheck,
mdiEarHearing,
mdiEye,
mdiRefresh,
captcha: null,
working: true,
inputSolution: '',
rules: [v => !!v || 'Please enter the text displayed in the picture so we are (somewhat) convinced you are human.'],
errors: [],
kind: 'image',
mimeAudio: 'audio/wav',
mimeImage: 'image/png',
l: {
altImage: 'Sign up / password reset is also possible by sending an email to our support.',
hintAudio: 'Can\'t see? Hear an audio CAPTCHA instead.',
hintImage: 'Trouble hearing? Switch to an image CAPTCHA.',
inputSolution: 'Type CAPTCHA text here',
newCaptcha: 'Get new CAPTCHA',
switchAudio: 'Switch to Audio CAPTCHA',
switchImage: 'Switch to Image CAPTCHA',
},
}),
methods: {
async getCaptcha(focus = false) {
this.working = true;
this.inputSolution = '';
await HTTP
.post('captcha/', {kind: this.kind})
.then((res) => {
this.captcha = res.data;
})
.catch((e) => {
if (e.response) {
this.errors = ['Captcha request: Server error(' + e.response.status.toString() + "): " + e.response.data.detail];
} else if (e.request) {
this.errors = ['Captcha request: Could not request from server.'];
} else {
this.errors = ['Captcha request: Unknown error.'];
}
})
;
if (focus) {
this.$refs.captchaField.focus();
}
this.working = false;
},
addError(values) {
this.errors.push(values);
},
},
async mounted() {
await this.getCaptcha();
},
computed: {
id() {
return this.captcha.id;
},
solution() {
return this.inputSolution.toUpperCase();
},
},
watch: {
kind(oldKind, newKind) {
if (oldKind !== newKind) {
this.getCaptcha(true);
}
},
},
};
</script>
Loading

0 comments on commit 56aba5d

Please sign in to comment.