ethiopian-captcha 1.0.1 → 1.1.1
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 +1 -7
- package/src/constants/common.js +1 -0
- package/src/constants/imageAPI.js +1 -0
- package/src/core/captchaEngine.js +62 -42
- package/src/react/ClickImage.jsx +166 -0
- package/src/react/index.js +2 -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/
|
|
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.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.js",
|
|
8
|
+
"./react": "./src/react/index.js"
|
|
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
|
+
];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEBUGING_OPACITY = 0;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const IMAGES_API = "https://ethiopian-captcha.vercel.app/";
|
|
@@ -1,52 +1,60 @@
|
|
|
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;
|
|
@@ -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
|
+
};
|