浏览代码

add captcha to the form validation

olinox14 1 年之前
父节点
当前提交
dbfd3ce159
共有 9 个文件被更改,包括 429 次插入280 次删除
  1. 46 0
      components/AltchaValidation.client.vue
  2. 0 22
      components/AntiBotDialog.client.vue
  3. 39 2
      components/Contact.vue
  4. 4 1
      lang/en.json
  5. 4 1
      lang/fr.json
  6. 7 0
      nuxt.config.ts
  7. 3 1
      package.json
  8. 27 1
      pages/index.vue
  9. 299 252
      yarn.lock

+ 46 - 0
components/AltchaValidation.client.vue

@@ -0,0 +1,46 @@
+<template>
+  <altcha-widget
+    v-show="widget !== null"
+    ref="widget"
+    :challengeurl="runtimeConfig.public.challengeUrl"
+    debug
+  />
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, onUnmounted, type Ref } from 'vue'
+import 'altcha'
+import { useRuntimeConfig } from 'nuxt/app'
+
+const runtimeConfig = useRuntimeConfig()
+
+const widget: Ref<HTMLElement | null> = ref(null)
+
+const emit = defineEmits(['verified'])
+
+const onStateChange = (e: CustomEvent | Event) => {
+  if ('detail' in e) {
+    const { payload, state } = e.detail
+    if (state === 'verified' && payload) {
+      emit('verified')
+    }
+  }
+}
+
+onMounted(() => {
+  setTimeout(() => {
+    if (widget.value) {
+      widget.value.addEventListener('statechange', onStateChange)
+    }
+  }, 10)
+})
+
+onUnmounted(() => {
+  if (widget.value) {
+    widget.value.removeEventListener('statechange', onStateChange)
+  }
+})
+</script>
+
+<style scoped lang="scss">
+</style>

+ 0 - 22
components/AntiBotDialog.client.vue

@@ -1,22 +0,0 @@
-<template>
-  <div>
-    {{ $t('anti-bot-test')}}
-
-    <altcha-widget
-      ref="altchaWidget"
-      style="--altcha-max-width:100%"
-      debug
-      test
-    ></altcha-widget>
-  </div>
-</template>
-
-<script setup lang="ts">
-import 'altcha';
-
-
-</script>
-
-<style scoped lang="scss">
-
-</style>

+ 39 - 2
components/Contact.vue

@@ -41,6 +41,17 @@
           </v-col>
         </v-row>
 
+        <v-row>
+          <v-col cols="12" class="captcha-container">
+            <AltchaValidation @verified="captchaVerified = true" />
+            <v-checkbox
+              v-model="honeyPotChecked"
+              :rules="[validateCaptcha]"
+              class="hidden-ctrl"
+            />
+          </v-col>
+        </v-row>
+
         <!-- Submit Button -->
         <div class="d-flex flex-row justify-center">
           <v-btn
@@ -80,15 +91,24 @@ const contactRequestSent: Ref<boolean> = ref(false)
 
 const errorMsg: Ref<string | null> = ref(null)
 
+const i18n = useI18n()
+
 const email: Ref<string | null> = ref(null)
 const name: Ref<string | null> = ref(null)
 const message: Ref<string | null> = ref(null)
+const captchaVerified: Ref<boolean> = ref(false)
+
+// Honeypot checkbox (if checked: it's probably a bot)
+const honeyPotChecked: Ref<boolean> = ref(false)
 
 const validateEmail = (email: string | null) =>
-  (!!email && /.+@.+\..+/.test(email)) || "L'adresse e-mail doit être valide"
+  (!!email && /.+@.+\..+/.test(email)) || i18n.t("email_must_be_valid")
 
 const validateNonEmptyMessage = (message: string | null) =>
