cabloy 5.1.240 → 5.1.242
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/.cabloy-version +1 -1
- package/CHANGELOG.md +24 -0
- package/package.json +1 -1
- package/repo-docs/backend/i18n-guide.md +109 -11
- package/repo-docs/backend/openapi-guide.md +18 -0
- package/repo-docs/frontend/environment-config-guide.md +43 -0
- package/repo-docs/frontend/module-scope.md +88 -0
- package/repo-docs/frontend/system-startup-guide.md +31 -0
- package/repo-e2e/specs/home-user-account.spec.ts +53 -8
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/pnpm-lock.yaml +17 -142
- package/vona/src/suite/a-commerce/modules/commerce-siteadmin/src/dto/operatorContext.ts +4 -1
- package/vona/src/suite/a-commerce/modules/commerce-siteadmin/src/service/operator.ts +1 -0
- package/vona/src/suite/a-commerce/modules/commerce-siteadmin/test/operatorAccess.test.ts +1 -0
- package/vona/src/suite/a-home/modules/home-user/src/bean/meta.version.ts +1 -0
- package/vona/src/suite/a-home/modules/home-user/src/controller/passport.ts +1 -0
- package/vona/src/suite/a-home/modules/home-user/src/dto/accountCurrent.ts +3 -0
- package/vona/src/suite/a-home/modules/home-user/src/dto/accountProfileUpdate.ts +9 -2
- package/vona/src/suite/a-home/modules/home-user/src/dto/passportUser.ts +3 -0
- package/vona/src/suite/a-home/modules/home-user/src/entity/user.ts +3 -0
- package/vona/src/suite/a-home/modules/home-user/src/service/account.ts +7 -3
- package/vona/src/suite/a-home/modules/home-user/src/service/userAdapter.ts +1 -0
- package/vona/src/suite/a-home/modules/home-user/test/account.test.ts +133 -8
- package/vona/src/suite/a-home/modules/home-user/test/accountOpenapi.test.ts +32 -5
- package/vona/src/suite/a-home/modules/home-user/test/passport.test.ts +159 -1
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
- package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
- package/vona/src/suite-vendor/a-captcha/modules/a-captcha/package.json +1 -1
- package/vona/src/suite-vendor/a-captcha/modules/a-captcha/src/bean/bean.captcha.ts +271 -137
- package/vona/src/suite-vendor/a-captcha/modules/a-captcha/src/bean/cacheRedis.captcha.ts +199 -2
- package/vona/src/suite-vendor/a-captcha/modules/a-captcha/src/lib/captchaLua.ts +90 -0
- package/vona/src/suite-vendor/a-captcha/modules/a-captcha/src/types/captcha.ts +15 -1
- package/vona/src/suite-vendor/a-captcha/modules/a-captcha/src/types/captchaProvider.ts +38 -1
- package/vona/src/suite-vendor/a-captcha/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-permission/src/bean/bean.permission.ts +54 -8
- package/vona/src/suite-vendor/a-vona/modules/a-permission/test/permission.test.ts +12 -15
- package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-user/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-user/src/types/user.ts +1 -0
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/api/openapi/schemas.ts +68 -52
- package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/api/openapi/types.ts +530 -344
- package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/page/dashboard/controller.tsx +7 -1
- package/zova/src/suite/a-home/modules/home-api/src/api/openapi/types.ts +6 -1
- package/zova/src/suite/a-home/modules/home-base/src/config/locale/en-us.ts +1 -0
- package/zova/src/suite/a-home/modules/home-base/src/config/locale/zh-cn.ts +1 -0
- package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/render.user.tsx +1 -1
- package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/render.user.tsx +3 -1
- package/zova/src/suite/a-home/modules/home-passport/src/model/passport.ts +4 -0
- package/zova/src/suite/a-home/modules/home-user/src/config/locale/en-us.ts +1 -0
- package/zova/src/suite/a-home/modules/home-user/src/config/locale/zh-cn.ts +1 -0
- package/zova/src/suite/a-home/modules/home-user/src/model/account.ts +2 -1
- package/zova/src/suite/a-home/modules/home-user/src/page/account/controller.tsx +17 -3
- package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/test/aggregate.test.ts +0 -247
|
@@ -1,169 +1,308 @@
|
|
|
1
1
|
import type { VonaContext } from 'vona';
|
|
2
2
|
|
|
3
3
|
import { getRandomInt } from '@cabloy/utils';
|
|
4
|
-
import { BeanBase, beanFullNameFromOnionName,
|
|
4
|
+
import { BeanBase, beanFullNameFromOnionName, uuidv4 } from 'vona';
|
|
5
5
|
import { Bean } from 'vona-module-a-bean';
|
|
6
6
|
|
|
7
7
|
import type { ICaptchaData, ICaptchaDataCache } from '../types/captcha.ts';
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
ICaptchaAuthorization,
|
|
10
|
+
ICaptchaProviderData,
|
|
11
|
+
ICaptchaProviderExecute,
|
|
12
|
+
ICaptchaProviderRecord,
|
|
13
|
+
} from '../types/captchaProvider.ts';
|
|
9
14
|
import type {
|
|
10
15
|
ICaptchaSceneOptionsProviders,
|
|
11
16
|
ICaptchaSceneOptionsResolverResult,
|
|
12
17
|
ICaptchaSceneRecord,
|
|
13
18
|
} from '../types/captchaScene.ts';
|
|
14
|
-
|
|
15
|
-
import { SymbolCacheSceneProviders } from '../lib/const.ts';
|
|
19
|
+
import type { ICaptchaSnapshot } from './cacheRedis.captcha.ts';
|
|
16
20
|
|
|
17
21
|
@Bean()
|
|
18
22
|
export class BeanCaptcha extends BeanBase {
|
|
19
|
-
async create(sceneName: keyof ICaptchaSceneRecord): Promise<ICaptchaData> {
|
|
20
|
-
// resolve provider
|
|
23
|
+
async create(sceneName: keyof ICaptchaSceneRecord, context?: unknown): Promise<ICaptchaData> {
|
|
21
24
|
const provider = await this._resolveProvider(sceneName);
|
|
22
25
|
if (!provider) throw new Error(`not found captcha provider for scene: ${sceneName}`);
|
|
23
|
-
// create
|
|
24
26
|
const beanInstance = this._getProviderInstance(provider.name);
|
|
27
|
+
if (beanInstance.bound === true)
|
|
28
|
+
return await this._createBound(sceneName, provider, beanInstance, context);
|
|
25
29
|
const captcha = await beanInstance.create(provider.options);
|
|
26
|
-
// data
|
|
27
30
|
const id = uuidv4();
|
|
28
|
-
const
|
|
31
|
+
const data: ICaptchaDataCache = {
|
|
29
32
|
scene: sceneName,
|
|
30
33
|
provider: provider.name,
|
|
31
34
|
token: captcha.token,
|
|
35
|
+
revision: uuidv4(),
|
|
32
36
|
};
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
this.$loggerChild('captcha').debug(
|
|
38
|
-
() =>
|
|
39
|
-
`captcha.create: id:${id}, sceneName:${sceneName}, provider:${provider.name}, token:${captcha.token}`,
|
|
37
|
+
const created = await this.scope.cacheRedis.captcha.createImage(
|
|
38
|
+
id,
|
|
39
|
+
data,
|
|
40
|
+
provider.options.ttl ?? this.scope.config.captchaProvider.ttl,
|
|
40
41
|
);
|
|
41
|
-
|
|
42
|
+
if (!created) throw new Error('captcha identity collision');
|
|
42
43
|
const result: ICaptchaData = { id, provider: provider.name, payload: captcha.payload };
|
|
43
|
-
if (this.scope.config.captcha.showToken)
|
|
44
|
-
|
|
44
|
+
if (this.scope.config.captcha.showToken) result.token = captcha.token;
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private async _createBound(
|
|
49
|
+
sceneName: keyof ICaptchaSceneRecord,
|
|
50
|
+
provider: ICaptchaSceneOptionsResolverResult,
|
|
51
|
+
beanInstance: ICaptchaProviderExecute,
|
|
52
|
+
context: unknown,
|
|
53
|
+
): Promise<ICaptchaData> {
|
|
54
|
+
const authorization = await this._authorize(beanInstance, context, 'issue', sceneName);
|
|
55
|
+
if (!authorization) return this.app.throw(403);
|
|
56
|
+
const cache = this.scope.cacheRedis.captcha;
|
|
57
|
+
const id = `${cache.boundPrefix()}${uuidv4()}`;
|
|
58
|
+
const pending: ICaptchaDataCache = {
|
|
59
|
+
scene: sceneName,
|
|
60
|
+
provider: provider.name,
|
|
61
|
+
bound: {
|
|
62
|
+
binding: authorization.binding,
|
|
63
|
+
scope: authorization.scope,
|
|
64
|
+
state: 'pending',
|
|
65
|
+
expiresAt: 0,
|
|
66
|
+
resendAt: 0,
|
|
67
|
+
attemptsRemaining: authorization.maxAttempts,
|
|
68
|
+
version: 0,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
const reservation = await cache.reserveBound(id, pending, authorization);
|
|
72
|
+
if (!reservation.reserved) {
|
|
73
|
+
return {
|
|
74
|
+
id: '',
|
|
75
|
+
provider: provider.name,
|
|
76
|
+
payload: undefined,
|
|
77
|
+
status: 'limited',
|
|
78
|
+
resendAt: reservation.resendAt || undefined,
|
|
79
|
+
retryAt: reservation.retryAt,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const snapshot = reservation.snapshot;
|
|
83
|
+
const timing = {
|
|
84
|
+
expiresAt: snapshot.data.bound!.expiresAt,
|
|
85
|
+
resendAt: snapshot.data.bound!.resendAt,
|
|
86
|
+
};
|
|
87
|
+
let captcha: ICaptchaProviderData;
|
|
88
|
+
try {
|
|
89
|
+
// A reservation authorizes exactly one external attempt. Never retry sends.
|
|
90
|
+
captcha = await beanInstance.create(provider.options, context);
|
|
91
|
+
} catch {
|
|
92
|
+
await cache.finalizeBound(id, snapshot);
|
|
93
|
+
return { id: '', provider: provider.name, payload: undefined, status: 'unknown', ...timing };
|
|
45
94
|
}
|
|
95
|
+
const outcome = captcha.outcome ?? 'accepted';
|
|
96
|
+
if (outcome !== 'accepted' || captcha.token === undefined || captcha.token === null) {
|
|
97
|
+
await cache.finalizeBound(id, snapshot);
|
|
98
|
+
return {
|
|
99
|
+
id: '',
|
|
100
|
+
provider: provider.name,
|
|
101
|
+
payload: undefined,
|
|
102
|
+
status: outcome === 'rejected' ? 'rejected' : 'unknown',
|
|
103
|
+
...timing,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const active: ICaptchaDataCache = {
|
|
107
|
+
...snapshot.data,
|
|
108
|
+
token: captcha.token,
|
|
109
|
+
bound: { ...snapshot.data.bound!, state: 'active', version: 1 },
|
|
110
|
+
};
|
|
111
|
+
const finalized = await cache.finalizeBound(id, snapshot, active);
|
|
112
|
+
if (!finalized)
|
|
113
|
+
return { id: '', provider: provider.name, payload: undefined, status: 'unknown', ...timing };
|
|
114
|
+
const result: ICaptchaData = {
|
|
115
|
+
id,
|
|
116
|
+
provider: provider.name,
|
|
117
|
+
payload: captcha.payload,
|
|
118
|
+
status: 'accepted',
|
|
119
|
+
...timing,
|
|
120
|
+
};
|
|
121
|
+
// Generic showToken never grants disclosure for a bound provider.
|
|
122
|
+
if (captcha.exposeToken === true) result.token = captcha.token;
|
|
46
123
|
return result;
|
|
47
124
|
}
|
|
48
125
|
|
|
49
126
|
async refresh(id: string, sceneName: keyof ICaptchaSceneRecord) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
127
|
+
const cache = this.scope.cacheRedis.captcha;
|
|
128
|
+
const snapshot = await cache.snapshot(id);
|
|
129
|
+
if (!snapshot) return await this.create(sceneName);
|
|
130
|
+
const data = snapshot.data;
|
|
131
|
+
if (data.scene !== sceneName) return this.app.throw(403);
|
|
132
|
+
const beanInstance = this._getProviderInstance(data.provider);
|
|
133
|
+
if (data.bound || beanInstance.bound === true) return this.app.throw(403);
|
|
134
|
+
const options = this._getProviderOptions(data.scene, data.provider);
|
|
135
|
+
if (!options) return this.app.throw(403);
|
|
136
|
+
const captcha = await beanInstance.create(options);
|
|
137
|
+
const next = {
|
|
138
|
+
scene: sceneName,
|
|
139
|
+
provider: data.provider,
|
|
140
|
+
token: captcha.token,
|
|
141
|
+
revision: uuidv4(),
|
|
142
|
+
};
|
|
143
|
+
if (
|
|
144
|
+
!(await cache.compareAndSet(
|
|
145
|
+
id,
|
|
146
|
+
snapshot,
|
|
147
|
+
next,
|
|
148
|
+
options.ttl ?? this.scope.config.captchaProvider.ttl,
|
|
149
|
+
))
|
|
150
|
+
) {
|
|
151
|
+
return this.app.throw(403);
|
|
75
152
|
}
|
|
153
|
+
const result: ICaptchaData = { id, provider: data.provider, payload: captcha.payload };
|
|
154
|
+
if (this.scope.config.captcha.showToken) result.token = captcha.token;
|
|
76
155
|
return result;
|
|
77
156
|
}
|
|
78
157
|
|
|
79
|
-
async verify(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
158
|
+
async verify(
|
|
159
|
+
id: string,
|
|
160
|
+
token: unknown,
|
|
161
|
+
sceneName: keyof ICaptchaSceneRecord,
|
|
162
|
+
context?: unknown,
|
|
163
|
+
): Promise<boolean> {
|
|
164
|
+
const cache = this.scope.cacheRedis.captcha;
|
|
165
|
+
const snapshot = await cache.snapshot(id);
|
|
166
|
+
if (!snapshot || snapshot.data.scene !== sceneName) return false;
|
|
167
|
+
const data = snapshot.data;
|
|
168
|
+
const beanInstance = this._getProviderInstance(data.provider);
|
|
169
|
+
if (data.bound || beanInstance.bound === true) {
|
|
170
|
+
if (!data.bound || beanInstance.bound !== true) return false;
|
|
171
|
+
const authorization = await this._authorize(beanInstance, context, 'consume', sceneName);
|
|
172
|
+
if (!authorization) return false;
|
|
173
|
+
return await this._verifyBound(id, token, sceneName, beanInstance, authorization, snapshot);
|
|
174
|
+
}
|
|
175
|
+
if (data.token2) {
|
|
176
|
+
const consumed = await cache.compareAndSet(id, snapshot);
|
|
177
|
+
return consumed && data.token2 === token;
|
|
178
|
+
}
|
|
179
|
+
const options = this._getProviderOptions(data.scene, data.provider);
|
|
180
|
+
if (!options || !data.token) return false;
|
|
181
|
+
const verified = await beanInstance.verify(data.token, token, options);
|
|
182
|
+
const changed = await cache.compareAndSet(
|
|
183
|
+
id,
|
|
184
|
+
snapshot,
|
|
185
|
+
verified ? undefined : { ...data, token: undefined, revision: uuidv4() },
|
|
85
186
|
);
|
|
86
|
-
return verified;
|
|
187
|
+
return verified && changed;
|
|
87
188
|
}
|
|
88
189
|
|
|
89
|
-
private async
|
|
90
|
-
captchaData: ICaptchaDataCache | undefined,
|
|
190
|
+
private async _verifyBound(
|
|
91
191
|
id: string,
|
|
92
192
|
token: unknown,
|
|
93
193
|
sceneName: keyof ICaptchaSceneRecord,
|
|
194
|
+
beanInstance: ICaptchaProviderExecute,
|
|
195
|
+
authorization: ICaptchaAuthorization,
|
|
196
|
+
original: ICaptchaSnapshot,
|
|
94
197
|
): Promise<boolean> {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
198
|
+
const cache = this.scope.cacheRedis.captcha;
|
|
199
|
+
let snapshot: ICaptchaSnapshot | undefined = original;
|
|
200
|
+
// At most five competing wrong writes exist for this generation. A stale
|
|
201
|
+
// wrong verdict is retried against the new snapshot, never a new generation.
|
|
202
|
+
for (let retry = 0; retry < 5 && snapshot; retry++) {
|
|
203
|
+
const data = snapshot.data;
|
|
204
|
+
const bound = data.bound;
|
|
205
|
+
if (
|
|
206
|
+
!bound ||
|
|
207
|
+
bound.state !== 'active' ||
|
|
208
|
+
bound.attemptsRemaining <= 0 ||
|
|
209
|
+
data.scene !== sceneName ||
|
|
210
|
+
data.provider !== original.data.provider ||
|
|
211
|
+
bound.scope !== authorization.scope ||
|
|
212
|
+
!this._bindingMatches(bound.binding, authorization.binding)
|
|
213
|
+
) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
const options = this._getProviderOptions(sceneName, data.provider);
|
|
217
|
+
if (!options || data.token === undefined) return false;
|
|
218
|
+
const verified = await beanInstance.verify(data.token, token, options);
|
|
219
|
+
const committed = await cache.consumeBound(id, snapshot, verified);
|
|
220
|
+
if (committed !== -1 || verified) return verified && committed === 1;
|
|
221
|
+
snapshot = await cache.snapshot(id);
|
|
104
222
|
}
|
|
105
|
-
|
|
106
|
-
const beanInstance = this._getProviderInstance(captchaData.provider);
|
|
107
|
-
const providerOptions = this._getProviderOptions(captchaData.scene, captchaData.provider)!;
|
|
108
|
-
// verify
|
|
109
|
-
if (!captchaData.token) return false;
|
|
110
|
-
const verified = await beanInstance.verify(captchaData.token, token, providerOptions);
|
|
111
|
-
if (!verified) {
|
|
112
|
-
// update token. not delete cache for refresh
|
|
113
|
-
captchaData = { ...captchaData, token: undefined };
|
|
114
|
-
await this.scope.cacheRedis.captcha.set(captchaData, id, {
|
|
115
|
-
ttl: providerOptions.ttl ?? this.scope.config.captchaProvider.ttl,
|
|
116
|
-
});
|
|
117
|
-
return false;
|
|
118
|
-
}
|
|
119
|
-
// delete cache
|
|
120
|
-
await this.scope.cacheRedis.captcha.del(id);
|
|
121
|
-
// ok
|
|
122
|
-
return true;
|
|
223
|
+
return false;
|
|
123
224
|
}
|
|
124
225
|
|
|
125
226
|
async verifyImmediate(id: string, token: unknown): Promise<false | string> {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (!
|
|
129
|
-
|
|
130
|
-
const beanInstance = this._getProviderInstance(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
227
|
+
const cache = this.scope.cacheRedis.captcha;
|
|
228
|
+
const snapshot = await cache.snapshot(id);
|
|
229
|
+
if (!snapshot) return false;
|
|
230
|
+
const data = snapshot.data;
|
|
231
|
+
const beanInstance = this._getProviderInstance(data.provider);
|
|
232
|
+
if (data.bound || beanInstance.bound === true || !data.token) return false;
|
|
233
|
+
const options = this._getProviderOptions(data.scene, data.provider);
|
|
234
|
+
if (!options) return false;
|
|
235
|
+
const verified = await beanInstance.verify(data.token, token, options);
|
|
134
236
|
if (!verified) {
|
|
135
|
-
|
|
136
|
-
captchaData = { ...captchaData, token: undefined };
|
|
137
|
-
await this.scope.cacheRedis.captcha.set(captchaData, id, {
|
|
138
|
-
ttl: providerOptions.ttl ?? this.scope.config.captchaProvider.ttl,
|
|
139
|
-
});
|
|
237
|
+
await cache.compareAndSet(id, snapshot, { ...data, token: undefined, revision: uuidv4() });
|
|
140
238
|
return false;
|
|
141
239
|
}
|
|
142
|
-
// tokenSecondary
|
|
143
240
|
const tokenSecondary = uuidv4();
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
241
|
+
const next = { ...data, token: undefined, token2: tokenSecondary, revision: uuidv4() };
|
|
242
|
+
const changed = await cache.compareAndSet(
|
|
243
|
+
id,
|
|
244
|
+
snapshot,
|
|
245
|
+
next,
|
|
246
|
+
options.ttlSecondary ?? this.scope.config.captchaProvider.ttlSecondary,
|
|
247
|
+
);
|
|
248
|
+
return changed ? tokenSecondary : false;
|
|
151
249
|
}
|
|
152
250
|
|
|
153
251
|
async getCaptchaData(id: string) {
|
|
154
|
-
return await this.scope.cacheRedis.captcha.get(id);
|
|
252
|
+
return await this.scope.cacheRedis.captcha.get(id, { updateAgeOnGet: false });
|
|
155
253
|
}
|
|
156
254
|
|
|
157
255
|
async updateCaptchaToken(id: string, token: unknown) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
256
|
+
const cache = this.scope.cacheRedis.captcha;
|
|
257
|
+
const snapshot = await cache.snapshot(id);
|
|
258
|
+
if (!snapshot) return this.app.throw(403);
|
|
259
|
+
const data = snapshot.data;
|
|
260
|
+
const beanInstance = this._getProviderInstance(data.provider);
|
|
261
|
+
if (data.bound || beanInstance.bound === true) return this.app.throw(403);
|
|
262
|
+
const options = this._getProviderOptions(data.scene, data.provider);
|
|
263
|
+
if (!options) return this.app.throw(403);
|
|
264
|
+
if (
|
|
265
|
+
!(await cache.compareAndSet(
|
|
266
|
+
id,
|
|
267
|
+
snapshot,
|
|
268
|
+
{ ...data, token, revision: uuidv4() },
|
|
269
|
+
options.ttl ?? this.scope.config.captchaProvider.ttl,
|
|
270
|
+
))
|
|
271
|
+
) {
|
|
272
|
+
return this.app.throw(403);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private async _authorize(
|
|
277
|
+
provider: ICaptchaProviderExecute,
|
|
278
|
+
context: unknown,
|
|
279
|
+
operation: 'issue' | 'consume',
|
|
280
|
+
sceneName: keyof ICaptchaSceneRecord,
|
|
281
|
+
): Promise<ICaptchaAuthorization | undefined> {
|
|
282
|
+
const authorization = await provider.authorize?.(context, operation, sceneName);
|
|
283
|
+
if (!authorization) return;
|
|
284
|
+
this.scope.cacheRedis.captcha.validateAuthorization(authorization);
|
|
285
|
+
// Snapshot only the server-authorized contract; later provider work cannot
|
|
286
|
+
// mutate the binding/policy already used for this issuance or consumption.
|
|
287
|
+
return {
|
|
288
|
+
...authorization,
|
|
289
|
+
binding: {
|
|
290
|
+
recipient: authorization.binding.recipient,
|
|
291
|
+
purpose: authorization.binding.purpose,
|
|
292
|
+
...(authorization.binding.subject === undefined
|
|
293
|
+
? {}
|
|
294
|
+
: { subject: authorization.binding.subject }),
|
|
295
|
+
},
|
|
296
|
+
recipientLimits: authorization.recipientLimits.map(limit => ({ ...limit })),
|
|
297
|
+
ipLimits: authorization.ipLimits.map(limit => ({ ...limit })),
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private _bindingMatches(
|
|
302
|
+
a: ICaptchaAuthorization['binding'],
|
|
303
|
+
b: ICaptchaAuthorization['binding'],
|
|
304
|
+
) {
|
|
305
|
+
return a.recipient === b.recipient && a.purpose === b.purpose && a.subject === b.subject;
|
|
167
306
|
}
|
|
168
307
|
|
|
169
308
|
private _getProviderInstance(providerName: keyof ICaptchaProviderRecord) {
|
|
@@ -175,51 +314,46 @@ export class BeanCaptcha extends BeanBase {
|
|
|
175
314
|
sceneName: keyof ICaptchaSceneRecord,
|
|
176
315
|
providerName: keyof ICaptchaProviderRecord,
|
|
177
316
|
) {
|
|
178
|
-
|
|
179
|
-
const providers = this._getProviders(sceneName);
|
|
180
|
-
return providers[providerName];
|
|
317
|
+
return this._getProviders(sceneName)[providerName];
|
|
181
318
|
}
|
|
182
319
|
|
|
183
320
|
private async _resolveProvider(
|
|
184
321
|
sceneName: keyof ICaptchaSceneRecord,
|
|
185
322
|
): Promise<ICaptchaSceneOptionsResolverResult | undefined> {
|
|
186
|
-
// providers
|
|
187
323
|
const providers = this._getProviders(sceneName);
|
|
188
324
|
if (Object.keys(providers).length === 0) return;
|
|
189
|
-
|
|
190
|
-
const onionSlice = this.bean.onion.captchaScene.getOnionSlice(sceneName);
|
|
191
|
-
const onionOptions = onionSlice.beanOptions.options;
|
|
325
|
+
const onionOptions = this.bean.onion.captchaScene.getOnionOptionsDynamic(sceneName);
|
|
192
326
|
const resolver = onionOptions?.resolver ?? resolverDefault;
|
|
193
327
|
const providerName = await resolver(this.ctx, providers);
|
|
194
|
-
if (!providerName) return;
|
|
328
|
+
if (!providerName || !providers[providerName]) return;
|
|
195
329
|
return { name: providerName, options: providers[providerName] };
|
|
196
330
|
}
|
|
197
331
|
|
|
198
332
|
private _getProviders(sceneName: keyof ICaptchaSceneRecord) {
|
|
199
|
-
|
|
200
|
-
if (!
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const onionOptions = onionSlice.beanOptions.options;
|
|
204
|
-
this.app.meta[SymbolCacheSceneProviders][sceneName] = this._prepareProviders(
|
|
205
|
-
onionOptions?.providers,
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
return this.app.meta[SymbolCacheSceneProviders][sceneName];
|
|
333
|
+
const scene = this.bean.onion.captchaScene.getOnionSlice(sceneName);
|
|
334
|
+
if (!scene) throw new Error(`not found captcha scene: ${sceneName}`);
|
|
335
|
+
const options = this.bean.onion.captchaScene.getOnionOptionsDynamic(sceneName);
|
|
336
|
+
return this._prepareProviders(options?.providers);
|
|
209
337
|
}
|
|
210
338
|
|
|
211
339
|
private _prepareProviders(providers?: ICaptchaSceneOptionsProviders): ICaptchaProviderRecord {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
for (const _key
|
|
215
|
-
const key
|
|
340
|
+
const result: ICaptchaProviderRecord = {} as any;
|
|
341
|
+
if (!providers) return result;
|
|
342
|
+
for (const _key of Object.keys(providers)) {
|
|
343
|
+
const key = _key as keyof ICaptchaProviderRecord;
|
|
216
344
|
const providerOptions = providers[key];
|
|
217
345
|
if (providerOptions === false) continue;
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
346
|
+
const onion = this.bean.onion.captchaProvider;
|
|
347
|
+
const slice = onion.getOnionSlice(key);
|
|
348
|
+
if (!slice) continue;
|
|
349
|
+
const options = onion.getOnionOptionsDynamic(
|
|
350
|
+
key,
|
|
351
|
+
typeof providerOptions === 'object' ? providerOptions : undefined,
|
|
352
|
+
);
|
|
353
|
+
if (!this.bean.onion.checkOnionOptionsEnabled(options)) continue;
|
|
354
|
+
Object.assign(result, { [key]: options });
|
|
221
355
|
}
|
|
222
|
-
return
|
|
356
|
+
return result;
|
|
223
357
|
}
|
|
224
358
|
}
|
|
225
359
|
|