Skip to content

Commit 2495de4

Browse files
committed
feat(captcha): add captcha helper feature
- Auto normalize captcha input (lowercase, remove diacritics, keep alphanumeric) - Auto-submit on Enter or blur when input has 5 characters - Debounce 150ms for Vietnamese IME compatibility - Support SSO login and Register pages - Extensible architecture with CaptchaPageHandler Other changes: - Add text-utils with removeDiacritics, keepAlphanumeric utilities - Add docs/pages/captcha.md for DOM selectors - Update haui-structure.md with Authentication section
1 parent 86ce5a5 commit 2495de4

10 files changed

Lines changed: 378 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,31 @@
22

33
Tất cả thay đổi đáng chú ý của dự án sẽ được ghi lại tại đây.
44

5+
## [1.2.0] - 2026-01-15
6+
7+
### Added
8+
- **Captcha Helper Feature**: Hỗ trợ nhập captcha tự động
9+
- Tự động chuyển chữ hoa → thường
10+
- Loại bỏ dấu tiếng Việt (á → a, đ → d)
11+
- Chỉ giữ ký tự a-z, 0-9
12+
- Auto-submit khi nhấn Enter hoặc blur (đủ 5 ký tự)
13+
- Debounce 150ms để tránh xung đột với IME tiếng Việt
14+
- Hỗ trợ 2 trang: SSO login (`/sso?token=`) và Register (`/register/`)
15+
- Kiến trúc mở rộng (`CaptchaPageHandler`) cho các trang khác
16+
17+
### Added (Utilities)
18+
- `text-utils.ts`: Thêm các hàm xử lý text
19+
- `removeDiacritics()`: Loại bỏ dấu tiếng Việt
20+
- `diacriticsToTelex()`: Chuyển dấu thành phím Telex
21+
- `keepAlphanumeric()`: Chỉ giữ a-z, 0-9
22+
- `normalizeCaptchaInput()`: Normalize cho captcha
23+
24+
### Documentation
25+
- Thêm `docs/pages/captcha.md`: DOM selectors cho các trang captcha
26+
- Cập nhật `docs/pages/haui-structure.md`: Thêm section Authentication
27+
28+
---
29+
530
## [1.1.0] - 2026-01-15
631

732
### Added

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ Dự án được xây dựng với kiến trúc module hóa, dễ dàng mở r
4040
### Tính năng hiện có
4141

4242
| Tính năng | Mô tả | Trạng thái |
43-
|-----------|-------|------------|
43+
|-----------|-------|-----------:|
44+
| 🏷️ Dynamic Title | Thay đổi tiêu đề tab theo trang ||
45+
| 🔐 Captcha Helper | Hỗ trợ nhập captcha (lowercase, bỏ dấu, auto-submit) ||
4446

4547
## 🚀 Cài đặt
4648

@@ -155,13 +157,13 @@ svHaUI-Helper/
155157
│ │ ├── settings.ts # Quản lý cài đặt
156158
│ │ └── index.ts
157159
│ │
158-
│ ├── features/ # Các tính năng
159-
│ │ ├── example/ # Feature mẫu
160-
│ │ │ └── index.ts
160+
│ │ ├── dynamic-title/ # Tiêu đề động
161+
│ │ ├── captcha-helper/ # Hỗ trợ captcha
161162
│ │ └── index.ts # Registry
162163
│ │
163164
│ └── utils/ # Tiện ích
164165
│ ├── dom.ts # DOM helpers
166+
│ ├── text-utils.ts # Text processing
165167
│ └── index.ts
166168
167169
├── dist/ # Build output

docs/pages/captcha.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Captcha Pages - DOM Structure Reference
2+
3+
> Captcha xuất hiện trên nhiều trang khác nhau trong hệ thống HaUI.
4+
5+
---
6+
7+
## 1. SSO Login Page
8+
9+
**URL:** `/sso?token=*`
10+
11+
| Element | Selector |
12+
|---------|----------|
13+
| Captcha Image | `#ctl00_Image1` |
14+
| Text Input | `#ctl00_txtimgcode` |
15+
| Submit Button | `#ctl00_butLogin` |
16+
17+
---
18+
19+
## 2. Register Page (Đăng ký học phần)
20+
21+
**URL:** `/register/`
22+
23+
| Element | Selector |
24+
|---------|----------|
25+
| Captcha Image | `#ctl02_Image1` |
26+
| Text Input | `#ctl02_txtimgcode` |
27+
| Submit Button | `#ctl02_btnSubmit` |
28+
29+
---
30+
31+
## Captcha Characteristics
32+
33+
- **Chỉ chữ cái thường (a-z)** - không có chữ in hoa
34+
- **Có thể có số (0-9)**
35+
- **Không có dấu** - chỉ ký tự ASCII cơ bản
36+
- **Tổng 5 ký tự**
37+
38+
---
39+
40+
## Integration Notes
41+
42+
- Captcha cần được nhập chính xác
43+
- Auto-submit khi nhấn Enter hoặc blur (out focus)
44+
- Trang này sử dụng ASP.NET Web Forms postback