-  (!!message && message.length > 0) || 'Le message ne peut pas être vide'
+  (!!message && message.length > 10) || i18n.t("message_must_be_valid")
+
+const validateCaptcha = () =>
+  captchaVerified.value && !honeyPotChecked.value || i18n.t("captcha_must_be_validated")
 
 /**
  * Submits the contact form.
@@ -143,7 +163,24 @@ const submit = async (): Promise<void> => {
 </script>
 
 <style scoped lang="scss">
+.captcha-container {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  margin: 24px 0;
+
+  :deep(altcha-widget) {
+    min-width: 280px;
+  }
+}
+
 .confirmation-message .v-card {
   padding: 14px;
 }
+
+.hidden-ctrl {
+  :deep(.v-input__control) {
+    display: none;
+  }
+}
 </style>

+ 4 - 1
lang/en.json

@@ -39,5 +39,8 @@
   "contact_confirmation": "Your message has been sent, I will get back to you as soon as possible.",
   "continuous_improvement": "Continuous improvement",
   "system_administration": "System administration",
-  "anti-bot-test": "Anti-bot check"
+  "anti-bot-test": "Anti-bot check",
+  "email_must_be_valid": "Email adress shall be valid",
+  "message_must_be_valid": "Message is too short (10 characters minimum)",
+  "captcha_must_be_validated": "Catcha shall be validated"
 }

+ 4 - 1
lang/fr.json

@@ -39,5 +39,8 @@
   "contact_confirmation": "Votre message a bien été envoyé, je reviendrai vers vous dès que possible.",
   "continuous_improvement": "Amélioration continue",
   "system_administration": "Administration système",
-  "anti-bot-test": "Contrôle anti-robots"
+  "anti-bot-test": "Contrôle anti-robots",
+  "email_must_be_valid": "L'adresse e-mail doit être valide",
+  "message_must_be_valid": "Le message est trop court (10 caractères minimum)",
+  "captcha_must_be_validated": "Le Captcha doit être validé"
 }

+ 7 - 0
nuxt.config.ts

@@ -22,6 +22,12 @@ export default defineNuxtConfig({
       }
     },
   },
+  runtimeConfig: {
+    challengeUrl: 'https://api.ogene.fr/api/challenge',
+    public: {
+      challengeUrl: 'https://api.ogene.fr/api/challenge'
+    }
+  },
   build: {
     transpile: ['vuetify'],
   },
@@ -48,6 +54,7 @@ export default defineNuxtConfig({
     '@nuxtjs/sitemap',
     '@nuxtjs/i18n',
     '@nuxtjs/google-fonts',
+    '@pinia/nuxt',
   ],
   vite: {
     esbuild: {

+ 3 - 1
package.json

@@ -19,10 +19,12 @@
     "@nuxtjs/i18n": "^8.3.1",
     "@nuxtjs/mdc": "^0.7.1",
     "@nuxtjs/sitemap": "^5.2.0",
-    "core-js": "^3.35.1",
+    "@pinia/nuxt": "^0.5.4",
+    "altcha": "^1.0.0",
     "highlight.js": "^11.9.0",
     "nuxt": "^3.11.2",
     "nuxt-lodash": "^2.5.3",
+    "pinia": "^2.2.2",
     "sass": "^1.70.0",
     "vite-plugin-vuetify": "^2.0.3",
     "vue-matomo": "^4.2.0",

+ 27 - 1
pages/index.vue

@@ -47,6 +47,17 @@
       {{ showLongIntro ? $t('show_less') : $t('show_more') }}
     </v-btn>
 
+    <div class="banner-buttons">
+      <v-btn variant="outlined" @click="showDownloadPdf = true">
+        {{$t('download_pdf')}}
+      </v-btn>
+      <v-btn variant="outlined" to="#contact" :active="false">
+        {{$t('contact_me')}}
+      </v-btn>
+    </div>
+
+    <AntiBotDialog v-if="showDownloadPdf"></AntiBotDialog>
+
     <h5 class="find-me-on">
       {{ $t('find_me_on')}}
     </h5>
@@ -94,7 +105,7 @@
     </div>
   </div>
 
-  <div class="contact">
+  <div id="contact">
     <h3>{{ $t("Contact") }}</h3>
 
     <Contact />
@@ -118,6 +129,8 @@ const XP_YEARS = CURRENT_YEAR - START_YEAR
 
 const showLongIntro: Ref<boolean> = ref(false)
 
+const showDownloadPdf: Ref<boolean> = ref(false)
+
 </script>
 
 <style scoped lang="scss">
@@ -167,6 +180,19 @@ const showLongIntro: Ref<boolean> = ref(false)
     padding: 3px 1px;
   }
 
+  .banner-buttons {
+    display: flex;
+    flex-direction: row;
+    justify-content: center;
+
+    .v-btn {
+      margin: 6px 24px;
+      width: 160px;
+      border-radius: 18px;
+      font-weight: 500;
+    }
+  }
+
   .find-me-on {
     margin: 16px auto 0 auto;
     text-transform: uppercase;

文件差异内容过多而无法显示
+ 299 - 252
yarn.lock


部分文件因为文件数量过多而无法显示