ethiopian-captcha 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +23 -23
- package/src/assets/metadatas/metaData.js +32 -0
- package/src/constants/amharicLetters.js +181 -33
- package/src/constants/common.js +1 -0
- package/src/constants/imageAPI.js +1 -0
- package/src/core/captchaEngine.js +63 -43
- package/src/react/ClickImage.jsx +166 -0
- package/src/utils/getRandomLetters.js +21 -0
package/package.json
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "ethiopian-captcha",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"type": "module",
|
|
5
|
-
"main": "src/index.js",
|
|
6
|
-
"exports": {
|
|
7
|
-
".": "./src/index.js",
|
|
8
|
-
"./react": "./src/react/EtCaptcha.jsx"
|
|
9
|
-
},
|
|
10
|
-
"keywords": [
|
|
11
|
-
"captcha",
|
|
12
|
-
"ethiopia",
|
|
13
|
-
"amharic",
|
|
14
|
-
"fidel",
|
|
15
|
-
"security"
|
|
16
|
-
],
|
|
17
|
-
"author": "Amenadam Solomon",
|
|
18
|
-
"license": "MIT",
|
|
19
|
-
"repository": {
|
|
20
|
-
"type": "git",
|
|
21
|
-
"url": "https://github.com/amenadam/ethiopian-captcha"
|
|
22
|
-
}
|
|
23
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "ethiopian-captcha",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.js",
|
|
8
|
+
"./react": "./src/react/EtCaptcha.jsx"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"captcha",
|
|
12
|
+
"ethiopia",
|
|
13
|
+
"amharic",
|
|
14
|
+
"fidel",
|
|
15
|
+
"security"
|
|
16
|
+
],
|
|
17
|
+
"author": "Amenadam Solomon",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/amenadam/ethiopian-captcha"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const IMAGE_CAPTCHA_METADATA = [
|
|
2
|
+
{
|
|
3
|
+
id: "CLICK_IMG_001",
|
|
4
|
+
question: "Jebena/ጀበና",
|
|
5
|
+
image: "CAPTCHA_ETHIOPIAN_ITEMS.png",
|
|
6
|
+
answer: [0, 3],
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
id: "CLICK_IMG_002",
|
|
10
|
+
question: "Mesob/መሶብ",
|
|
11
|
+
image: "CAPTCHA_ETHIOPIAN_ITEMS.png",
|
|
12
|
+
answer: [2, 4, 5, 8],
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: "CLICK_IMG_003",
|
|
16
|
+
question: "Bandira/ባንዲራ",
|
|
17
|
+
image: "CAPTCHA_ETHIOPIAN_ITEMS.png",
|
|
18
|
+
answer: [1],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "CLICK_IMG_004",
|
|
22
|
+
question: "Buna/ቡና",
|
|
23
|
+
image: "CAPTCHA_ETHIOPIAN_ITEMS.png",
|
|
24
|
+
answer: [6],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "CLICK_IMG_005",
|
|
28
|
+
question: "Injera/እንጀራ",
|
|
29
|
+
image: "CAPTCHA_ETHIOPIAN_ITEMS.png",
|
|
30
|
+
answer: [4, 7],
|
|
31
|
+
},
|
|
32
|
+
];
|
|
@@ -1,35 +1,183 @@
|
|
|
1
1
|
export const AMHARIC_LETTERS = [
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
2
|
+
"ሀ",
|
|
3
|
+
"ሁ",
|
|
4
|
+
"ሂ",
|
|
5
|
+
"ሃ",
|
|
6
|
+
"ሄ",
|
|
7
|
+
"ህ",
|
|
8
|
+
"ሆ",
|
|
9
|
+
"ለ",
|
|
10
|
+
"ሉ",
|
|
11
|
+
"ሊ",
|
|
12
|
+
"ላ",
|
|
13
|
+
"ሌ",
|
|
14
|
+
"ል",
|
|
15
|
+
"ሎ",
|
|
16
|
+
"ሐ",
|
|
17
|
+
"ሑ",
|
|
18
|
+
"ሒ",
|
|
19
|
+
"ሓ",
|
|
20
|
+
"ሔ",
|
|
21
|
+
"ሕ",
|
|
22
|
+
"ሖ",
|
|
23
|
+
"መ",
|
|
24
|
+
"ሙ",
|
|
25
|
+
"ሚ",
|
|
26
|
+
"ማ",
|
|
27
|
+
"ሜ",
|
|
28
|
+
"ም",
|
|
29
|
+
"ሞ",
|
|
30
|
+
|
|
31
|
+
"ረ",
|
|
32
|
+
"ሩ",
|
|
33
|
+
"ሪ",
|
|
34
|
+
"ራ",
|
|
35
|
+
"ሬ",
|
|
36
|
+
"ር",
|
|
37
|
+
"ሮ",
|
|
38
|
+
"ሰ",
|
|
39
|
+
"ሱ",
|
|
40
|
+
"ሲ",
|
|
41
|
+
"ሳ",
|
|
42
|
+
"ሴ",
|
|
43
|
+
"ስ",
|
|
44
|
+
"ሶ",
|
|
45
|
+
"ሸ",
|
|
46
|
+
"ሹ",
|
|
47
|
+
"ሺ",
|
|
48
|
+
"ሻ",
|
|
49
|
+
"ሼ",
|
|
50
|
+
"ሽ",
|
|
51
|
+
"ሾ",
|
|
52
|
+
"ቀ",
|
|
53
|
+
"ቁ",
|
|
54
|
+
"ቂ",
|
|
55
|
+
"ቃ",
|
|
56
|
+
"ቄ",
|
|
57
|
+
"ቅ",
|
|
58
|
+
"ቆ",
|
|
59
|
+
"በ",
|
|
60
|
+
"ቡ",
|
|
61
|
+
"ቢ",
|
|
62
|
+
"ባ",
|
|
63
|
+
"ቤ",
|
|
64
|
+
"ብ",
|
|
65
|
+
"ቦ",
|
|
66
|
+
"ተ",
|
|
67
|
+
"ቱ",
|
|
68
|
+
"ቲ",
|
|
69
|
+
"ታ",
|
|
70
|
+
"ቴ",
|
|
71
|
+
"ት",
|
|
72
|
+
"ቶ",
|
|
73
|
+
"ቸ",
|
|
74
|
+
"ቹ",
|
|
75
|
+
"ቺ",
|
|
76
|
+
"ቻ",
|
|
77
|
+
"ቼ",
|
|
78
|
+
"ች",
|
|
79
|
+
"ቾ",
|
|
80
|
+
|
|
81
|
+
"ነ",
|
|
82
|
+
"ኑ",
|
|
83
|
+
"ኒ",
|
|
84
|
+
"ና",
|
|
85
|
+
"ኔ",
|
|
86
|
+
"ን",
|
|
87
|
+
"ኖ",
|
|
88
|
+
"ኘ",
|
|
89
|
+
"ኙ",
|
|
90
|
+
"ኚ",
|
|
91
|
+
"ኛ",
|
|
92
|
+
"ኜ",
|
|
93
|
+
"ኝ",
|
|
94
|
+
"ኞ",
|
|
95
|
+
"አ",
|
|
96
|
+
"ኡ",
|
|
97
|
+
"ኢ",
|
|
98
|
+
"ኣ",
|
|
99
|
+
"ኤ",
|
|
100
|
+
"እ",
|
|
101
|
+
"ኦ",
|
|
102
|
+
"ከ",
|
|
103
|
+
"ኩ",
|
|
104
|
+
"ኪ",
|
|
105
|
+
"ካ",
|
|
106
|
+
"ኬ",
|
|
107
|
+
"ክ",
|
|
108
|
+
"ኮ",
|
|
109
|
+
|
|
110
|
+
"ወ",
|
|
111
|
+
"ዉ",
|
|
112
|
+
"ዊ",
|
|
113
|
+
"ዋ",
|
|
114
|
+
"ዌ",
|
|
115
|
+
"ው",
|
|
116
|
+
"ዎ",
|
|
117
|
+
|
|
118
|
+
"ዘ",
|
|
119
|
+
"ዙ",
|
|
120
|
+
"ዚ",
|
|
121
|
+
"ዛ",
|
|
122
|
+
"ዜ",
|
|
123
|
+
"ዝ",
|
|
124
|
+
"ዞ",
|
|
125
|
+
|
|
126
|
+
"የ",
|
|
127
|
+
"ዩ",
|
|
128
|
+
"ዪ",
|
|
129
|
+
"ያ",
|
|
130
|
+
"ዬ",
|
|
131
|
+
"ይ",
|
|
132
|
+
"ዮ",
|
|
133
|
+
"ደ",
|
|
134
|
+
"ዱ",
|
|
135
|
+
"ዲ",
|
|
136
|
+
"ዳ",
|
|
137
|
+
"ዴ",
|
|
138
|
+
"ድ",
|
|
139
|
+
"ዶ",
|
|
140
|
+
"ጀ",
|
|
141
|
+
"ጁ",
|
|
142
|
+
"ጂ",
|
|
143
|
+
"ጃ",
|
|
144
|
+
"ጄ",
|
|
145
|
+
"ጅ",
|
|
146
|
+
"ጆ",
|
|
147
|
+
"ገ",
|
|
148
|
+
"ጉ",
|
|
149
|
+
"ጊ",
|
|
150
|
+
"ጋ",
|
|
151
|
+
"ጌ",
|
|
152
|
+
"ግ",
|
|
153
|
+
"ጎ",
|
|
154
|
+
"ጠ",
|
|
155
|
+
"ጡ",
|
|
156
|
+
"ጢ",
|
|
157
|
+
"ጣ",
|
|
158
|
+
"ጤ",
|
|
159
|
+
"ጥ",
|
|
160
|
+
"ጦ",
|
|
161
|
+
"ጨ",
|
|
162
|
+
"ጩ",
|
|
163
|
+
"ጪ",
|
|
164
|
+
"ጫ",
|
|
165
|
+
"ጬ",
|
|
166
|
+
"ጭ",
|
|
167
|
+
"ጮ",
|
|
168
|
+
|
|
169
|
+
"ፈ",
|
|
170
|
+
"ፉ",
|
|
171
|
+
"ፊ",
|
|
172
|
+
"ፋ",
|
|
173
|
+
"ፌ",
|
|
174
|
+
"ፍ",
|
|
175
|
+
"ፎ",
|
|
176
|
+
"ፐ",
|
|
177
|
+
"ፑ",
|
|
178
|
+
"ፒ",
|
|
179
|
+
"ፓ",
|
|
180
|
+
"ፔ",
|
|
181
|
+
"ፕ",
|
|
182
|
+
"ፖ",
|
|
35
183
|
];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEBUGING_OPACITY = 0;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const IMAGES_API = "https://ethiopian-captcha.vercel.app/";
|
|
@@ -1,56 +1,64 @@
|
|
|
1
|
-
import { AMHARIC_LETTERS } from "../constants/amharicLetters.js";
|
|
2
1
|
import { hashAnswer } from "../utils/hash.js";
|
|
3
2
|
import { captchaConfig } from "../config/config.js";
|
|
3
|
+
import { getRandomLetters } from "../utils/getRandomLetters.js";
|
|
4
|
+
import { IMAGE_CAPTCHA_METADATA } from "../assets/metadatas/metaData.js";
|
|
5
|
+
import { IMAGES_API } from "../constants/imageAPI.js";
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
return AMHARIC_LETTERS[Math.floor(Math.random() * AMHARIC_LETTERS.length)];
|
|
7
|
-
};
|
|
7
|
+
let imageAPI = IMAGES_API;
|
|
8
8
|
|
|
9
|
-
const
|
|
10
|
-
let last = "";
|
|
11
|
-
let randomLetters = [];
|
|
12
|
-
for (let i = 0; i < count; i++) {
|
|
13
|
-
let letter;
|
|
14
|
-
do {
|
|
15
|
-
letter = getRandomLetter();
|
|
16
|
-
} while (letter === last);
|
|
17
|
-
|
|
18
|
-
last = letter;
|
|
19
|
-
randomLetters.push(letter);
|
|
20
|
-
}
|
|
9
|
+
const generatedCaptchas = new Map();
|
|
21
10
|
|
|
22
|
-
|
|
11
|
+
const getRandomImageClickCaptcha = () => {
|
|
12
|
+
let randomCaptcha =
|
|
13
|
+
IMAGE_CAPTCHA_METADATA[
|
|
14
|
+
Math.floor(Math.random() * IMAGE_CAPTCHA_METADATA.length)
|
|
15
|
+
];
|
|
16
|
+
return randomCaptcha;
|
|
23
17
|
};
|
|
24
18
|
|
|
25
|
-
const generatedCaptchas = new Map();
|
|
26
|
-
|
|
27
19
|
export const generateCaptcha = (
|
|
28
20
|
type = captchaConfig.type,
|
|
29
|
-
count = captchaConfig.count
|
|
21
|
+
count = captchaConfig.count,
|
|
30
22
|
) => {
|
|
31
|
-
if (type
|
|
32
|
-
|
|
23
|
+
if (type === "fidel") {
|
|
24
|
+
const captcha = getRandomLetters(count);
|
|
25
|
+
const captchaId = `ETH-fidel-${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
|
|
26
|
+
|
|
27
|
+
generatedCaptchas.set(captchaId, {
|
|
28
|
+
answerHash: hashAnswer(captcha.join("")),
|
|
29
|
+
generatedAt: Date.now(),
|
|
30
|
+
attempts: 0,
|
|
31
|
+
type: "fidel",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
success: true,
|
|
36
|
+
type,
|
|
37
|
+
captchaId,
|
|
38
|
+
captcha,
|
|
39
|
+
};
|
|
33
40
|
}
|
|
34
41
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
captchaId,
|
|
48
|
-
|
|
49
|
-
|
|
42
|
+
if (type === "click-image") {
|
|
43
|
+
let captcha = getRandomImageClickCaptcha();
|
|
44
|
+
const captchaId = `ETH-click-image-${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
|
|
45
|
+
let image = imageAPI + captcha.image;
|
|
46
|
+
generatedCaptchas.set(captchaId, {
|
|
47
|
+
captcha,
|
|
48
|
+
image,
|
|
49
|
+
|
|
50
|
+
generatedAt: Date.now(),
|
|
51
|
+
attempts: 0,
|
|
52
|
+
type: "click-image",
|
|
53
|
+
});
|
|
54
|
+
return { success: true, type, captchaId, captcha: captcha.question, image };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { success: false, message: "Unsupported captcha type" };
|
|
50
58
|
};
|
|
51
59
|
|
|
52
60
|
const CAPTCHA_TTL = captchaConfig.ttl;
|
|
53
|
-
const MAX_ATTEMPTS = captchaConfig.
|
|
61
|
+
const MAX_ATTEMPTS = captchaConfig.maxAttempts;
|
|
54
62
|
export const validateCaptcha = (captchaId, userInput) => {
|
|
55
63
|
const captcha = generatedCaptchas.get(captchaId);
|
|
56
64
|
|
|
@@ -64,15 +72,27 @@ export const validateCaptcha = (captchaId, userInput) => {
|
|
|
64
72
|
}
|
|
65
73
|
|
|
66
74
|
captcha.attempts++;
|
|
67
|
-
if (captcha.attempts
|
|
75
|
+
if (captcha.attempts >= MAX_ATTEMPTS) {
|
|
68
76
|
generatedCaptchas.delete(captchaId);
|
|
69
77
|
return { success: false, message: "Too many attempts" };
|
|
70
78
|
}
|
|
71
79
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
80
|
+
if (captcha.type === "fidel") {
|
|
81
|
+
const inputHash = hashAnswer(userInput.trim());
|
|
82
|
+
if (inputHash === captcha.answerHash) {
|
|
83
|
+
generatedCaptchas.delete(captchaId);
|
|
84
|
+
return { success: true, message: "captcha correctly matched" };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { success: false, message: "Incorrect captcha" };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (captcha.type === "click-image") {
|
|
91
|
+
if (captcha.captcha.answer.includes(userInput)) {
|
|
92
|
+
return { success: true, message: "Correct Choice" };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { success: false, message: "incorrect choice" };
|
|
76
96
|
}
|
|
77
97
|
|
|
78
98
|
return { success: false, message: "Incorrect captcha" };
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { DEBUGING_OPACITY } from "../constants/common";
|
|
3
|
+
|
|
4
|
+
export function ClickImage({
|
|
5
|
+
captcha = {},
|
|
6
|
+
captchaId,
|
|
7
|
+
onVerify,
|
|
8
|
+
onRefresh,
|
|
9
|
+
loading = false,
|
|
10
|
+
}) {
|
|
11
|
+
const [input, setInput] = useState(1000);
|
|
12
|
+
const [iconPosition, setIconPosition] = useState({ x: 0, y: 0 });
|
|
13
|
+
|
|
14
|
+
const handleClick = (e) => {
|
|
15
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
16
|
+
const x = e.clientX - rect.left - 6; // Relative to container
|
|
17
|
+
const y = e.clientY - rect.top - 6;
|
|
18
|
+
setIconPosition({ x, y });
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const handleVerify = () => {
|
|
22
|
+
onVerify({ captchaId, input });
|
|
23
|
+
setIconPosition({ x: 0, y: 0 });
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
style={{
|
|
29
|
+
border: "1px solid #ddd",
|
|
30
|
+
borderRadius: 8,
|
|
31
|
+
padding: 16,
|
|
32
|
+
width: 260,
|
|
33
|
+
fontFamily: "system-ui, sans-serif",
|
|
34
|
+
display: "flex",
|
|
35
|
+
flexDirection: "column",
|
|
36
|
+
alignItems: "center",
|
|
37
|
+
justifyContent: "center",
|
|
38
|
+
gap: "1rem",
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
<div
|
|
42
|
+
style={{
|
|
43
|
+
fontSize: 24,
|
|
44
|
+
textAlign: "center",
|
|
45
|
+
marginBottom: 12,
|
|
46
|
+
userSelect: "none",
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
Click on <span style={{ fontWeight: "bold" }}>{captcha.captcha}</span>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div
|
|
53
|
+
style={{
|
|
54
|
+
width: "15rem",
|
|
55
|
+
position: "relative",
|
|
56
|
+
}}
|
|
57
|
+
onClick={handleClick}
|
|
58
|
+
>
|
|
59
|
+
<div
|
|
60
|
+
style={{
|
|
61
|
+
position: "absolute",
|
|
62
|
+
background: "blue",
|
|
63
|
+
border: "1px solid white",
|
|
64
|
+
width: "15px",
|
|
65
|
+
height: "15px",
|
|
66
|
+
left: iconPosition.x,
|
|
67
|
+
top: iconPosition.y,
|
|
68
|
+
pointerEvents: "none",
|
|
69
|
+
display: iconPosition.x == 0 || iconPosition.y == 0 ? "none" : "",
|
|
70
|
+
borderRadius: "50%",
|
|
71
|
+
}}
|
|
72
|
+
></div>
|
|
73
|
+
<div
|
|
74
|
+
style={{
|
|
75
|
+
position: "absolute",
|
|
76
|
+
opacity: DEBUGING_OPACITY,
|
|
77
|
+
display: "grid",
|
|
78
|
+
gridTemplateColumns: "repeat(3, 1fr)",
|
|
79
|
+
gridTemplateRows: "repeat(3, 1fr)",
|
|
80
|
+
width: "100%",
|
|
81
|
+
height: "100%",
|
|
82
|
+
borderRadius: "0.5rem",
|
|
83
|
+
margin: "0 auto",
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
<div
|
|
87
|
+
onClick={() => setInput(0)}
|
|
88
|
+
style={{
|
|
89
|
+
background: `${input === 0 ? "#00000080" : ""}`,
|
|
90
|
+
border: "1px solid #000",
|
|
91
|
+
}}
|
|
92
|
+
>
|
|
93
|
+
0
|
|
94
|
+
</div>
|
|
95
|
+
<div
|
|
96
|
+
onClick={() => setInput(1)}
|
|
97
|
+
style={{
|
|
98
|
+
background: `${input === 1 ? "#00000080" : ""}`,
|
|
99
|
+
border: "1px solid #000",
|
|
100
|
+
}}
|
|
101
|
+
>
|
|
102
|
+
1
|
|
103
|
+
</div>
|
|
104
|
+
<div
|
|
105
|
+
onClick={() => setInput(2)}
|
|
106
|
+
style={{ background: "", border: "1px solid #000" }}
|
|
107
|
+
>
|
|
108
|
+
2
|
|
109
|
+
</div>
|
|
110
|
+
<div
|
|
111
|
+
onClick={() => setInput(3)}
|
|
112
|
+
style={{ background: "", border: "1px solid #000" }}
|
|
113
|
+
>
|
|
114
|
+
3
|
|
115
|
+
</div>
|
|
116
|
+
<div
|
|
117
|
+
onClick={() => setInput(4)}
|
|
118
|
+
style={{ background: "", border: "1px solid #000" }}
|
|
119
|
+
>
|
|
120
|
+
4
|
|
121
|
+
</div>
|
|
122
|
+
<div
|
|
123
|
+
onClick={() => setInput(5)}
|
|
124
|
+
style={{ background: "", border: "1px solid #000" }}
|
|
125
|
+
>
|
|
126
|
+
5
|
|
127
|
+
</div>
|
|
128
|
+
<div
|
|
129
|
+
onClick={() => setInput(6)}
|
|
130
|
+
style={{ background: "", border: "1px solid #000" }}
|
|
131
|
+
>
|
|
132
|
+
6
|
|
133
|
+
</div>
|
|
134
|
+
<div
|
|
135
|
+
onClick={() => setInput(7)}
|
|
136
|
+
style={{ background: "", border: "1px solid #000" }}
|
|
137
|
+
>
|
|
138
|
+
7
|
|
139
|
+
</div>
|
|
140
|
+
<div
|
|
141
|
+
onClick={() => setInput(8)}
|
|
142
|
+
style={{ background: "", border: "1px solid #000" }}
|
|
143
|
+
>
|
|
144
|
+
8
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<img
|
|
149
|
+
src={`${captcha.image}`}
|
|
150
|
+
style={{ width: "100%", borderRadius: "0.5rem" }}
|
|
151
|
+
alt="CAPTCHA"
|
|
152
|
+
/>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<div style={{ display: "flex", gap: 8 }}>
|
|
156
|
+
<button onClick={handleVerify} disabled={loading} style={{ flex: 1 }}>
|
|
157
|
+
Verify
|
|
158
|
+
</button>
|
|
159
|
+
|
|
160
|
+
<button onClick={onRefresh} disabled={loading}>
|
|
161
|
+
↻
|
|
162
|
+
</button>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { AMHARIC_LETTERS } from "../constants/amharicLetters.js";
|
|
2
|
+
|
|
3
|
+
const getRandomLetter = () => {
|
|
4
|
+
return AMHARIC_LETTERS[Math.floor(Math.random() * AMHARIC_LETTERS.length)];
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const getRandomLetters = (count = 4) => {
|
|
8
|
+
let last = "";
|
|
9
|
+
let randomLetters = [];
|
|
10
|
+
for (let i = 0; i < count; i++) {
|
|
11
|
+
let letter;
|
|
12
|
+
do {
|
|
13
|
+
letter = getRandomLetter();
|
|
14
|
+
} while (letter === last);
|
|
15
|
+
|
|
16
|
+
last = letter;
|
|
17
|
+
randomLetters.push(letter);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return randomLetters;
|
|
21
|
+
};
|