You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@answer.apache.org by li...@apache.org on 2024/05/06 08:49:41 UTC

(incubator-answer-plugins) 01/13: feat: add captcha plugin

This is an automated email from the ASF dual-hosted git repository.

linkinstar pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-answer-plugins.git

commit 64b6be34b1108538ca0dc337d83b2c982583623c
Author: shuai <li...@sifou.com>
AuthorDate: Tue Apr 9 11:47:05 2024 +0800

    feat: add captcha plugin
---
 captcha-basic/.eslintrc.cjs                        |  19 ++
 captcha-basic/Captcha.go                           |  23 ++
 captcha-basic/Captcha.tsx                          |   5 +
 captcha-basic/README.md                            |   2 +
 captcha-basic/i18n/en_US.json                      |  12 +
 captcha-basic/i18n/en_US.yaml                      |  14 +
 captcha-basic/i18n/index.ts                        |   7 +
 captcha-basic/i18n/translation.go                  |   6 +
 captcha-basic/i18n/zh_CN.json                      |  12 +
 captcha-basic/i18n/zh_CN.yaml                      |   9 +
 captcha-basic/index.ts                             |  15 +
 captcha-basic/interface.ts                         |  40 +++
 captcha-basic/node_modules/.bin/acorn              |  17 ++
 captcha-basic/node_modules/.bin/eslint             |  17 ++
 captcha-basic/node_modules/.bin/tsc                |  17 ++
 captcha-basic/node_modules/.bin/tsserver           |  17 ++
 captcha-basic/node_modules/.bin/vite               |  17 ++
 .../node_modules/@typescript-eslint/eslint-plugin  |   1 +
 .../node_modules/@typescript-eslint/parser         |   1 +
 .../node_modules/@vitejs/plugin-react-swc          |   1 +
 captcha-basic/node_modules/eslint                  |   1 +
 .../node_modules/eslint-plugin-react-hooks         |   1 +
 .../node_modules/eslint-plugin-react-refresh       |   1 +
 captcha-basic/node_modules/typescript              |   1 +
 captcha-basic/node_modules/vite                    |   1 +
 captcha-basic/package.json                         |  35 +++
 captcha-basic/tsconfig.json                        |  31 ++
 captcha-basic/tsconfig.node.json                   |  10 +
 captcha-basic/useCaptcha.tsx                       | 307 ++++++++++++++++++++
 captcha-basic/vite.config.ts                       |  33 +++
 captcha-google-v2/.eslintrc.cjs                    |  19 ++
 captcha-google-v2/Captcha.go                       |  23 ++
 captcha-google-v2/Captcha.tsx                      |   9 +
 captcha-google-v2/README.md                        |   2 +
 captcha-google-v2/common.ts                        |  12 +
 captcha-google-v2/i18n/en_US.json                  |  12 +
 captcha-google-v2/i18n/en_US.yaml                  |  14 +
 captcha-google-v2/i18n/index.ts                    |   7 +
 captcha-google-v2/i18n/translation.go              |   6 +
 captcha-google-v2/i18n/zh_CN.json                  |  12 +
 captcha-google-v2/i18n/zh_CN.yaml                  |   9 +
 captcha-google-v2/index.ts                         |  15 +
 captcha-google-v2/interface.ts                     |  40 +++
 captcha-google-v2/node_modules/.bin/acorn          |  17 ++
 captcha-google-v2/node_modules/.bin/eslint         |  17 ++
 captcha-google-v2/node_modules/.bin/tsc            |  17 ++
 captcha-google-v2/node_modules/.bin/tsserver       |  17 ++
 captcha-google-v2/node_modules/.bin/vite           |  17 ++
 .../node_modules/@typescript-eslint/eslint-plugin  |   1 +
 .../node_modules/@typescript-eslint/parser         |   1 +
 .../node_modules/@vitejs/plugin-react-swc          |   1 +
 captcha-google-v2/node_modules/eslint              |   1 +
 .../node_modules/eslint-plugin-react-hooks         |   1 +
 .../node_modules/eslint-plugin-react-refresh       |   1 +
 .../node_modules/react-google-recaptcha            |   1 +
 captcha-google-v2/node_modules/typescript          |   1 +
 captcha-google-v2/node_modules/vite                |   1 +
 captcha-google-v2/package.json                     |  38 +++
 captcha-google-v2/tsconfig.json                    |  31 ++
 captcha-google-v2/tsconfig.node.json               |  10 +
 captcha-google-v2/useCaptcha.tsx                   | 318 +++++++++++++++++++++
 captcha-google-v2/vite.config.ts                   |  35 +++
 62 files changed, 1379 insertions(+)

