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 CHANGED
@@ -1,23 +1,23 @@
1
- {
2
- "name": "ethiopian-captcha",
3
- "version": "1.0.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.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
+ ];
@@ -122,13 +122,7 @@ export const AMHARIC_LETTERS = [
122
122
  "ዜ",
123
123
  "ዝ",
124
124
  "ዞ",
125
- "ዠ",
126
- "ዡ",
127
- "ዢ",
128
- "ዣ",
129
- "ዤ",
130
- "ዥ",
131
- "ዦ",
125
+
132
126
  "የ",
133
127
  "ዩ",
134
128
  "ዪ",
@@ -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
- const getRandomLetter = () => {
6
- return AMHARIC_LETTERS[Math.floor(Math.random() * AMHARIC_LETTERS.length)];
7
- };
7
+ let imageAPI = IMAGES_API;
8
8
 
9
- const getRandomLetters = (count = 4) => {
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
- return randomLetters;
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 !== "fidel") {
32
- return { success: false, message: "Unsupported captcha type" };
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
- const captcha = getRandomLetters(count);
36
- const captchaId = `ETH-${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
37
-
38
- generatedCaptchas.set(captchaId, {
39
- answerHash: hashAnswer(captcha.join("")),
40
- generatedAt: Date.now(),
41
- attempts: 0,
42
- });
43
-
44
- return {
45
- success: true,
46
- type,
47
- captchaId,
48
- captcha,
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 > MAX_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
- const inputHash = hashAnswer(userInput.trim());
73
- if (inputHash === captcha.answerHash) {
74
- generatedCaptchas.delete(captchaId);
75
- return { success: true };
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,2 @@
1
+ export { default as EtCaptcha } from "./EtCaptcha.jsx";
2
+ export { default as ClickImage } from "./ClickImage.jsx";
@@ -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
+ };