Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(webapp): captcha to component #754

Merged
merged 1 commit into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 17 additions & 90 deletions www/webapp/src/components/ActivateAccountActionHandler.vue
Original file line number Diff line number Diff line change
@@ -1,53 +1,11 @@
<template>
<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="mdiAccountCheck"
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>{{ mdiRefresh }}</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>{{ mdiEye }}</v-icon></v-btn>
<v-btn text outlined value="audio" aria-label="Switch to Audio CAPTCHA" :disabled="captchaWorking"><v-icon>{{ mdiEarHearing }}</v-icon></v-btn>
</v-btn-toggle>
</v-col>
</v-row>
</v-container>
<generic-captcha
@update="(id, solution) => {setCaptchaPayload(id, solution)}"
tabindex="3"
ref="captchaField"
/>
<v-btn
depressed
color="primary"
Expand All @@ -64,66 +22,35 @@
</template>

<script>
import axios from 'axios';
import GenericActionHandler from "./GenericActionHandler.vue"
import {mdiAccountCheck, mdiEarHearing, mdiEye, mdiRefresh} from "@mdi/js";

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,

mdiAccountCheck: mdiAccountCheck,
mdiEarHearing: mdiEarHearing,
mdiEye: mdiEye,
mdiRefresh: mdiRefresh,

/* captcha field */
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',
captcha_required: false,
}),
computed: {
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;
}
/* captcha field */
setCaptchaPayload(id, solution) {
this.payload.captcha = {
id: id,
solution: solution,
};
},
},
watch: {
captcha_error(value) {
if(value) {
error(value) {
if(value && this.response.data.captcha !== undefined) {
// Captcha is required because not verified during the initial registration.
this.$emit('clearerrors');
this.captcha_required = true;
this.payload.captcha = {};
this.getCaptcha();
}
},
captcha_kind: function (oldKind, newKind) {
if (oldKind !== newKind) {
this.getCaptcha();
}
},
success(value) {
Expand Down
149 changes: 149 additions & 0 deletions www/webapp/src/components/Field/GenericCaptcha.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<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.hintProblemWithImage : l.hintProblemWithAudio"
:prepend-icon="mdiAccountCheck"
:rules="rules"
:error-messages="errors"
:tabindex="tabindex"
@input="emitChange()"
@change="errors=[]"
@keydown="errors=[]"
outlined
required
class="uppercase"
ref="captchaField"
></v-text-field>
</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 CAPTCHA text 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.',
hintProblemWithAudio: 'Trouble hearing? Switch to an image CAPTCHA.',
hintProblemWithImage: 'Can\'t see? Hear an audio CAPTCHA instead.',
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;
this.emitChange();
},
addError(values) {
this.errors.push(values);
},
captchaID() {
return this.captcha.id;
},
captchaSolution() {
return this.inputSolution.toUpperCase();
},
emitChange() {
this.$emit('update', this.captchaID(), this.captchaSolution());
}
},
async mounted() {
await this.getCaptcha();
},
watch: {
kind(oldKind, newKind) {
if(oldKind !== newKind) {
this.getCaptcha(true);
}
},
},
};
</script>
Loading
Loading