docs/pages/haui-structure.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@ body > .be-wrapper
3737

3838
---
3939

40+
## Authentication
41+
42+
> 📄 Chi tiết: [captcha.md](captcha.md)
43+
44+
| URL Pattern | Ghi chú |
45+
|-------------|---------|
46+
| `/sso?token=*` | Captcha page - xác thực SSO |
47+
48+
---
49+
4050
## Page Routes
4151

4252
### Trang chủ

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "svhaui-helper",
33
"private": true,
4-
"version": "1.1.1",
4+
"version": "1.2.0",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

src/core/logger.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export function getGlobalLogLevel(): LogLevel {
4747
}
4848

4949
// No-op function khi log bị disabled
50-
const noop = () => {};
50+
const noop = () => { };
5151

5252
export class Logger {
5353
private prefix: string;
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
/**
2+
* Captcha Helper Feature
3+
* Hỗ trợ nhập captcha: normalize input, auto-submit on blur/Enter
4+
*/
5+
6+
import { Feature } from '../../core';
7+
import { normalizeCaptchaInput } from '../../utils';
8+
9+
// ============================================
10+
// Types & Interfaces (extensibility)
11+
// ============================================
12+
13+
/**
14+
* Interface cho captcha handler - dễ mở rộng cho trang khác
15+
*/
16+
interface CaptchaPageHandler {
17+
/** URL pattern để match */
18+
urlPattern: RegExp;
19+
/** Selector cho input field */
20+
inputSelector: string;
21+
/** Selector cho submit button */
22+
submitSelector: string;
23+
/** Optional: selector cho captcha image (for future auto-solve) */
24+
imageSelector?: string;
25+
}
26+
27+
// ============================================
28+
// Page Handlers
29+
// ============================================
30+
31+
const CAPTCHA_HANDLERS: CaptchaPageHandler[] = [
32+
// SSO Login page
33+
{
34+
urlPattern: /\/sso\?token=/,
35+
inputSelector: '#ctl00_txtimgcode',
36+
submitSelector: '#ctl00_butLogin',
37+
imageSelector: '#ctl00_Image1',
38+
},
39+
// Register page (đăng ký học phần)
40+
{
41+
urlPattern: /\/register\//,
42+
inputSelector: '#ctl02_txtimgcode',
43+
submitSelector: '#ctl02_btnSubmit',
44+
imageSelector: '#ctl02_Image1',
45+
},
46+
];
47+
48+
// ============================================
49+
// CaptchaHelper Feature
50+
// ============================================
51+
52+
export class CaptchaHelperFeature extends Feature {
53+
private inputEl: HTMLInputElement | null = null;
54+
private submitEl: HTMLElement | null = null;
55+
private currentHandler: CaptchaPageHandler | null = null;
56+
57+
// Debounce timer
58+
private normalizeTimer: ReturnType<typeof setTimeout> | null = null;
59+
private readonly DEBOUNCE_DELAY = 150; // ms
60+
61+
// Event listener references for cleanup
62+
private handleInput = this.onInput.bind(this);
63+
private handleKeyDown = this.onKeyDown.bind(this);
64+
private handleBlur = this.onBlur.bind(this);
65+
66+
constructor() {
67+
super({
68+
id: 'captcha-helper',
69+
name: 'Captcha Helper',
70+
description: 'Hỗ trợ nhập captcha: tự động chuyển chữ thường, loại bỏ dấu, submit khi Enter/blur',
71+
});
72+
}
73+
74+
/**
75+
* Override shouldRun để chỉ chạy trên các trang có captcha
76+
*/
77+
override shouldRun(): boolean {
78+
if (!super.shouldRun()) return false;
79+
80+
const url = window.location.pathname + window.location.search;
81+
return CAPTCHA_HANDLERS.some(h => h.urlPattern.test(url));
82+
}
83+
84+
init(): void {
85+
this.log.i('Initializing...');
86+
87+
// Tìm handler phù hợp với URL hiện tại
88+
const url = window.location.pathname + window.location.search;
89+
this.currentHandler = CAPTCHA_HANDLERS.find(h => h.urlPattern.test(url)) || null;
90+
91+
if (!this.currentHandler) {
92+
this.log.w('No matching captcha handler found');
93+
return;
94+
}
95+
96+
// Tìm elements
97+
this.inputEl = document.querySelector<HTMLInputElement>(this.currentHandler.inputSelector);
98+
this.submitEl = document.querySelector<HTMLElement>(this.currentHandler.submitSelector);
99+
100+
if (!this.inputEl) {
101+
this.log.w('Captcha input not found:', this.currentHandler.inputSelector);
102+
return;
103+
}
104+
105+
if (!this.submitEl) {
106+
this.log.w('Submit button not found:', this.currentHandler.submitSelector);
107+
}
108+
109+
// Attach event listeners
110+
this.inputEl.addEventListener('input', this.handleInput);
111+
this.inputEl.addEventListener('keydown', this.handleKeyDown);
112+
this.inputEl.addEventListener('blur', this.handleBlur);
113+
114+
// Focus input để người dùng có thể gõ ngay
115+
this.inputEl.focus();
116+
117+
this.log.i('Ready! Input:', this.currentHandler.inputSelector);
118+
}
119+
120+
/**
121+
* Xử lý input: debounce normalize
122+
*/
123+
private onInput(): void {
124+
// Clear timer cũ
125+
if (this.normalizeTimer) {
126+
clearTimeout(this.normalizeTimer);
127+
}
128+
129+
// Set timer mới - normalize sau DEBOUNCE_DELAY ms
130+
this.normalizeTimer = setTimeout(() => {
131+
this.normalizeInput();
132+
}, this.DEBOUNCE_DELAY);
133+
}
134+
135+
/**
136+
* Normalize input value
137+
*/
138+
private normalizeInput(): void {
139+
if (!this.inputEl) return;
140+
141+
// Clear timer nếu có
142+
if (this.normalizeTimer) {
143+
clearTimeout(this.normalizeTimer);
144+
this.normalizeTimer = null;
145+
}
146+
147+
const original = this.inputEl.value;
148+
const normalized = normalizeCaptchaInput(original);
149+
150+
if (original !== normalized) {
151+
this.inputEl.value = normalized;
152+
// Đặt cursor ở cuối
153+
this.inputEl.setSelectionRange(normalized.length, normalized.length);
154+
this.log.d(`Normalized: "${original}" → "${normalized}"`);
155+
}
156+
}
157+
158+
/**
159+
* Xử lý keydown: submit khi Enter
160+
*/
161+
private onKeyDown(e: KeyboardEvent): void {
162+
if (e.key === 'Enter') {
163+
e.preventDefault();
164+
this.normalizeInput();
165+
this.submit();
166+
}
167+
}
168+
169+
/**
170+
* Xử lý blur: normalize và submit khi out focus
171+
*/
172+
private onBlur(): void {
173+
this.normalizeInput();
174+
175+
// Chỉ submit nếu có giá trị
176+
if (this.inputEl?.value.trim()) {
177+
this.submit();
178+
}
179+
}
180+
181+
/**
182+
* Submit form
183+
*/
184+
private submit(): void {
185+
const value = this.inputEl?.value.trim() || '';
186+
const CAPTCHA_LENGTH = 5;
187+
188+
if (value.length < CAPTCHA_LENGTH) {
189+
this.log.d(`Need ${CAPTCHA_LENGTH} chars, got ${value.length}`);
190+
return;
191+
}
192+
193+
if (this.submitEl) {
194+
this.log.i('Submitting...');
195+
this.submitEl.click();
196+
}
197+
}
198+
199+
destroy(): void {
200+
// Clear timer
201+
if (this.normalizeTimer) {
202+
clearTimeout(this.normalizeTimer);
203+
this.normalizeTimer = null;
204+
}
205+
206+
// Cleanup event listeners
207+
if (this.inputEl) {
208+
this.inputEl.removeEventListener('input', this.handleInput);
209+
this.inputEl.removeEventListener('keydown', this.handleKeyDown);
210+
this.inputEl.removeEventListener('blur', this.handleBlur);
211+
}
212+
213+
this.inputEl = null;
214+
this.submitEl = null;
215+
this.currentHandler = null;
216+
}
217+
}
218+

src/features/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55

66
import { Feature } from '../core';
77
import { DynamicTitleFeature } from './dynamic-title';
8+
import { CaptchaHelperFeature } from './captcha-helper';
89

910
// Thêm feature mới vào đây
1011
export const allFeatures: Feature[] = [
1112
new DynamicTitleFeature(),
13+
new CaptchaHelperFeature(),
1214
// new HomepageShortcuts(),
1315
// new NotificationPopup(),
1416
// new GradeCalculator(),

src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
*/
44

55
export * from './dom';
6+
export * from './text-utils';
67

78
export { log, createLogger } from '../core/logger';

0 commit comments

Comments
 (0)