Skip to content

Commit 76297f8

Browse files
authored
feat(Captcha): add AV.Captcha (#472)
* feat(Captcha): add AV.Captcha * feat(Captcha): ttl and size requires masterKey * fix(Captcha): Cloud.requestCaptcha returns AV.Captcha * fix(Captcha): assign AV.Captcha before AV.Cloud
1 parent 53a7c8c commit 76297f8

File tree

9 files changed

+217
-35
lines changed

9 files changed

+217
-35
lines changed

demo/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ <h1><a href="https://leancloud.cn">LeanCloud</a></h1>
1010
<h2><a href="https://leancloud.cn">为开发加速</a></h2>
1111
<div class="tips">
1212
<p>欢迎调试 JavaScript SDK,请打开浏览器控制台</p>
13-
<img id='captcha' onclick="refreshCaptcha()"/>
13+
<img id='captcha'/>
1414
<input type="text" id='code'/>
15-
<button onclick="verify()">verify</button>
15+
<button id="verify">verify</button>
1616
</div>
1717
<script src="../dist/av.js"></script>
1818
<script src="./test.js"></script>

demo/test.js

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,13 @@ var region = 'cn';
1818

1919
av.init({ appId: appId, appKey: appKey, region: region });
2020

21-
let captchaToken;
22-
const captchaImage = document.getElementById('captcha');
23-
const captchaInput = document.getElementById('code');
24-
25-
function refreshCaptcha(){
26-
AV.Cloud.requestCaptcha({
27-
size: 6,
28-
ttl: 30,
29-
}).then(function(data) {
30-
captchaToken = data.captchaToken;
31-
captchaImage.src = data.url;
32-
}).catch(console.error);
33-
}
34-
refreshCaptcha();
35-
36-
function verify() {
37-
AV.Cloud.verifyCaptcha(captchaInput.value, captchaToken).then(function(validateCode) {
38-
console.log('validateCode: ' + validateCode);
39-
}, console.error);
40-
}
21+
av.Captcha.request().then(captcha => {
22+
captcha.bind({
23+
textInput: 'code',
24+
image: 'captcha',
25+
verifyButton: 'verify',
26+
}, {
27+
success: validateCode => console.log('validateCode: ' + validateCode),
28+
error: console.error,
29+
});
30+
});

src/captcha.js

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
const { tap } = require('./utils');
2+
3+
module.exports = (AV) => {
4+
/**
5+
* @class
6+
* @example
7+
* AV.Captcha.request().then(captcha => {
8+
* captcha.bind({
9+
* textInput: 'code', // the id for textInput
10+
* image: 'captcha',
11+
* verifyButton: 'verify',
12+
* }, {
13+
* success: (validateCode) => {}, // next step
14+
* error: (error) => {}, // present error.message to user
15+
* });
16+
* });
17+
*/
18+
AV.Captcha = function Captcha(options, authOptions) {
19+
this._options = options;
20+
this._authOptions = authOptions;
21+
/**
22+
* The image url of the captcha
23+
* @type string
24+
*/
25+
this.url = undefined;
26+
/**
27+
* The captchaToken of the captcha.
28+
* @type string
29+
*/
30+
this.captchaToken = undefined;
31+
/**
32+
* The validateToken of the captcha.
33+
* @type string
34+
*/
35+
this.validateToken = undefined;
36+
};
37+
38+
/**
39+
* Refresh the captcha
40+
* @return {Promise.<string>} a new capcha url
41+
*/
42+
AV.Captcha.prototype.refresh = function refresh() {
43+
return AV.Cloud.requestCaptcha(this._options, this._authOptions).then(({
44+
captchaToken, url,
45+
}) => {
46+
Object.assign(this, { captchaToken, url });
47+
return url;
48+
});
49+
};
50+
51+
/**
52+
* Verify the captcha
53+
* @param {String} code The code from user input
54+
* @return {Promise.<string>} validateToken if the code is valid
55+
*/
56+
AV.Captcha.prototype.verify = function verify(code) {
57+
return AV.Cloud.verifyCaptcha(code, this.captchaToken)
58+
.then(tap(validateToken => (this.validateToken = validateToken)));
59+
};
60+
61+
if (process.env.CLIENT_PLATFORM === 'Browser') {
62+
/**
63+
* Bind the captcha to HTMLElements. <b>ONLY AVAILABLE in browsers</b>.
64+
* @param [elements]
65+
* @param {String|HTMLInputElement} [elements.textInput] An input element typed text, or the id for the element.
66+
* @param {String|HTMLImageElement} [elements.image] An image element, or the id for the element.
67+
* @param {String|HTMLElement} [elements.verifyButton] A button element, or the id for the element.
68+
* @param [callbacks]
69+
* @param {Function} [callbacks.success] Success callback will be called if the code is verified. The param `validateCode` can be used for further SMS request.
70+
* @param {Function} [callbacks.error] Error callback will be called if something goes wrong, detailed in param `error.message`.
71+
*/
72+
AV.Captcha.prototype.bind = function bind({
73+
textInput,
74+
image,
75+
verifyButton,
76+
}, {
77+
success,
78+
error,
79+
}) {
80+
if (typeof textInput === 'string') {
81+
textInput = document.getElementById(textInput);
82+
if (!textInput) throw new Error(`textInput with id ${textInput} not found`);
83+
}
84+
if (typeof image === 'string') {
85+
image = document.getElementById(image);
86+
if (!image) throw new Error(`image with id ${image} not found`);
87+
}
88+
if (typeof verifyButton === 'string') {
89+
verifyButton = document.getElementById(verifyButton);
90+
if (!verifyButton) throw new Error(`verifyButton with id ${verifyButton} not found`);
91+
}
92+
93+
this.__refresh = () => this.refresh().then(url => {
94+
image.src = url;
95+
if (textInput) {
96+
textInput.value = '';
97+
textInput.focus();
98+
}
99+
}).catch(err => console.warn(`refresh captcha fail: ${err.message}`));
100+
if (image) {
101+
this.__image = image;
102+
image.src = this.url;
103+
image.addEventListener('click', this.__refresh);
104+
}
105+
106+
this.__verify = () => {
107+
const code = textInput.value;
108+
this.verify(code).catch(err => {
109+
this.__refresh();
110+
throw err;
111+
}).then(success, error).catch(err => console.warn(`verify captcha fail: ${err.message}`));
112+
};
113+
if (textInput && verifyButton) {
114+
this.__verifyButton = verifyButton;
115+
verifyButton.addEventListener('click', this.__verify);
116+
}
117+
};
118+
119+
/**
120+
* unbind the captcha from HTMLElements. <b>ONLY AVAILABLE in browsers</b>.
121+
*/
122+
AV.Captcha.prototype.unbind = function unbind() {
123+
if (this.__image) this.__image.removeEventListener('click', this.__refresh);
124+
if (this.__verifyButton) this.__verifyButton.removeEventListener('click', this.__verify);
125+
};
126+
}
127+
128+
129+
/**
130+
* Request a captcha
131+
* @param [options]
132+
* @param {Number} [options.width] width(px) of the captcha, ranged 60-200
133+
* @param {Number} [options.height] height(px) of the captcha, ranged 30-100
134+
* @param {Number} [options.size=4] length of the captcha, ranged 3-6. MasterKey required.
135+
* @param {Number} [options.ttl=60] time to live(s), ranged 10-180. MasterKey required.
136+
* @return {Promise.<AV.Captcha>}
137+
*/
138+
AV.Captcha.request = (options, authOptions) => {
139+
const captcha = new AV.Captcha(options, authOptions);
140+
return captcha.refresh().then(() => captcha);
141+
};
142+
};

src/cloudfunction.js

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ module.exports = function(AV) {
99
* </em></strong></p>
1010
*
1111
* @namespace
12+
* @borrows AV.Captcha.request as requestCaptcha
1213
*/
1314
AV.Cloud = AV.Cloud || {};
1415

@@ -110,17 +111,8 @@ module.exports = function(AV) {
110111
return request;
111112
},
112113

113-
/**
114-
* request a captcha
115-
* @param {Object} [options]
116-
* @param {Number} [options.size=4] length of the captcha, ranged 3-6
117-
* @param {Number} [options.width] width(px) of the captcha, ranged 60-200
118-
* @param {Number} [options.height] height(px) of the captcha, ranged 30-100
119-
* @param {Number} [options.ttl=60] time to live(s), ranged 10-180
120-
* @return {Promise} { captchaToken, url }
121-
*/
122-
requestCaptcha(options) {
123-
return AVRequest('requestCaptcha', null, null, 'GET', options).then(({
114+
_requestCaptcha(options, authOptions) {
115+
return AVRequest('requestCaptcha', null, null, 'GET', options, authOptions).then(({
124116
captcha_url: url,
125117
captcha_token: captchaToken,
126118
}) => ({
@@ -130,7 +122,13 @@ module.exports = function(AV) {
130122
},
131123

132124
/**
133-
* verify captcha code
125+
* Request a captcha.
126+
*/
127+
requestCaptcha: AV.Captcha.request,
128+
129+
/**
130+
* Verify captcha code. This is the low-level API for captcha.
131+
* Checkout {@link AV.Captcha} for high abstract APIs.
134132
* @param {String} code the code from user input
135133
* @param {String} captchaToken captchaToken returned by {@link AV.Cloud.requestCaptcha}
136134
* @return {Promise.<String>} validateToken if the code is valid

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ require('./object')(AV);
2626
require('./role')(AV);
2727
require('./user')(AV);
2828
require('./query')(AV);
29+
require('./captcha')(AV);
2930
require('./cloudfunction')(AV);
3031
require('./push')(AV);
3132
require('./status')(AV);

storage.d.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ declare namespace AV {
2727
validateToken?: string;
2828
}
2929

30+
interface CaptchaOptions {
31+
size?: number;
32+
width?: number;
33+
height?: number;
34+
ttl?: number;
35+
}
36+
3037
export interface WaitOption {
3138
/**
3239
* Set to true to wait for the server to confirm success
@@ -546,6 +553,27 @@ declare namespace AV {
546553
refreshSessionToken(options?: AuthOptions): Promise<User>;
547554

548555
getRoles(options?: AuthOptions): Promise<Role>;
556+
557+
}
558+
559+
export class Captcha {
560+
url: string;
561+
captchaToken: string;
562+
validateToken: string;
563+
564+
static request(options?: CaptchaOptions, authOptions?: AuthOptions): Promise<Captcha>;
565+
566+
refresh(): Promise<string>;
567+
verify(code: string): Promise<string>;
568+
bind(elements?: {
569+
textInput?: string|HTMLInputElement,
570+
image?: string|HTMLImageElement,
571+
verifyButton?: string|HTMLElement,
572+
}, callbacks?: {
573+
success?: (validateToken: string) => any,
574+
error?: (error: Error) => any,
575+
}): void;
576+
unbind(): void;
549577
}
550578

551579
/**
@@ -710,7 +738,7 @@ declare namespace AV {
710738
function run(name: string, data?: any, options?: AuthOptions): Promise<any>;
711739
function requestSmsCode(data: string|{ mobilePhoneNumber: string, template?: string, sign?: string }, options?: SMSAuthOptions): Promise<void>;
712740
function verifySmsCode(code: string, phone: string): Promise<void>;
713-
function requestCaptcha(options?: { size?: number, width?: number, height?: number, ttl?: number}): Promise<{ captchaToken: string, dataURI: string }>;
741+
function requestCaptcha(options?: CaptchaOptions, authOptions?: AuthOptions): Promise<AV.Captcha>;
714742
function verifyCaptcha(code: string, captchaToken: string): Promise<void>;
715743
}
716744

test/captcha.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
describe('Captcha', () => {
2+
before(function () {
3+
return AV.Captcha.request().then(captcha => {
4+
this.captcha = captcha;
5+
});
6+
});
7+
it('.request', function () {
8+
this.captcha.should.be.instanceof(AV.Captcha);
9+
this.captcha.url.should.be.a.String();
10+
this.captcha.captchaToken.should.be.a.String();
11+
});
12+
it('.refresh', function () {
13+
const currentUrl = this.captcha.url;
14+
return this.captcha.refresh().then(() => {
15+
this.captcha.url.should.not.equalTo(currentUrl);
16+
});
17+
});
18+
it('.refresh', function () {
19+
return this.captcha.verify('fakecode').should.be.rejected();
20+
});
21+
});

test/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ require('./test.js');
33
require('./av.js');
44
require('./file.js');
55
require('./error.js');
6+
// require('./captcha.js');
67
require('./object.js');
78
require('./user.js');
89
require('./query.js');

test/test.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
<script src="cache.js"></script>
2828
<script src="file.js"></script>
2929
<script src="error.js"></script>
30+
<!--<script src="captcha.js"></script>-->
3031
<script src="object.js"></script>
3132
<script src="user.js"></script>
3233
<script src="query.js"></script>

0 commit comments

Comments
 (0)