diff --git a/captcha-basic/.eslintrc.cjs b/captcha-basic/.eslintrc.cjs
new file mode 100644
index 0000000..c03c28c
--- /dev/null
+++ b/captcha-basic/.eslintrc.cjs
@@ -0,0 +1,19 @@
+module.exports = {
+  root: true,
+  env: { browser: true, es2020: true },
+  extends: [
+    'eslint:recommended',
+    'plugin:@typescript-eslint/recommended',
+    'plugin:react-hooks/recommended',
+  ],
+  ignorePatterns: ['dist', '.eslintrc.cjs'],
+  parser: '@typescript-eslint/parser',
+  plugins: ['react-refresh'],
+  rules: {
+    'react-refresh/only-export-components': [
+      'warn',
+      { allowConstantExport: true },
+    ],
+    "@typescript-eslint/no-explicit-any": "off"
+  },
+}
diff --git a/captcha-basic/Captcha.go b/captcha-basic/Captcha.go
new file mode 100644
index 0000000..5ed9dfb
--- /dev/null
+++ b/captcha-basic/Captcha.go
@@ -0,0 +1,23 @@
+package chart
+
+import (
+	"github.com/apache/incubator-answer-plugins/captcha_basic/i18n"
+	"github.com/apache/incubator-answer/plugin"
+)
+
+type ChartPlugin struct {
+}
+
+func init() {
+	plugin.Register(&ChartPlugin{})
+}
+
+func (d ChartPlugin) Info() plugin.Info {
+	return plugin.Info{
+		Name:        plugin.MakeTranslator(i18n.InfoName),
+		SlugName:    "captcha_basic",
+		Description: plugin.MakeTranslator(i18n.InfoDescription),
+		Author:      "answerdev",
+		Version:     "0.0.1",
+	}
+}
diff --git a/captcha-basic/Captcha.tsx b/captcha-basic/Captcha.tsx
new file mode 100644
index 0000000..5780ced
--- /dev/null
+++ b/captcha-basic/Captcha.tsx
@@ -0,0 +1,5 @@
+const Index = () => {
+  return <div>111 {process.env.REACT_APP_API_URL }\n {process.env.REACT_APP_BASE_URL}</div>;
+};
+
+export default Index;
diff --git a/captcha-basic/README.md b/captcha-basic/README.md
new file mode 100644
index 0000000..9af7af1
--- /dev/null
+++ b/captcha-basic/README.md
@@ -0,0 +1,2 @@
+# captcha basic
+This plug-in is the default graphical verification code plug-in, used to do some human-machine verification, prevent malicious registration and malicious submission, etc.
diff --git a/captcha-basic/i18n/en_US.json b/captcha-basic/i18n/en_US.json
new file mode 100644
index 0000000..76a3b84
--- /dev/null
+++ b/captcha-basic/i18n/en_US.json
@@ -0,0 +1,12 @@
+{
+  "plugin": {
+    "captcha_basic": {
+      "title": "Captcha",
+      "placeholder": "Type the text above",
+      "msg": {
+        "empty": "Captcha cannot be empty."
+      },
+      "verify": "Verify"
+    }
+  }
+}
diff --git a/captcha-basic/i18n/en_US.yaml b/captcha-basic/i18n/en_US.yaml
new file mode 100644
index 0000000..66394e3
--- /dev/null
+++ b/captcha-basic/i18n/en_US.yaml
@@ -0,0 +1,14 @@
+plugin:
+  captcha_basic:
+    backend:
+      info:
+        name:
+          other: Captcha Basic
+        description:
+          other: Default graphic verification code
+    frontend:
+      info:
+        name:
+          other: Captcha Basic
+        description:
+          other: Default graphic verification code
diff --git a/captcha-basic/i18n/index.ts b/captcha-basic/i18n/index.ts
new file mode 100644
index 0000000..bc0b816
--- /dev/null
+++ b/captcha-basic/i18n/index.ts
@@ -0,0 +1,7 @@
+import en_US from './en_US.json';
+import zh_CN from './zh_CN.json';
+
+export default {
+  en_US,
+  zh_CN,
+};
diff --git a/captcha-basic/i18n/translation.go b/captcha-basic/i18n/translation.go
new file mode 100644
index 0000000..f6f38a3
--- /dev/null
+++ b/captcha-basic/i18n/translation.go
@@ -0,0 +1,6 @@
+package i18n
+
+const (
+	InfoName        = "plugin.captcha_basic.backend.info.name"
+	InfoDescription = "plugin.captcha_basic.backend.info.description"
+)
diff --git a/captcha-basic/i18n/zh_CN.json b/captcha-basic/i18n/zh_CN.json
new file mode 100644
index 0000000..76a3b84
--- /dev/null
+++ b/captcha-basic/i18n/zh_CN.json
@@ -0,0 +1,12 @@
+{
+  "plugin": {
+    "captcha_basic": {
+      "title": "Captcha",
+      "placeholder": "Type the text above",
+      "msg": {
+        "empty": "Captcha cannot be empty."
+      },
+      "verify": "Verify"
+    }
+  }
+}
diff --git a/captcha-basic/i18n/zh_CN.yaml b/captcha-basic/i18n/zh_CN.yaml
new file mode 100644
index 0000000..bbaad87
--- /dev/null
+++ b/captcha-basic/i18n/zh_CN.yaml
@@ -0,0 +1,9 @@
+plugin:
+  captcha_basic:
+    backend:
+      info:
+        name:
+          other: 验证码
+        description:
+          other: 默认图形验证码
+
diff --git a/captcha-basic/index.ts b/captcha-basic/index.ts
new file mode 100644
index 0000000..5961351
--- /dev/null
+++ b/captcha-basic/index.ts
@@ -0,0 +1,15 @@
+import Captcha from './Captcha';
+import i18nConfig from './i18n';
+import useCaptcha from './useCaptcha';
+
+export default {
+  info: {
+    type: 'captcha',
+    slug_name: 'captcha_basic',
+  },
+  component: Captcha,
+  i18nConfig,
+  hooks: {
+    useCaptcha: useCaptcha,
+  },
+};
diff --git a/captcha-basic/interface.ts b/captcha-basic/interface.ts
new file mode 100644
index 0000000..3366902
--- /dev/null
+++ b/captcha-basic/interface.ts
@@ -0,0 +1,40 @@
+export interface FormValue<T = any> {
+  value: T;
+  isInvalid: boolean;
+  errorMsg: string;
+  [prop: string]: any;
+}
+
+export interface FormDataType {
+  [prop: string]: FormValue;
+}
+
+export interface FieldError {
+  error_field: string;
+  error_msg: string;
+}
+
+export interface ImgCodeReq {
+  captcha_id?: string;
+  captcha_code?: string;
+}
+
+export interface ImgCodeRes {
+  captcha_id: string;
+  captcha_img: string;
+  verify: boolean;
+}
+
+export type CaptchaKey =
+  | 'email'
+  | 'password'
+  | 'edit_userinfo'
+  | 'question'
+  | 'answer'
+  | 'comment'
+  | 'edit'
+  | 'invitation_answer'
+  | 'search'
+  | 'report'
+  | 'delete'
+  | 'vote';
diff --git a/captcha-basic/node_modules/.bin/acorn b/captcha-basic/node_modules/.bin/acorn
new file mode 100755
index 0000000..b42329e
--- /dev/null
+++ b/captcha-basic/node_modules/.bin/acorn
@@ -0,0 +1,17 @@
+#!/bin/sh
+basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
+
+case `uname` in
+    *CYGWIN*) basedir=`cygpath -w "$basedir"`;;
+esac
+
+if [ -z "$NODE_PATH" ]; then
+  export NODE_PATH="/Users/shuai/answer/answer/ui/node_modules/.pnpm/acorn@8.11.3/node_modules/acorn/bin/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/acorn@8.11.3/node_modules/acorn/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/acorn@8.11.3/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/node_modules"
+else
+  export NODE_PATH="/Users/shuai/answer/answer/ui/node_modules/.pnpm/acorn@8.11.3/node_modules/acorn/bin/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/acorn@8.11.3/node_modules/acorn/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/acorn@8.11.3/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/node_modules:$NODE_PATH"
+fi
+if [ -x "$basedir/node" ]; then
+  exec "$basedir/node"  "$basedir/../../../../../node_modules/.pnpm/acorn@8.11.3/node_modules/acorn/bin/acorn" "$@"
+else
+  exec node  "$basedir/../../../../../node_modules/.pnpm/acorn@8.11.3/node_modules/acorn/bin/acorn" "$@"
+fi
diff --git a/captcha-basic/node_modules/.bin/eslint b/captcha-basic/node_modules/.bin/eslint
new file mode 100755
index 0000000..628d350
--- /dev/null
+++ b/captcha-basic/node_modules/.bin/eslint
@@ -0,0 +1,17 @@
+#!/bin/sh
+basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
+
+case `uname` in
+    *CYGWIN*) basedir=`cygpath -w "$basedir"`;;
+esac
+
+if [ -z "$NODE_PATH" ]; then
+  export NODE_PATH="/Users/shuai/answer/answer/ui/node_modules/.pnpm/eslint@8.53.0/node_modules/eslint/bin/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/eslint@8.53.0/node_modules/eslint/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/eslint@8.53.0/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/node_modules"
+else
+  export NODE_PATH="/Users/shuai/answer/answer/ui/node_modules/.pnpm/eslint@8.53.0/node_modules/eslint/bin/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/eslint@8.53.0/node_modules/eslint/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/eslint@8.53.0/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/node_modules:$NODE_PATH"
+fi
+if [ -x "$basedir/node" ]; then
+  exec "$basedir/node"  "$basedir/../eslint/bin/eslint.js" "$@"
+else
+  exec node  "$basedir/../eslint/bin/eslint.js" "$@"
+fi
diff --git a/captcha-basic/node_modules/.bin/tsc b/captcha-basic/node_modules/.bin/tsc
new file mode 100755
index 0000000..3591c45
--- /dev/null
+++ b/captcha-basic/node_modules/.bin/tsc
@@ -0,0 +1,17 @@
+#!/bin/sh
+basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
+
+case `uname` in
+    *CYGWIN*) basedir=`cygpath -w "$basedir"`;;
+esac
+
+if [ -z "$NODE_PATH" ]; then
+  export NODE_PATH="/Users/shuai/answer/answer/ui/node_modules/.pnpm/typescript@5.3.2/node_modules/typescript/bin/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/typescript@5.3.2/node_modules/typescript/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/typescript@5.3.2/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/node_modules"
+else
+  export NODE_PATH="/Users/shuai/answer/answer/ui/node_modules/.pnpm/typescript@5.3.2/node_modules/typescript/bin/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/typescript@5.3.2/node_modules/typescript/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/typescript@5.3.2/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/node_modules:$NODE_PATH"
+fi
+if [ -x "$basedir/node" ]; then
+  exec "$basedir/node"  "$basedir/../typescript/bin/tsc" "$@"
+else
+  exec node  "$basedir/../typescript/bin/tsc" "$@"
+fi
diff --git a/captcha-basic/node_modules/.bin/tsserver b/captcha-basic/node_modules/.bin/tsserver
new file mode 100755
index 0000000..494465b
--- /dev/null
+++ b/captcha-basic/node_modules/.bin/tsserver
@@ -0,0 +1,17 @@
+#!/bin/sh
+basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
+
+case `uname` in
+    *CYGWIN*) basedir=`cygpath -w "$basedir"`;;
+esac
+
+if [ -z "$NODE_PATH" ]; then
+  export NODE_PATH="/Users/shuai/answer/answer/ui/node_modules/.pnpm/typescript@5.3.2/node_modules/typescript/bin/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/typescript@5.3.2/node_modules/typescript/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/typescript@5.3.2/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/node_modules"
+else
+  export NODE_PATH="/Users/shuai/answer/answer/ui/node_modules/.pnpm/typescript@5.3.2/node_modules/typescript/bin/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/typescript@5.3.2/node_modules/typescript/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/typescript@5.3.2/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/node_modules:$NODE_PATH"
+fi
+if [ -x "$basedir/node" ]; then
+  exec "$basedir/node"  "$basedir/../typescript/bin/tsserver" "$@"
+else
+  exec node  "$basedir/../typescript/bin/tsserver" "$@"
+fi
diff --git a/captcha-basic/node_modules/.bin/vite b/captcha-basic/node_modules/.bin/vite
new file mode 100755
index 0000000..8def179
--- /dev/null
+++ b/captcha-basic/node_modules/.bin/vite
@@ -0,0 +1,17 @@
+#!/bin/sh
+basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
+
+case `uname` in
+    *CYGWIN*) basedir=`cygpath -w "$basedir"`;;
+esac
+
+if [ -z "$NODE_PATH" ]; then
+  export NODE_PATH="/Users/shuai/answer/answer/ui/node_modules/.pnpm/vite@4.5.0_@types+node@16.11.59_sass@1.54.9/node_modules/vite/bin/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/vite@4.5.0_@types+node@16.11.59_sass@1.54.9/node_modules/vite/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/vite@4.5.0_@types+node@16.11.59_sass@1.54.9/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/node_modules"
+else
+  export NODE_PATH="/Users/shuai/answer/answer/ui/node_modules/.pnpm/vite@4.5.0_@types+node@16.11.59_sass@1.54.9/node_modules/vite/bin/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/vite@4.5.0_@types+node@16.11.59_sass@1.54.9/node_modules/vite/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/vite@4.5.0_@types+node@16.11.59_sass@1.54.9/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/node_modules:$NODE_PATH"
+fi
+if [ -x "$basedir/node" ]; then
+  exec "$basedir/node"  "$basedir/../vite/bin/vite.js" "$@"
+else
+  exec node  "$basedir/../vite/bin/vite.js" "$@"
+fi
diff --git a/captcha-basic/node_modules/@typescript-eslint/eslint-plugin b/captcha-basic/node_modules/@typescript-eslint/eslint-plugin
new file mode 120000
index 0000000..a5ee89e
--- /dev/null
+++ b/captcha-basic/node_modules/@typescript-eslint/eslint-plugin
@@ -0,0 +1 @@
+../../../../../node_modules/.pnpm/@typescript-eslint+eslint-plugin@6.11.0_@typescript-eslint+parser@6.11.0_eslint@8.53.0_typescript@5.3.2/node_modules/@typescript-eslint/eslint-plugin
\ No newline at end of file
diff --git a/captcha-basic/node_modules/@typescript-eslint/parser b/captcha-basic/node_modules/@typescript-eslint/parser
new file mode 120000
index 0000000..0608c9e
--- /dev/null
+++ b/captcha-basic/node_modules/@typescript-eslint/parser
@@ -0,0 +1 @@
+../../../../../node_modules/.pnpm/@typescript-eslint+parser@6.11.0_eslint@8.53.0_typescript@5.3.2/node_modules/@typescript-eslint/parser
\ No newline at end of file
diff --git a/captcha-basic/node_modules/@vitejs/plugin-react-swc b/captcha-basic/node_modules/@vitejs/plugin-react-swc
new file mode 120000
index 0000000..2d2c027
--- /dev/null
+++ b/captcha-basic/node_modules/@vitejs/plugin-react-swc
@@ -0,0 +1 @@
+../../../../../node_modules/.pnpm/@vitejs+plugin-react-swc@3.5.0_vite@4.5.0/node_modules/@vitejs/plugin-react-swc
\ No newline at end of file
diff --git a/captcha-basic/node_modules/eslint b/captcha-basic/node_modules/eslint
new file mode 120000
index 0000000..ca3f14f
--- /dev/null
+++ b/captcha-basic/node_modules/eslint
@@ -0,0 +1 @@
+../../../../node_modules/.pnpm/eslint@8.53.0/node_modules/eslint
\ No newline at end of file
diff --git a/captcha-basic/node_modules/eslint-plugin-react-hooks b/captcha-basic/node_modules/eslint-plugin-react-hooks
new file mode 120000
index 0000000..d6575de
--- /dev/null
+++ b/captcha-basic/node_modules/eslint-plugin-react-hooks
@@ -0,0 +1 @@
+../../../../node_modules/.pnpm/eslint-plugin-react-hooks@4.6.0_eslint@8.53.0/node_modules/eslint-plugin-react-hooks
\ No newline at end of file
diff --git a/captcha-basic/node_modules/eslint-plugin-react-refresh b/captcha-basic/node_modules/eslint-plugin-react-refresh
new file mode 120000
index 0000000..53f02e1
--- /dev/null
+++ b/captcha-basic/node_modules/eslint-plugin-react-refresh
@@ -0,0 +1 @@
+../../../../node_modules/.pnpm/eslint-plugin-react-refresh@0.4.5_eslint@8.53.0/node_modules/eslint-plugin-react-refresh
\ No newline at end of file
diff --git a/captcha-basic/node_modules/typescript b/captcha-basic/node_modules/typescript
new file mode 120000
index 0000000..ea020bc
--- /dev/null
+++ b/captcha-basic/node_modules/typescript
@@ -0,0 +1 @@
+../../../../node_modules/.pnpm/typescript@5.3.2/node_modules/typescript
\ No newline at end of file
diff --git a/captcha-basic/node_modules/vite b/captcha-basic/node_modules/vite
new file mode 120000
index 0000000..2f262d2
--- /dev/null
+++ b/captcha-basic/node_modules/vite
@@ -0,0 +1 @@
+../../../../node_modules/.pnpm/vite@4.5.0_@types+node@16.11.59_sass@1.54.9/node_modules/vite
\ No newline at end of file
diff --git a/captcha-basic/package.json b/captcha-basic/package.json
new file mode 100644
index 0000000..7574c09
--- /dev/null
+++ b/captcha-basic/package.json
@@ -0,0 +1,35 @@
+{
+  "name": "captcha_basic",
+  "private": true,
+  "author": "Answer.dev",
+  "description": "Basic for captcha",
+  "version": "0.0.1",
+  "files": [
+    "dist",
+    "README.md"
+  ],
+  "main": "./dist/captcha_basic.umd.js",
+  "module": "./dist/captcha_basic.es.js",
+  "exports": {
+    ".": {
+      "import": "./dist/captcha_basic.es.js",
+      "require": "./dist/captcha_basic.umd.js"
+    }
+  },
+  "scripts": {
+    "dev": "vite build --mode development --watch",
+    "build": "tsc && vite build",
+    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
+    "preview": "vite preview"
+  },
+  "devDependencies": {
+    "@typescript-eslint/eslint-plugin": "^6.0.0",
+    "@typescript-eslint/parser": "^6.0.0",
+    "@vitejs/plugin-react-swc": "^3.3.2",
+    "eslint": "^8.45.0",
+    "eslint-plugin-react-hooks": "^4.6.0",
+    "eslint-plugin-react-refresh": "^0.4.3",
+    "typescript": "^5.0.2",
+    "vite": "^4.4.5"
+  }
+}
diff --git a/captcha-basic/tsconfig.json b/captcha-basic/tsconfig.json
new file mode 100644
index 0000000..f6c88fb
--- /dev/null
+++ b/captcha-basic/tsconfig.json
@@ -0,0 +1,31 @@
+{
+  "compilerOptions": {
+    "target": "ES2020",
+    "useDefineForClassFields": true,
+    "lib": [
+      "ES2020",
+      "DOM",
+      "DOM.Iterable"
+    ],
+    "module": "ESNext",
+    "skipLibCheck": true,
+    /* Bundler mode */
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "noImplicitAny": false,
+    "noEmit": true,
+    "jsx": "react-jsx",
+    /* Linting */
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "noFallthroughCasesInSwitch": true,
+  },
+  "references": [
+    {
+      "path": "./tsconfig.node.json"
+    }
+  ]
+}
diff --git a/captcha-basic/tsconfig.node.json b/captcha-basic/tsconfig.node.json
new file mode 100644
index 0000000..42872c5
--- /dev/null
+++ b/captcha-basic/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+  "compilerOptions": {
+    "composite": true,
+    "skipLibCheck": true,
+    "module": "ESNext",
+    "moduleResolution": "bundler",
+    "allowSyntheticDefaultImports": true
+  },
+  "include": ["vite.config.ts"]
+}
diff --git a/captcha-basic/useCaptcha.tsx b/captcha-basic/useCaptcha.tsx
new file mode 100644
index 0000000..152d333
--- /dev/null
+++ b/captcha-basic/useCaptcha.tsx
@@ -0,0 +1,307 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { useEffect, useRef, useState, useLayoutEffect } from 'react';
+import { Modal, Form, Button, InputGroup } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+
+import ReactDOM from 'react-dom/client';
+
+import type {
+  FormValue,
+  ImgCodeRes,
+  CaptchaKey,
+  FieldError,
+  ImgCodeReq,
+} from './interface';
+
+const checkImgCode = (key: CaptchaKey) => {
+  return new Promise<ImgCodeRes>((resolve) => {
+    fetch(`/answer/api/v1/user/action/record?action=${key}`)
+      .then((resp) => {
+        return resp.json();
+      })
+      .then((data) => {
+        console.log('checkImgCode', data);
+        resolve(data.data);
+      });
+  });
+};
+
+type SubmitCallback = {
+  (): void;
+};
+
+const Index = (captchaKey: CaptchaKey) => {
+  const refRoot = useRef<ReactDOM.Root | null>(null);
+  if (refRoot.current === null) {
+    refRoot.current = ReactDOM.createRoot(document.createElement('div'));
+  }
+
+  const { t } = useTranslation('plugin', {
+    keyPrefix: 'captcha_basic',
+  });
+  const refKey = useRef<CaptchaKey>(captchaKey);
+  const refCallback = useRef<SubmitCallback>();
+  const pending = useRef(false);
+  const autoInitCaptchaData = /email/i.test(refKey.current);
+
+  const [stateShow, setStateShow] = useState(false);
+  const [captcha, setCaptcha] = useState<ImgCodeRes>({
+    captcha_id: '',
+    captcha_img: '',
+    verify: false,
+  });
+  const [imgCode, setImgCode] = useState<FormValue>({
+    value: '',
+    isInvalid: false,
+    errorMsg: '',
+  });
+  const refCaptcha = useRef(captcha);
+  const refImgCode = useRef(imgCode);
+
+  const fetchCaptchaData = () => {
+    pending.current = true;
+    checkImgCode(refKey.current)
+      .then((resp) => {
+        setCaptcha(resp);
+      })
+      .finally(() => {
+        pending.current = false;
+      });
+  };
+
+  const resetCapture = () => {
+    setCaptcha({
+      captcha_id: '',
+      captcha_img: '',
+      verify: false,
+    });
+  };
+
+  const resetImgCode = () => {
+    setImgCode({
+      value: '',
+      isInvalid: false,
+      errorMsg: '',
+    });
+  };
+  const resetCallback = () => {
+    refCallback.current = undefined;
+  };
+
+  const show = () => {
+    if (!stateShow) {
+      setStateShow(true);
+    }
+  };
+  /**
+   * There are some cases where the React scheduler cancels the execution of some functions,
+   * which prevents them from closing properly:
+   *  for example, if the parent component uninstalls the child component directly,
+   *  and the `captchaModal.close()` call is inside the child component.
+   * In this case, call `await captchaModal.close()` and wait for the close action to complete.
+   */
+  const close = () => {
+    setStateShow(false);
+    resetCapture();
+    resetImgCode();
+    resetCallback();
+
+    const p = new Promise<void>((resolve) => {
+      setTimeout(resolve, 50);
+    });
+    return p;
+  };
+
+  const handleCaptchaError = (fel: FieldError[] = []) => {
+    console.log('handleCaptchaError', fel);
+    const captchaErr = fel.find((o) => {
+      return o.error_field === 'captcha_code';
+    });
+
+    const ri = refImgCode.current;
+    if (captchaErr) {
+      /**
+       * `imgCode.value` No value but a validation error is received,
+       * indicating that it is the first time the interface has returned a CAPTCHA error,
+       * triggering the CAPTCHA logic. There is no need to display the error message at this point.
+       */
+      if (ri.value) {
+        setImgCode({
+          ...ri,
+          isInvalid: true,
+          errorMsg: captchaErr.error_msg,
+        });
+      }
+      fetchCaptchaData();
+      show();
+    } else {
+      close();
+    }
+    // Assist business logic in filtering CAPTCHA error messages when necessary
+    return captchaErr;
+  };
+
+  const handleChange = (evt) => {
+    evt.preventDefault();
+    setImgCode({
+      value: evt.target.value || '',
+      isInvalid: false,
+      errorMsg: '',
+    });
+  };
+
+  const getCaptcha = () => {
+    const rc = refCaptcha.current;
+    const ri = refImgCode.current;
+    const r = {
+      verify: !!rc?.verify,
+      captcha_id: rc?.captcha_id,
+      captcha_code: ri.value,
+    };
+
+    return r;
+  };
+
+  const resolveCaptchaReq = (req: ImgCodeReq) => {
+    const r = getCaptcha();
+    if (r.verify) {
+      req.captcha_code = r.captcha_code;
+      req.captcha_id = r.captcha_id;
+    }
+  };
+
+  const handleSubmit = (evt) => {
+    evt.preventDefault();
+    if (!imgCode.value) {
+      return;
+    }
+
+    if (refCallback.current) {
+      refCallback.current();
+    }
+  };
+
+  useEffect(() => {
+    if (autoInitCaptchaData) {
+      fetchCaptchaData();
+    }
+  }, []);
+
+  useLayoutEffect(() => {
+    refImgCode.current = imgCode;
+    refCaptcha.current = captcha;
+  }, [captcha, imgCode]);
+
+  useEffect(() => {
+    refRoot.current?.render(
+      <Modal
+        size="sm"
+        title="Captcha"
+        show={stateShow}
+        onHide={() => close()}
+        centered>
+        <Modal.Header closeButton>
+          <Modal.Title as="h5">{t('title')}</Modal.Title>
+        </Modal.Header>
+        <Modal.Body>
+          <Form noValidate onSubmit={handleSubmit}>
+            <Form.Group controlId="code" className="mb-3">
+              <div className="mb-3 p-2 d-flex align-items-center justify-content-center bg-light rounded-2">
+                <img
+                  src={captcha?.captcha_img}
+                  alt="captcha img"
+                  width="auto"
+                  height="60px"
+                />
+              </div>
+              <InputGroup>
+                <Form.Control
+                  type="text"
+                  autoComplete="off"
+                  placeholder={t('placeholder')}
+                  isInvalid={imgCode?.isInvalid}
+                  onChange={handleChange}
+                  value={imgCode.value}
+                />
+                <Button
+                  onClick={fetchCaptchaData}
+                  variant="outline-secondary"
+                  title={t('refresh', { keyPrefix: 'btns' })}
+                  style={{
+                    borderTopRightRadius: '0.375rem',
+                    borderBottomRightRadius: '0.375rem',
+                  }}>
+                  <svg
+                    xmlns="http://www.w3.org/2000/svg"
+                    width="16"
+                    height="16"
+                    fill="currentColor"
+                    className="bi bi-arrow-repeat"
+                    viewBox="0 0 16 16">
+                    <path d="M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41m-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9" />
+                    <path
+                      fillRule="evenodd"
+                      d="M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5 5 0 0 0 8 3M3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9z"
+                    />
+                  </svg>
+                </Button>
+
+                <Form.Control.Feedback type="invalid">
+                  {imgCode?.errorMsg}
+                </Form.Control.Feedback>
+              </InputGroup>
+            </Form.Group>
+
+            <div className="d-grid">
+              <Button type="submit" disabled={!imgCode.value}>
+                {t('verify')}
+              </Button>
+            </div>
+          </Form>
+        </Modal.Body>
+      </Modal>,
+    );
+  });
+
+  const r = {
+    close,
+    show,
+    check: (submitFunc: SubmitCallback) => {
+      if (pending.current) {
+        return false;
+      }
+      refCallback.current = submitFunc;
+      if (captcha?.verify) {
+        show();
+        return false;
+      }
+      return submitFunc();
+    },
+    getCaptcha,
+    resolveCaptchaReq,
+    fetchCaptchaData,
+    handleCaptchaError,
+  };
+
+  return r;
+};
+
+export default Index;
diff --git a/captcha-basic/vite.config.ts b/captcha-basic/vite.config.ts
new file mode 100644
index 0000000..d3c9777
--- /dev/null
+++ b/captcha-basic/vite.config.ts
@@ -0,0 +1,33 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react-swc';
+import packageJson from './package.json';
+
+// https://vitejs.dev/config/
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  plugins: [react()],
+  build: {
+    lib: {
+      entry: 'index.ts',
+      name: packageJson.name,
+      fileName: (format) => `${packageJson.name}.${format}.js`,
+    },
+    rollupOptions: {
+      external: [
+        'react',
+        'react-dom',
+        'react-i18next',
+        'react-bootstrap',
+      ],
+      output: {
+        globals: {
+          react: 'React',
+          'react-dom': 'ReactDOM',
+          'react-i18next': 'reactI18next',
+          'react-bootstrap': 'reactBootstrap',
+        },
+      },
+    },
+  },
+});
diff --git a/captcha-google-v2/.eslintrc.cjs b/captcha-google-v2/.eslintrc.cjs
new file mode 100644
index 0000000..c03c28c
--- /dev/null
+++ b/captcha-google-v2/.eslintrc.cjs
@@ -0,0 +1,19 @@
+module.exports = {
+  root: true,
+  env: { browser: true, es2020: true },
+  extends: [
+    'eslint:recommended',
+    'plugin:@typescript-eslint/recommended',
+    'plugin:react-hooks/recommended',
+  ],
+  ignorePatterns: ['dist', '.eslintrc.cjs'],
+  parser: '@typescript-eslint/parser',
+  plugins: ['react-refresh'],
+  rules: {
+    'react-refresh/only-export-components': [
+      'warn',
+      { allowConstantExport: true },
+    ],
+    "@typescript-eslint/no-explicit-any": "off"
+  },
+}
diff --git a/captcha-google-v2/Captcha.go b/captcha-google-v2/Captcha.go
new file mode 100644
index 0000000..f9e144a
--- /dev/null
+++ b/captcha-google-v2/Captcha.go
@@ -0,0 +1,23 @@
+package chart
+
+import (
+	"github.com/apache/incubator-answer-plugins/captcha_google_v2/i18n"
+	"github.com/apache/incubator-answer/plugin"
+)
+
+type ChartPlugin struct {
+}
+
+func init() {
+	plugin.Register(&ChartPlugin{})
+}
+
+func (d ChartPlugin) Info() plugin.Info {
+	return plugin.Info{
+		Name:        plugin.MakeTranslator(i18n.InfoName),
+		SlugName:    "captcha_google_v2",
+		Description: plugin.MakeTranslator(i18n.InfoDescription),
+		Author:      "answerdev",
+		Version:     "0.0.1",
+	}
+}
diff --git a/captcha-google-v2/Captcha.tsx b/captcha-google-v2/Captcha.tsx
new file mode 100644
index 0000000..9d626e3
--- /dev/null
+++ b/captcha-google-v2/Captcha.tsx
@@ -0,0 +1,9 @@
+const Index = () => {
+  return (
+    <div>
+      <div>gogle v2</div>
+    </div>
+  );
+};
+
+export default Index;
diff --git a/captcha-google-v2/README.md b/captcha-google-v2/README.md
new file mode 100644
index 0000000..fddceac
--- /dev/null
+++ b/captcha-google-v2/README.md
@@ -0,0 +1,2 @@
+# captcha google v2
+This plug-in is the default graphical verification code plug-in, used to do some human-machine verification, prevent malicious registration and malicious submission, etc.
diff --git a/captcha-google-v2/common.ts b/captcha-google-v2/common.ts
new file mode 100644
index 0000000..228bc94
--- /dev/null
+++ b/captcha-google-v2/common.ts
@@ -0,0 +1,12 @@
+/**
+ * @description: System language conversion bit google reacptcah corresponding language format
+ * @link: https://developers.google.com/recaptcha/docs/language
+ */
+export const languageKeys = {
+  af_ZA: 'af',
+  ar_SA: 'ar',
+  az_AZ: 'az',
+  en_US: 'en',
+  zh_CN: 'zh-CN',
+  zh_TW: 'zh-TW',
+};
diff --git a/captcha-google-v2/i18n/en_US.json b/captcha-google-v2/i18n/en_US.json
new file mode 100644
index 0000000..7b8f588
--- /dev/null
+++ b/captcha-google-v2/i18n/en_US.json
@@ -0,0 +1,12 @@
+{
+  "plugin": {
+    "captcha_google_v2": {
+      "title": "Captcha",
+      "placeholder": "Type the text above",
+      "msg": {
+        "empty": "Captcha cannot be empty."
+      },
+      "verify": "Verify"
+    }
+  }
+}
diff --git a/captcha-google-v2/i18n/en_US.yaml b/captcha-google-v2/i18n/en_US.yaml
new file mode 100644
index 0000000..1e00fb3
--- /dev/null
+++ b/captcha-google-v2/i18n/en_US.yaml
@@ -0,0 +1,14 @@
+plugin:
+  captcha_google_v2:
+    backend:
+      info:
+        name:
+          other: Captcha Basic
+        description:
+          other: Default graphic verification code
+    frontend:
+      info:
+        name:
+          other: Captcha Basic
+        description:
+          other: Default graphic verification code
diff --git a/captcha-google-v2/i18n/index.ts b/captcha-google-v2/i18n/index.ts
new file mode 100644
index 0000000..bc0b816
--- /dev/null
+++ b/captcha-google-v2/i18n/index.ts
@@ -0,0 +1,7 @@
+import en_US from './en_US.json';
+import zh_CN from './zh_CN.json';
+
+export default {
+  en_US,
+  zh_CN,
+};
diff --git a/captcha-google-v2/i18n/translation.go b/captcha-google-v2/i18n/translation.go
new file mode 100644
index 0000000..9ccf6fa
--- /dev/null
+++ b/captcha-google-v2/i18n/translation.go
@@ -0,0 +1,6 @@
+package i18n
+
+const (
+	InfoName        = "plugin.captcha_google_v2.backend.info.name"
+	InfoDescription = "plugin.captcha_google_v2.backend.info.description"
+)
diff --git a/captcha-google-v2/i18n/zh_CN.json b/captcha-google-v2/i18n/zh_CN.json
new file mode 100644
index 0000000..7b8f588
--- /dev/null
+++ b/captcha-google-v2/i18n/zh_CN.json
@@ -0,0 +1,12 @@
+{
+  "plugin": {
+    "captcha_google_v2": {
+      "title": "Captcha",
+      "placeholder": "Type the text above",
+      "msg": {
+        "empty": "Captcha cannot be empty."
+      },
+      "verify": "Verify"
+    }
+  }
+}
diff --git a/captcha-google-v2/i18n/zh_CN.yaml b/captcha-google-v2/i18n/zh_CN.yaml
new file mode 100644
index 0000000..f0e5f25
--- /dev/null
+++ b/captcha-google-v2/i18n/zh_CN.yaml
@@ -0,0 +1,9 @@
+plugin:
+  captcha_google_v2:
+    backend:
+      info:
+        name:
+          other: 验证码
+        description:
+          other: 默认图形验证码
+
diff --git a/captcha-google-v2/index.ts b/captcha-google-v2/index.ts
new file mode 100644
index 0000000..e7c6fe1
--- /dev/null
+++ b/captcha-google-v2/index.ts
@@ -0,0 +1,15 @@
+import Captcha from './Captcha';
+import i18nConfig from './i18n';
+import useCaptcha from './useCaptcha';
+
+export default {
+  info: {
+    type: 'captcha',
+    slug_name: 'captcha_google_v2',
+  },
+  component: Captcha,
+  i18nConfig,
+  hooks: {
+    useCaptcha: useCaptcha,
+  },
+};
diff --git a/captcha-google-v2/interface.ts b/captcha-google-v2/interface.ts
new file mode 100644
index 0000000..3366902
--- /dev/null
+++ b/captcha-google-v2/interface.ts
@@ -0,0 +1,40 @@
+export interface FormValue<T = any> {
+  value: T;
+  isInvalid: boolean;
+  errorMsg: string;
+  [prop: string]: any;
+}
+
+export interface FormDataType {
+  [prop: string]: FormValue;
+}
+
+export interface FieldError {
+  error_field: string;
+  error_msg: string;
+}
+
+export interface ImgCodeReq {
+  captcha_id?: string;
+  captcha_code?: string;
+}
+
+export interface ImgCodeRes {
+  captcha_id: string;
+  captcha_img: string;
+  verify: boolean;
+}
+
+export type CaptchaKey =
+  | 'email'
+  | 'password'
+  | 'edit_userinfo'
+  | 'question'
+  | 'answer'
+  | 'comment'
+  | 'edit'
+  | 'invitation_answer'
+  | 'search'
+  | 'report'
+  | 'delete'
+  | 'vote';
diff --git a/captcha-google-v2/node_modules/.bin/acorn b/captcha-google-v2/node_modules/.bin/acorn
new file mode 100755
index 0000000..b42329e
--- /dev/null
+++ b/captcha-google-v2/node_modules/.bin/acorn
@@ -0,0 +1,17 @@
+#!/bin/sh
+basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
+
+case `uname` in
+    *CYGWIN*) basedir=`cygpath -w "$basedir"`;;
+esac
+
+if [ -z "$NODE_PATH" ]; then
+  export NODE_PATH="/Users/shuai/answer/answer/ui/node_modules/.pnpm/acorn@8.11.3/node_modules/acorn/bin/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/acorn@8.11.3/node_modules/acorn/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/acorn@8.11.3/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/node_modules"
+else
+  export NODE_PATH="/Users/shuai/answer/answer/ui/node_modules/.pnpm/acorn@8.11.3/node_modules/acorn/bin/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/acorn@8.11.3/node_modules/acorn/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/acorn@8.11.3/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/node_modules:$NODE_PATH"
+fi
+if [ -x "$basedir/node" ]; then
+  exec "$basedir/node"  "$basedir/../../../../../node_modules/.pnpm/acorn@8.11.3/node_modules/acorn/bin/acorn" "$@"
+else
+  exec node  "$basedir/../../../../../node_modules/.pnpm/acorn@8.11.3/node_modules/acorn/bin/acorn" "$@"
+fi
diff --git a/captcha-google-v2/node_modules/.bin/eslint b/captcha-google-v2/node_modules/.bin/eslint
new file mode 100755
index 0000000..bd67541
--- /dev/null
+++ b/captcha-google-v2/node_modules/.bin/eslint
@@ -0,0 +1,17 @@
+#!/bin/sh
+basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
+
+case `uname` in
+    *CYGWIN*) basedir=`cygpath -w "$basedir"`;;
+esac
+
+if [ -z "$NODE_PATH" ]; then
+  export NODE_PATH="/Users/shuai/answer/answer/ui/node_modules/.pnpm/eslint@8.53.0/node_modules/eslint/bin/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/eslint@8.53.0/node_modules/eslint/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/eslint@8.53.0/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/node_modules"
+else
+  export NODE_PATH="/Users/shuai/answer/answer/ui/node_modules/.pnpm/eslint@8.53.0/node_modules/eslint/bin/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/eslint@8.53.0/node_modules/eslint/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/eslint@8.53.0/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/node_modules:$NODE_PATH"
+fi
+if [ -x "$basedir/node" ]; then
+  exec "$basedir/node"  "$basedir/../../../../../node_modules/.pnpm/eslint@8.53.0/node_modules/eslint/bin/eslint.js" "$@"
+else
+  exec node  "$basedir/../../../../../node_modules/.pnpm/eslint@8.53.0/node_modules/eslint/bin/eslint.js" "$@"
+fi
diff --git a/captcha-google-v2/node_modules/.bin/tsc b/captcha-google-v2/node_modules/.bin/tsc
new file mode 100755
index 0000000..60f3a2b
--- /dev/null
+++ b/captcha-google-v2/node_modules/.bin/tsc
@@ -0,0 +1,17 @@
+#!/bin/sh
+basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
+
+case `uname` in
+    *CYGWIN*) basedir=`cygpath -w "$basedir"`;;
+esac
+
+if [ -z "$NODE_PATH" ]; then
+  export NODE_PATH="/Users/shuai/answer/answer/ui/node_modules/.pnpm/typescript@5.3.2/node_modules/typescript/bin/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/typescript@5.3.2/node_modules/typescript/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/typescript@5.3.2/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/node_modules"
+else
+  export NODE_PATH="/Users/shuai/answer/answer/ui/node_modules/.pnpm/typescript@5.3.2/node_modules/typescript/bin/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/typescript@5.3.2/node_modules/typescript/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/typescript@5.3.2/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/node_modules:$NODE_PATH"
+fi
+if [ -x "$basedir/node" ]; then
+  exec "$basedir/node"  "$basedir/../../../../../node_modules/.pnpm/typescript@5.3.2/node_modules/typescript/bin/tsc" "$@"
+else
+  exec node  "$basedir/../../../../../node_modules/.pnpm/typescript@5.3.2/node_modules/typescript/bin/tsc" "$@"
+fi
diff --git a/captcha-google-v2/node_modules/.bin/tsserver b/captcha-google-v2/node_modules/.bin/tsserver
new file mode 100755
index 0000000..49af353
--- /dev/null
+++ b/captcha-google-v2/node_modules/.bin/tsserver
@@ -0,0 +1,17 @@
+#!/bin/sh
+basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
+
+case `uname` in
+    *CYGWIN*) basedir=`cygpath -w "$basedir"`;;
+esac
+
+if [ -z "$NODE_PATH" ]; then
+  export NODE_PATH="/Users/shuai/answer/answer/ui/node_modules/.pnpm/typescript@5.3.2/node_modules/typescript/bin/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/typescript@5.3.2/node_modules/typescript/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/typescript@5.3.2/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/node_modules"
+else
+  export NODE_PATH="/Users/shuai/answer/answer/ui/node_modules/.pnpm/typescript@5.3.2/node_modules/typescript/bin/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/typescript@5.3.2/node_modules/typescript/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/typescript@5.3.2/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/node_modules:$NODE_PATH"
+fi
+if [ -x "$basedir/node" ]; then
+  exec "$basedir/node"  "$basedir/../../../../../node_modules/.pnpm/typescript@5.3.2/node_modules/typescript/bin/tsserver" "$@"
+else
+  exec node  "$basedir/../../../../../node_modules/.pnpm/typescript@5.3.2/node_modules/typescript/bin/tsserver" "$@"
+fi
diff --git a/captcha-google-v2/node_modules/.bin/vite b/captcha-google-v2/node_modules/.bin/vite
new file mode 100755
index 0000000..05fe5ab
--- /dev/null
+++ b/captcha-google-v2/node_modules/.bin/vite
@@ -0,0 +1,17 @@
+#!/bin/sh
+basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
+
+case `uname` in
+    *CYGWIN*) basedir=`cygpath -w "$basedir"`;;
+esac
+
+if [ -z "$NODE_PATH" ]; then
+  export NODE_PATH="/Users/shuai/answer/answer/ui/node_modules/.pnpm/vite@4.5.0/node_modules/vite/bin/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/vite@4.5.0/node_modules/vite/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/vite@4.5.0/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/node_modules"
+else
+  export NODE_PATH="/Users/shuai/answer/answer/ui/node_modules/.pnpm/vite@4.5.0/node_modules/vite/bin/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/vite@4.5.0/node_modules/vite/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/vite@4.5.0/node_modules:/Users/shuai/answer/answer/ui/node_modules/.pnpm/node_modules:$NODE_PATH"
+fi
+if [ -x "$basedir/node" ]; then
+  exec "$basedir/node"  "$basedir/../../../../../node_modules/.pnpm/vite@4.5.0/node_modules/vite/bin/vite.js" "$@"
+else
+  exec node  "$basedir/../../../../../node_modules/.pnpm/vite@4.5.0/node_modules/vite/bin/vite.js" "$@"
+fi
diff --git a/captcha-google-v2/node_modules/@typescript-eslint/eslint-plugin b/captcha-google-v2/node_modules/@typescript-eslint/eslint-plugin
new file mode 120000
index 0000000..a5ee89e
--- /dev/null
+++ b/captcha-google-v2/node_modules/@typescript-eslint/eslint-plugin
@@ -0,0 +1 @@
+../../../../../node_modules/.pnpm/@typescript-eslint+eslint-plugin@6.11.0_@typescript-eslint+parser@6.11.0_eslint@8.53.0_typescript@5.3.2/node_modules/@typescript-eslint/eslint-plugin
\ No newline at end of file
diff --git a/captcha-google-v2/node_modules/@typescript-eslint/parser b/captcha-google-v2/node_modules/@typescript-eslint/parser
new file mode 120000
index 0000000..0608c9e
--- /dev/null
+++ b/captcha-google-v2/node_modules/@typescript-eslint/parser
@@ -0,0 +1 @@
+../../../../../node_modules/.pnpm/@typescript-eslint+parser@6.11.0_eslint@8.53.0_typescript@5.3.2/node_modules/@typescript-eslint/parser
\ No newline at end of file
diff --git a/captcha-google-v2/node_modules/@vitejs/plugin-react-swc b/captcha-google-v2/node_modules/@vitejs/plugin-react-swc
new file mode 120000
index 0000000..2d2c027
--- /dev/null
+++ b/captcha-google-v2/node_modules/@vitejs/plugin-react-swc
@@ -0,0 +1 @@
+../../../../../node_modules/.pnpm/@vitejs+plugin-react-swc@3.5.0_vite@4.5.0/node_modules/@vitejs/plugin-react-swc
\ No newline at end of file
diff --git a/captcha-google-v2/node_modules/eslint b/captcha-google-v2/node_modules/eslint
new file mode 120000
index 0000000..ca3f14f
--- /dev/null
+++ b/captcha-google-v2/node_modules/eslint
@@ -0,0 +1 @@
+../../../../node_modules/.pnpm/eslint@8.53.0/node_modules/eslint
\ No newline at end of file
diff --git a/captcha-google-v2/node_modules/eslint-plugin-react-hooks b/captcha-google-v2/node_modules/eslint-plugin-react-hooks
new file mode 120000
index 0000000..d6575de
--- /dev/null
+++ b/captcha-google-v2/node_modules/eslint-plugin-react-hooks
@@ -0,0 +1 @@
+../../../../node_modules/.pnpm/eslint-plugin-react-hooks@4.6.0_eslint@8.53.0/node_modules/eslint-plugin-react-hooks
\ No newline at end of file
diff --git a/captcha-google-v2/node_modules/eslint-plugin-react-refresh b/captcha-google-v2/node_modules/eslint-plugin-react-refresh
new file mode 120000
index 0000000..53f02e1
--- /dev/null
+++ b/captcha-google-v2/node_modules/eslint-plugin-react-refresh
@@ -0,0 +1 @@
+../../../../node_modules/.pnpm/eslint-plugin-react-refresh@0.4.5_eslint@8.53.0/node_modules/eslint-plugin-react-refresh
\ No newline at end of file
diff --git a/captcha-google-v2/node_modules/react-google-recaptcha b/captcha-google-v2/node_modules/react-google-recaptcha
new file mode 120000
index 0000000..f6eec60
--- /dev/null
+++ b/captcha-google-v2/node_modules/react-google-recaptcha
@@ -0,0 +1 @@
+../../../../node_modules/.pnpm/react-google-recaptcha@3.1.0_react@18.2.0/node_modules/react-google-recaptcha
\ No newline at end of file
diff --git a/captcha-google-v2/node_modules/typescript b/captcha-google-v2/node_modules/typescript
new file mode 120000
index 0000000..ea020bc
--- /dev/null
+++ b/captcha-google-v2/node_modules/typescript
@@ -0,0 +1 @@
+../../../../node_modules/.pnpm/typescript@5.3.2/node_modules/typescript
\ No newline at end of file
diff --git a/captcha-google-v2/node_modules/vite b/captcha-google-v2/node_modules/vite
new file mode 120000
index 0000000..0d47d66
--- /dev/null
+++ b/captcha-google-v2/node_modules/vite
@@ -0,0 +1 @@
+../../../../node_modules/.pnpm/vite@4.5.0/node_modules/vite
\ No newline at end of file
diff --git a/captcha-google-v2/package.json b/captcha-google-v2/package.json
new file mode 100644
index 0000000..28a2882
--- /dev/null
+++ b/captcha-google-v2/package.json
@@ -0,0 +1,38 @@
+{
+  "name": "captcha_google_v2",
+  "private": true,
+  "author": "Answer.dev",
+  "description": "google reCaptcha v2",
+  "version": "0.0.1",
+  "files": [
+    "dist",
+    "README.md"
+  ],
+  "main": "./dist/captcha_google_v2.umd.js",
+  "module": "./dist/captcha_google_v2.es.js",
+  "exports": {
+    ".": {
+      "import": "./dist/captcha_google_v2.es.js",
+      "require": "./dist/captcha_google_v2.umd.js"
+    }
+  },
+  "scripts": {
+    "dev": "vite build --mode development --watch",
+    "build": "tsc && vite build",
+    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
+    "preview": "vite preview"
+  },
+  "devDependencies": {
+    "@typescript-eslint/eslint-plugin": "^6.0.0",
+    "@typescript-eslint/parser": "^6.0.0",
+    "@vitejs/plugin-react-swc": "^3.3.2",
+    "eslint": "^8.45.0",
+    "eslint-plugin-react-hooks": "^4.6.0",
+    "eslint-plugin-react-refresh": "^0.4.3",
+    "typescript": "^5.0.2",
+    "vite": "^4.4.5"
+  },
+  "dependencies": {
+    "react-google-recaptcha": "^3.1.0"
+  }
+}
diff --git a/captcha-google-v2/tsconfig.json b/captcha-google-v2/tsconfig.json
new file mode 100644
index 0000000..f6c88fb
--- /dev/null
+++ b/captcha-google-v2/tsconfig.json
@@ -0,0 +1,31 @@
+{
+  "compilerOptions": {
+    "target": "ES2020",
+    "useDefineForClassFields": true,
+    "lib": [
+      "ES2020",
+      "DOM",
+      "DOM.Iterable"
+    ],
+    "module": "ESNext",
+    "skipLibCheck": true,
+    /* Bundler mode */
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "noImplicitAny": false,
+    "noEmit": true,
+    "jsx": "react-jsx",
+    /* Linting */
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "noFallthroughCasesInSwitch": true,
+  },
+  "references": [
+    {
+      "path": "./tsconfig.node.json"
+    }
+  ]
+}
diff --git a/captcha-google-v2/tsconfig.node.json b/captcha-google-v2/tsconfig.node.json
new file mode 100644
index 0000000..42872c5
--- /dev/null
+++ b/captcha-google-v2/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+  "compilerOptions": {
+    "composite": true,
+    "skipLibCheck": true,
+    "module": "ESNext",
+    "moduleResolution": "bundler",
+    "allowSyntheticDefaultImports": true
+  },
+  "include": ["vite.config.ts"]
+}
diff --git a/captcha-google-v2/useCaptcha.tsx b/captcha-google-v2/useCaptcha.tsx
new file mode 100644
index 0000000..d188c9a
--- /dev/null
+++ b/captcha-google-v2/useCaptcha.tsx
@@ -0,0 +1,318 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { useEffect, useRef, useState, useLayoutEffect } from 'react';
+import { Modal, Form, Button, InputGroup, Spinner } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+import ReCAPTCHA from "react-google-recaptcha";
+
+import ReactDOM from 'react-dom/client';
+
+import { languageKeys } from './common';
+import type {
+  FormValue,
+  ImgCodeRes,
+  CaptchaKey,
+  FieldError,
+  ImgCodeReq,
+} from './interface';
+
+const checkImgCode = (key: CaptchaKey) => {
+  return new Promise<ImgCodeRes>((resolve) => {
+    fetch(`/answer/api/v1/user/action/record?action=${key}`)
+      .then((resp) => {
+        return resp.json();
+      })
+      .then((data) => {
+        console.log('checkImgCode', data);
+        resolve(data.data);
+      });
+  });
+};
+
+type SubmitCallback = {
+  (): void;
+};
+
+const Index = (captchaKey: CaptchaKey) => {
+  const refRoot = useRef<ReactDOM.Root | null>(null);
+
+  if (refRoot.current === null) {
+    refRoot.current = ReactDOM.createRoot(document.createElement('div'));
+  }
+
+  const { t, i18n } = useTranslation('plugin', {
+    keyPrefix: 'captcha_basic',
+  });
+
+  const refKey = useRef<CaptchaKey>(captchaKey);
+  const refCallback = useRef<SubmitCallback>();
+  const pending = useRef(false);
+  const autoInitCaptchaData = /email/i.test(refKey.current);
+
+  const [isLoading, setIsLoading] = useState(true);
+  const [stateShow, setStateShow] = useState(false);
+  const [googleKey, setGoogleKey] = useState('');
+  const [captcha, setCaptcha] = useState<ImgCodeRes>({
+    captcha_id: '',
+    captcha_img: '',
+    verify: false,
+  });
+  const [imgCode, setImgCode] = useState<FormValue>({
+    value: '',
+    isInvalid: false,
+    errorMsg: '',
+  });
+  const refCaptcha = useRef(captcha);
+  const refImgCode = useRef(imgCode);
+
+  const getCaptchaKey = () => {
+    fetch(`/answer/api/v1/captcha/config`)
+      .then((resp) => {
+        return resp.json();
+      })
+      .then((data) => {
+        setGoogleKey(data?.data?.config.key);
+      })
+  }
+
+  const fetchCaptchaData = () => {
+    pending.current = true;
+    checkImgCode(refKey.current)
+      .then((resp) => {
+        setCaptcha(resp);
+      })
+      .finally(() => {
+        pending.current = false;
+      });
+  };
+
+  const resetCapture = () => {
+    setCaptcha({
+      captcha_id: '',
+      captcha_img: '',
+      verify: false,
+    });
+  };
+
+  const resetImgCode = () => {
+    setImgCode({
+      value: '',
+      isInvalid: false,
+      errorMsg: '',
+    });
+  };
+  const resetCallback = () => {
+    refCallback.current = undefined;
+  };
+
+  const show = () => {
+    if (!stateShow) {
+      setStateShow(true);
+    }
+  };
+  /**
+   * There are some cases where the React scheduler cancels the execution of some functions,
+   * which prevents them from closing properly:
+   *  for example, if the parent component uninstalls the child component directly,
+   *  and the `captchaModal.close()` call is inside the child component.
+   * In this case, call `await captchaModal.close()` and wait for the close action to complete.
+   */
+  const close = () => {
+    setStateShow(false);
+    resetCapture();
+    resetImgCode();
+    resetCallback();
+
+    const p = new Promise<void>((resolve) => {
+      setTimeout(resolve, 50);
+    });
+    return p;
+  };
+
+  const handleCaptchaError = (fel: FieldError[] = []) => {
+    console.log('handleCaptchaError', fel);
+    const captchaErr = fel.find((o) => {
+      return o.error_field === 'captcha_code';
+    });
+
+    const ri = refImgCode.current;
+    if (captchaErr) {
+      /**
+       * `imgCode.value` No value but a validation error is received,
+       * indicating that it is the first time the interface has returned a CAPTCHA error,
+       * triggering the CAPTCHA logic. There is no need to display the error message at this point.
+       */
+      if (ri.value) {
+        setImgCode({
+          ...ri,
+          isInvalid: true,
+          errorMsg: captchaErr.error_msg,
+        });
+      }
+      fetchCaptchaData();
+      show();
+    } else {
+      close();
+    }
+    // Assist business logic in filtering CAPTCHA error messages when necessary
+    return captchaErr;
+  };
+
+  const handleChange = (token) => {
+    setImgCode({
+      value: token || '',
+      isInvalid: false,
+      errorMsg: '',
+    });
+  };
+
+  const getCaptcha = () => {
+    const rc = refCaptcha.current;
+    const ri = refImgCode.current;
+    const r = {
+      verify: !!rc?.verify,
+      captcha_id: rc?.captcha_id,
+      captcha_code: ri.value,
+    };
+
+    return r;
+  };
+
+  const resolveCaptchaReq = (req: ImgCodeReq) => {
+    const r = getCaptcha();
+    if (r.verify) {
+      req.captcha_code = r.captcha_code;
+      req.captcha_id = r.captcha_id;
+    }
+  };
+
+  const handleSubmit = (evt) => {
+    evt.preventDefault();
+    if (!imgCode.value) {
+      return;
+    }
+
+    if (refCallback.current) {
+      refCallback.current();
+    }
+  };
+
+  useEffect(() => {
+    if (autoInitCaptchaData) {
+      fetchCaptchaData();
+    }
+  }, []);
+
+  useEffect(() => {
+    if (stateShow) {
+      getCaptchaKey();
+      setTimeout(() => {
+        setIsLoading(false);
+      }, 800)
+    } else {
+      setTimeout(() => {
+        setIsLoading(true);
+      }, 100)
+    }
+  }, [stateShow]);
+
+  useLayoutEffect(() => {
+    refImgCode.current = imgCode;
+    refCaptcha.current = captcha;
+  }, [captcha, imgCode]);
+
+  useEffect(() => {
+    refRoot.current?.render(
+      <Modal
+        style={{ width: '336px', margin: '0 auto', right: 0 }}
+        title="Captcha"
+        show={stateShow}
+        onHide={() => close()}
+        centered>
+        <Modal.Header closeButton>
+          <Modal.Title as="h5">{t('title')}</Modal.Title>
+        </Modal.Header>
+        <Modal.Body>
+          <Form noValidate onSubmit={handleSubmit}>
+            <Form.Group controlId="code" className="mb-3">
+              <InputGroup>
+                {isLoading && !captchaKey && <div style={{ height: '78px' }} />}
+                <div className={isLoading ? 'w-100 text-center d-block' : 'w-100 text-center d-none'} style={{ position: 'absolute', top: 0, left: 0, height: '78px', lineHeight: '78px' }}>
+                  <Spinner animation="border" variant="secondary" />
+                </div>
+                {googleKey && (
+                  <ReCAPTCHA
+                    sitekey={googleKey}
+                    theme="light"
+                    size="normal"
+                    className={isLoading ? 'invisible' : 'visible'}
+                    hl={languageKeys[i18n.language] || 'en'}
+                    onChange={(token) =>handleChange(token)}
+                    onErrored={() => resetImgCode()}
+                    onExpired={() => resetImgCode()}
+                  />
+                )}
+                <Form.Control
+                  type="text"
+                  autoComplete="off"
+                  className="d-none"
+                  isInvalid={imgCode?.isInvalid}
+                />
+                <Form.Control.Feedback type="invalid">
+                  {imgCode?.errorMsg}
+                </Form.Control.Feedback>
+              </InputGroup>
+            </Form.Group>
+
+            <div className="d-grid">
+              <Button type="submit" disabled={!imgCode.value}>
+                {t('verify')}
+              </Button>
+            </div>
+          </Form>
+        </Modal.Body>
+      </Modal>,
+    );
+  });
+
+  const r = {
+    close,
+    show,
+    check: (submitFunc: SubmitCallback) => {
+      if (pending.current) {
+        return false;
+      }
+      refCallback.current = submitFunc;
+      if (captcha?.verify) {
+        show();
+        return false;
+      }
+      return submitFunc();
+    },
+    getCaptcha,
+    resolveCaptchaReq,
+    fetchCaptchaData,
+    handleCaptchaError,
+  };
+
+  return r;
+};
+
+export default Index;
diff --git a/captcha-google-v2/vite.config.ts b/captcha-google-v2/vite.config.ts
new file mode 100644
index 0000000..f96ea57
--- /dev/null
+++ b/captcha-google-v2/vite.config.ts
@@ -0,0 +1,35 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react-swc';
+import packageJson from './package.json';
+
+// https://vitejs.dev/config/
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  plugins: [react()],
+  build: {
+    lib: {
+      entry: 'index.ts',
+      name: packageJson.name,
+      fileName: (format) => `${packageJson.name}.${format}.js`,
+    },
+    rollupOptions: {
+      external: [
+        'react',
+        'react-dom',
+        'react-i18next',
+        'react-bootstrap',
+        'react-google-recaptcha',
+      ],
+      output: {
+        globals: {
+          react: 'React',
+          'react-dom': 'ReactDOM',
+          'react-i18next': 'reactI18next',
+          'react-bootstrap': 'reactBootstrap',
+          'react-google-recaptcha': 'reactGoogleRecaptcha'
+        },
+      },
+    },
+  },
+});