mirror of
https://github.com/logto-io/logto.git
synced 2025-03-10 22:22:45 -05:00
feat(connector): connector error handler, throw errmsg on general errors (#1458)
This commit is contained in:
parent
da882cee85
commit
7da1de33e9
26 changed files with 293 additions and 167 deletions
|
@ -11,6 +11,10 @@ export const alipaySigningAlgorithmMapping = {
|
|||
} as const;
|
||||
export const alipaySigningAlgorithms = ['RSA', 'RSA2'] as const;
|
||||
|
||||
export const invalidAccessTokenCode = ['20001'];
|
||||
|
||||
export const invalidAccessTokenSubCode = ['isv.code-invalid'];
|
||||
|
||||
export const defaultMetadata: ConnectorMetadata = {
|
||||
id: 'alipay-native',
|
||||
target: 'alipay',
|
||||
|
|
|
@ -216,7 +216,12 @@ describe('getUserInfo', () => {
|
|||
});
|
||||
|
||||
await expect(alipayNativeMethods.getUserInfo({ auth_code: 'wrong_code' })).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.General, 'Invalid parameter')
|
||||
new ConnectorError(ConnectorErrorCodes.General, {
|
||||
errorDescription: 'Invalid parameter',
|
||||
code: '40002',
|
||||
sub_code: 'isv.invalid-parameter',
|
||||
sub_msg: '参数无效',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -30,6 +30,8 @@ import {
|
|||
defaultMetadata,
|
||||
defaultTimeout,
|
||||
timestampFormat,
|
||||
invalidAccessTokenCode,
|
||||
invalidAccessTokenSubCode,
|
||||
} from './constant';
|
||||
import {
|
||||
alipayNativeConfigGuard,
|
||||
|
@ -139,28 +141,35 @@ export default class AlipayNativeConnector implements SocialConnector {
|
|||
|
||||
const { alipay_user_info_share_response } = result.data;
|
||||
|
||||
const {
|
||||
user_id: id,
|
||||
avatar,
|
||||
nick_name: name,
|
||||
code,
|
||||
msg,
|
||||
sub_code,
|
||||
} = alipay_user_info_share_response;
|
||||
this.errorHandler(alipay_user_info_share_response);
|
||||
|
||||
this.errorHandler({ code, msg, sub_code });
|
||||
assert(id, new ConnectorError(ConnectorErrorCodes.InvalidResponse));
|
||||
const { user_id: id, avatar, nick_name: name } = alipay_user_info_share_response;
|
||||
|
||||
if (!id) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse);
|
||||
}
|
||||
|
||||
return { id, avatar, name };
|
||||
};
|
||||
|
||||
private readonly errorHandler: ErrorHandler = ({ code, msg, sub_code }) => {
|
||||
assert(code !== '20001', new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid, msg));
|
||||
assert(
|
||||
sub_code !== 'isv.code-invalid',
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, msg)
|
||||
);
|
||||
assert(!sub_code, new ConnectorError(ConnectorErrorCodes.General, msg));
|
||||
private readonly errorHandler: ErrorHandler = ({ code, msg, sub_code, sub_msg }) => {
|
||||
if (invalidAccessTokenCode.includes(code)) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid, msg);
|
||||
}
|
||||
|
||||
if (sub_code) {
|
||||
assert(
|
||||
!invalidAccessTokenSubCode.includes(sub_code),
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, msg)
|
||||
);
|
||||
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, {
|
||||
errorDescription: msg,
|
||||
code,
|
||||
sub_code,
|
||||
sub_msg,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private readonly authorizationCallbackHandler = async (parameterObject: unknown) => {
|
||||
|
|
|
@ -47,6 +47,8 @@ export const alipayUserInfoShareResponseGuard = z.object({
|
|||
sub_msg: z.string().optional(),
|
||||
});
|
||||
|
||||
type AlipayUserInfoShareResponseGuard = z.infer<typeof alipayUserInfoShareResponseGuard>;
|
||||
|
||||
export const userInfoResponseGuard = z.object({
|
||||
sign: z.string(), // To know `sign` details, see: https://opendocs.alipay.com/common/02kf5q
|
||||
alipay_user_info_share_response: alipayUserInfoShareResponseGuard,
|
||||
|
@ -54,9 +56,4 @@ export const userInfoResponseGuard = z.object({
|
|||
|
||||
export type UserInfoResponse = z.infer<typeof userInfoResponseGuard>;
|
||||
|
||||
export type ErrorHandler = (response: {
|
||||
code: string;
|
||||
msg: string;
|
||||
sub_code?: string;
|
||||
sub_msg?: string;
|
||||
}) => void;
|
||||
export type ErrorHandler = (response: AlipayUserInfoShareResponseGuard) => void;
|
||||
|
|
|
@ -14,6 +14,10 @@ export const alipaySigningAlgorithms = ['RSA', 'RSA2'] as const;
|
|||
export const charsetEnum = ['GBK', 'UTF8'] as const;
|
||||
export const fallbackCharset = 'UTF8';
|
||||
|
||||
export const invalidAccessTokenCode = ['20001'];
|
||||
|
||||
export const invalidAccessTokenSubCode = ['isv.code-invalid'];
|
||||
|
||||
export const defaultMetadata: ConnectorMetadata = {
|
||||
id: 'alipay-web',
|
||||
target: 'alipay',
|
||||
|
|
|
@ -159,7 +159,7 @@ describe('getUserInfo', () => {
|
|||
|
||||
it('throw General error if auth_code not provided in input', async () => {
|
||||
await expect(alipayMethods.getUserInfo({})).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.General, '{}')
|
||||
new ConnectorError(ConnectorErrorCodes.InvalidResponse, '{}')
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -216,7 +216,12 @@ describe('getUserInfo', () => {
|
|||
});
|
||||
|
||||
await expect(alipayMethods.getUserInfo({ auth_code: 'wrong_code' })).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.General, 'Invalid parameter')
|
||||
new ConnectorError(ConnectorErrorCodes.General, {
|
||||
errorDescription: 'Invalid parameter',
|
||||
code: '40002',
|
||||
sub_code: 'isv.invalid-parameter',
|
||||
sub_msg: '参数无效',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -30,6 +30,8 @@ import {
|
|||
defaultTimeout,
|
||||
timestampFormat,
|
||||
fallbackCharset,
|
||||
invalidAccessTokenCode,
|
||||
invalidAccessTokenSubCode,
|
||||
} from './constant';
|
||||
import {
|
||||
alipayConfigGuard,
|
||||
|
@ -140,7 +142,9 @@ export default class AlipayConnector implements SocialConnector {
|
|||
timeout: defaultTimeout,
|
||||
});
|
||||
|
||||
const result = userInfoResponseGuard.safeParse(JSON.parse(httpResponse.body));
|
||||
const { body: rawBody } = httpResponse;
|
||||
|
||||
const result = userInfoResponseGuard.safeParse(JSON.parse(rawBody));
|
||||
|
||||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error.message);
|
||||
|
@ -148,28 +152,35 @@ export default class AlipayConnector implements SocialConnector {
|
|||
|
||||
const { alipay_user_info_share_response } = result.data;
|
||||
|
||||
const {
|
||||
user_id: id,
|
||||
avatar,
|
||||
nick_name: name,
|
||||
code,
|
||||
msg,
|
||||
sub_code,
|
||||
} = alipay_user_info_share_response;
|
||||
this.errorHandler(alipay_user_info_share_response);
|
||||
|
||||
this.errorHandler({ code, msg, sub_code });
|
||||
assert(id, new ConnectorError(ConnectorErrorCodes.InvalidResponse));
|
||||
const { user_id: id, avatar, nick_name: name } = alipay_user_info_share_response;
|
||||
|
||||
if (!id) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse);
|
||||
}
|
||||
|
||||
return { id, avatar, name };
|
||||
};
|
||||
|
||||
private readonly errorHandler: ErrorHandler = ({ code, msg, sub_code }) => {
|
||||
assert(code !== '20001', new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid, msg));
|
||||
assert(
|
||||
sub_code !== 'isv.code-invalid',
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, msg)
|
||||
);
|
||||
assert(!sub_code, new ConnectorError(ConnectorErrorCodes.General, msg));
|
||||
private readonly errorHandler: ErrorHandler = ({ code, msg, sub_code, sub_msg }) => {
|
||||
if (invalidAccessTokenCode.includes(code)) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid, msg);
|
||||
}
|
||||
|
||||
if (sub_code) {
|
||||
assert(
|
||||
!invalidAccessTokenSubCode.includes(sub_code),
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, msg)
|
||||
);
|
||||
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, {
|
||||
errorDescription: msg,
|
||||
code,
|
||||
sub_code,
|
||||
sub_msg,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private readonly authorizationCallbackHandler = async (parameterObject: unknown) => {
|
||||
|
@ -178,7 +189,10 @@ export default class AlipayConnector implements SocialConnector {
|
|||
const result = dataGuard.safeParse(parameterObject);
|
||||
|
||||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(parameterObject));
|
||||
throw new ConnectorError(
|
||||
ConnectorErrorCodes.InvalidResponse,
|
||||
JSON.stringify(parameterObject)
|
||||
);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
|
|
|
@ -48,6 +48,8 @@ export const alipayUserInfoShareResponseGuard = z.object({
|
|||
sub_msg: z.string().optional(),
|
||||
});
|
||||
|
||||
type AlipayUserInfoShareResponse = z.infer<typeof alipayUserInfoShareResponseGuard>;
|
||||
|
||||
export const userInfoResponseGuard = z.object({
|
||||
sign: z.string(), // To know `sign` details, see: https://opendocs.alipay.com/common/02kf5q
|
||||
alipay_user_info_share_response: alipayUserInfoShareResponseGuard,
|
||||
|
@ -55,9 +57,4 @@ export const userInfoResponseGuard = z.object({
|
|||
|
||||
export type UserInfoResponse = z.infer<typeof userInfoResponseGuard>;
|
||||
|
||||
export type ErrorHandler = (response: {
|
||||
code: string;
|
||||
msg: string;
|
||||
sub_code?: string;
|
||||
sub_msg?: string;
|
||||
}) => void;
|
||||
export type ErrorHandler = (response: AlipayUserInfoShareResponse) => void;
|
||||
|
|
|
@ -78,12 +78,13 @@ export default class AliyunDmConnector implements EmailConnector {
|
|||
const {
|
||||
response: { body: rawBody },
|
||||
} = error;
|
||||
|
||||
assert(
|
||||
typeof rawBody === 'string',
|
||||
new ConnectorError(ConnectorErrorCodes.InvalidResponse)
|
||||
);
|
||||
|
||||
this.errorHandler(rawBody);
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, rawBody);
|
||||
}
|
||||
|
||||
throw error;
|
||||
|
@ -97,17 +98,8 @@ export default class AliyunDmConnector implements EmailConnector {
|
|||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error.message);
|
||||
}
|
||||
|
||||
const { Code } = result.data;
|
||||
const { Message: errorDescription, ...rest } = result.data;
|
||||
|
||||
// See https://help.aliyun.com/document_detail/29444.html.
|
||||
assert(
|
||||
!(
|
||||
Code === 'InvalidBody' ||
|
||||
Code === 'InvalidTemplate.NotFound' ||
|
||||
Code === 'InvalidSubject.Malformed' ||
|
||||
Code === 'InvalidFromAlias.Malformed'
|
||||
),
|
||||
new ConnectorError(ConnectorErrorCodes.InvalidConfig, errorResponseBody)
|
||||
);
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, { errorDescription, ...rest });
|
||||
};
|
||||
}
|
||||
|
|
|
@ -63,6 +63,7 @@ export const sendMailErrorResponseGuard = z.object({
|
|||
Message: z.string(),
|
||||
RequestId: z.string().optional(),
|
||||
HostId: z.string().optional(),
|
||||
Recommend: z.string().optional(),
|
||||
});
|
||||
|
||||
export type SendMailErrorResponse = z.infer<typeof sendMailErrorResponseGuard>;
|
||||
|
|
|
@ -27,7 +27,6 @@ export default class AliyunSmsConnector implements SmsConnector {
|
|||
}
|
||||
};
|
||||
|
||||
/* eslint-disable complexity */
|
||||
public sendMessage: SmsSendMessageFunction = async (phone, type, { code }, config) => {
|
||||
const smsConfig =
|
||||
(config as AliyunSmsConfig | undefined) ?? (await this.getConfig(this.metadata.id));
|
||||
|
@ -54,14 +53,14 @@ export default class AliyunSmsConnector implements SmsConnector {
|
|||
|
||||
const { body: rawBody } = httpResponse;
|
||||
|
||||
const { Code } = this.parseResponseString(rawBody);
|
||||
|
||||
if (Code === 'isv.ACCOUNT_NOT_EXISTS' || Code === 'isv.SMS_TEMPLATE_ILLEGAL') {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, rawBody);
|
||||
}
|
||||
const { Code, Message, ...rest } = this.parseResponseString(rawBody);
|
||||
|
||||
if (Code !== 'OK') {
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, rawBody);
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, {
|
||||
errorDescription: Message,
|
||||
Code,
|
||||
...rest,
|
||||
});
|
||||
}
|
||||
|
||||
return httpResponse;
|
||||
|
@ -76,20 +75,15 @@ export default class AliyunSmsConnector implements SmsConnector {
|
|||
|
||||
assert(typeof rawBody === 'string', new ConnectorError(ConnectorErrorCodes.InvalidResponse));
|
||||
|
||||
const { Code } = this.parseResponseString(rawBody);
|
||||
const { Code, Message, ...rest } = this.parseResponseString(rawBody);
|
||||
|
||||
if (Code.includes('InvalidAccessKeyId')) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, rawBody);
|
||||
}
|
||||
|
||||
if (Code === 'SignatureDoesNotMatch' || Code === 'IncompleteSignature') {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, rawBody);
|
||||
}
|
||||
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, rawBody);
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, {
|
||||
errorDescription: Message,
|
||||
Code,
|
||||
...rest,
|
||||
});
|
||||
}
|
||||
};
|
||||
/* eslint-enable complexity */
|
||||
|
||||
private readonly parseResponseString = (response: string) => {
|
||||
const result = sendSmsResponseGuard.safeParse(JSON.parse(response));
|
||||
|
|
|
@ -194,10 +194,12 @@ describe('facebook connector', () => {
|
|||
error_reason: 'user_denied',
|
||||
})
|
||||
).rejects.toMatchError(
|
||||
new ConnectorError(
|
||||
ConnectorErrorCodes.General,
|
||||
'{"error":"general_error","error_code":200,"error_description":"General error encountered.","error_reason":"user_denied"}'
|
||||
)
|
||||
new ConnectorError(ConnectorErrorCodes.General, {
|
||||
error: 'general_error',
|
||||
error_code: 200,
|
||||
errorDescription: 'General error encountered.',
|
||||
error_reason: 'user_denied',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
codeWithRedirectDataGuard,
|
||||
} from '@logto/connector-types';
|
||||
import { assert } from '@silverhand/essentials';
|
||||
import got, { RequestError as GotRequestError } from 'got';
|
||||
import got, { HTTPError } from 'got';
|
||||
|
||||
import {
|
||||
accessTokenEndpoint,
|
||||
|
@ -118,9 +118,16 @@ export default class FacebookConnector implements SocialConnector {
|
|||
name,
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof GotRequestError && error.response?.statusCode === 400) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid);
|
||||
if (error instanceof HTTPError) {
|
||||
const { statusCode, body: rawBody } = error.response;
|
||||
|
||||
if (statusCode === 400) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid);
|
||||
}
|
||||
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(rawBody));
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
@ -135,16 +142,23 @@ export default class FacebookConnector implements SocialConnector {
|
|||
const parsedError = authorizationCallbackErrorGuard.safeParse(parameterObject);
|
||||
|
||||
if (!parsedError.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(parameterObject));
|
||||
}
|
||||
|
||||
if (parsedError.data.error === 'access_denied') {
|
||||
throw new ConnectorError(
|
||||
ConnectorErrorCodes.AuthorizationFailed,
|
||||
parsedError.data.error_description
|
||||
ConnectorErrorCodes.InvalidResponse,
|
||||
JSON.stringify(parameterObject)
|
||||
);
|
||||
}
|
||||
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(parameterObject));
|
||||
const { error, error_code, error_description, error_reason } = parsedError.data;
|
||||
|
||||
if (error === 'access_denied') {
|
||||
throw new ConnectorError(ConnectorErrorCodes.AuthorizationFailed, error_description);
|
||||
}
|
||||
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, {
|
||||
error,
|
||||
error_code,
|
||||
errorDescription: error_description,
|
||||
error_reason,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
codeDataGuard,
|
||||
} from '@logto/connector-types';
|
||||
import { assert, conditional } from '@silverhand/essentials';
|
||||
import got, { RequestError as GotRequestError } from 'got';
|
||||
import got, { HTTPError } from 'got';
|
||||
import * as qs from 'query-string';
|
||||
|
||||
import {
|
||||
|
@ -110,9 +110,16 @@ export default class GithubConnector implements SocialConnector {
|
|||
name: conditional(name),
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof GotRequestError && error.response?.statusCode === 401) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid);
|
||||
if (error instanceof HTTPError) {
|
||||
const { statusCode, body: rawBody } = error.response;
|
||||
|
||||
if (statusCode === 401) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid);
|
||||
}
|
||||
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(rawBody));
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
@ -130,13 +137,16 @@ export default class GithubConnector implements SocialConnector {
|
|||
throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(parameterObject));
|
||||
}
|
||||
|
||||
if (parsedError.data.error === 'access_denied') {
|
||||
throw new ConnectorError(
|
||||
ConnectorErrorCodes.AuthorizationFailed,
|
||||
parsedError.data.error_description
|
||||
);
|
||||
const { error, error_description, error_uri } = parsedError.data;
|
||||
|
||||
if (error === 'access_denied') {
|
||||
throw new ConnectorError(ConnectorErrorCodes.AuthorizationFailed, error_description);
|
||||
}
|
||||
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(parameterObject));
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, {
|
||||
error,
|
||||
errorDescription: error_description,
|
||||
error_uri,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
codeWithRedirectDataGuard,
|
||||
} from '@logto/connector-types';
|
||||
import { conditional, assert } from '@silverhand/essentials';
|
||||
import got, { RequestError as GotRequestError } from 'got';
|
||||
import got, { HTTPError } from 'got';
|
||||
|
||||
import {
|
||||
accessTokenEndpoint,
|
||||
|
@ -87,6 +87,7 @@ export default class GoogleConnector implements SocialConnector {
|
|||
return { accessToken };
|
||||
};
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
public getUserInfo: GetUserInfo = async (data) => {
|
||||
const { code, redirectUri } = await this.authorizationCallbackHandler(data);
|
||||
const { accessToken } = await this.getAccessToken(code, redirectUri);
|
||||
|
@ -104,6 +105,7 @@ export default class GoogleConnector implements SocialConnector {
|
|||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error.message);
|
||||
}
|
||||
|
||||
const { sub: id, picture: avatar, email, email_verified, name } = result.data;
|
||||
|
||||
return {
|
||||
|
@ -113,10 +115,15 @@ export default class GoogleConnector implements SocialConnector {
|
|||
name,
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
assert(
|
||||
!(error instanceof GotRequestError && error.response?.statusCode === 401),
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid)
|
||||
);
|
||||
if (error instanceof HTTPError) {
|
||||
const { statusCode, body: rawBody } = error.response;
|
||||
|
||||
if (statusCode === 401) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid);
|
||||
}
|
||||
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(rawBody));
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
|
|
@ -62,11 +62,12 @@ export type SendSmsResponse = {
|
|||
uri: string;
|
||||
};
|
||||
|
||||
// See https://www.twilio.com/docs/usage/twilios-response
|
||||
export const sendSmsErrorResponseGuard = z.object({
|
||||
code: z.number(),
|
||||
message: z.string(),
|
||||
more_info: z.string(),
|
||||
status: z.number(),
|
||||
message: z.string(),
|
||||
code: z.number().optional(),
|
||||
more_info: z.string().optional(),
|
||||
});
|
||||
|
||||
export type SendSmsErrorResponse = z.infer<typeof sendSmsErrorResponseGuard>;
|
||||
|
|
|
@ -5,6 +5,11 @@ export const accessTokenEndpoint = 'https://api.weixin.qq.com/sns/oauth2/access_
|
|||
export const userInfoEndpoint = 'https://api.weixin.qq.com/sns/userinfo';
|
||||
export const scope = 'snsapi_userinfo';
|
||||
|
||||
// See https://developers.weixin.qq.com/doc/oplatform/Return_codes/Return_code_descriptions_new.html to know more about WeChat response error code
|
||||
export const invalidAuthCodeErrcode = [40_029, 40_163, 42_003];
|
||||
|
||||
export const invalidAccessTokenErrcode = [40_001, 40_014];
|
||||
|
||||
export const defaultMetadata: ConnectorMetadata = {
|
||||
id: 'wechat-native',
|
||||
target: 'wechat',
|
||||
|
|
|
@ -83,7 +83,10 @@ describe('getAccessToken', () => {
|
|||
.query(true)
|
||||
.reply(200, { errcode: -1, errmsg: 'system error' });
|
||||
await expect(wechatNativeMethods.getAccessToken('wrong_code')).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.General, 'system error')
|
||||
new ConnectorError(ConnectorErrorCodes.General, {
|
||||
errorDescription: 'system error',
|
||||
errcode: -1,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -175,7 +178,10 @@ describe('getUserInfo', () => {
|
|||
errmsg: 'missing openid',
|
||||
});
|
||||
await expect(wechatNativeMethods.getUserInfo({ code: 'code' })).rejects.toMatchError(
|
||||
new Error('missing openid')
|
||||
new ConnectorError(ConnectorErrorCodes.General, {
|
||||
errorDescription: 'missing openid',
|
||||
errcode: 41_009,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -200,7 +206,10 @@ describe('getUserInfo', () => {
|
|||
.query(parameters)
|
||||
.reply(200, { errcode: 40_003, errmsg: 'invalid openid' });
|
||||
await expect(wechatNativeMethods.getUserInfo({ code: 'code' })).rejects.toMatchError(
|
||||
new Error('invalid openid')
|
||||
new ConnectorError(ConnectorErrorCodes.General, {
|
||||
errorDescription: 'invalid openid',
|
||||
errcode: 40_003,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
codeDataGuard,
|
||||
} from '@logto/connector-types';
|
||||
import { assert } from '@silverhand/essentials';
|
||||
import got, { RequestError as GotRequestError } from 'got';
|
||||
import got, { HTTPError } from 'got';
|
||||
|
||||
import {
|
||||
authorizationEndpoint,
|
||||
|
@ -23,6 +23,8 @@ import {
|
|||
userInfoEndpoint,
|
||||
defaultMetadata,
|
||||
defaultTimeout,
|
||||
invalidAccessTokenErrcode,
|
||||
invalidAuthCodeErrcode,
|
||||
} from './constant';
|
||||
import {
|
||||
wechatNativeConfigGuard,
|
||||
|
@ -84,6 +86,7 @@ export default class WechatNativeConnector implements SocialConnector {
|
|||
return { accessToken, openid };
|
||||
};
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
public getUserInfo: GetUserInfo = async (data) => {
|
||||
const { code } = await this.authorizationCallbackHandler(data);
|
||||
const { accessToken, openid } = await this.getAccessToken(code);
|
||||
|
@ -109,10 +112,15 @@ export default class WechatNativeConnector implements SocialConnector {
|
|||
|
||||
return { id: unionid ?? openid, avatar: headimgurl, name: nickname };
|
||||
} catch (error: unknown) {
|
||||
assert(
|
||||
!(error instanceof GotRequestError && error.response?.statusCode === 401),
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid)
|
||||
);
|
||||
if (error instanceof HTTPError) {
|
||||
const { statusCode, body: rawBody } = error.response;
|
||||
|
||||
if (statusCode === 401) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid);
|
||||
}
|
||||
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(rawBody));
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
@ -121,28 +129,28 @@ export default class WechatNativeConnector implements SocialConnector {
|
|||
// See https://developers.weixin.qq.com/doc/oplatform/Return_codes/Return_code_descriptions_new.html
|
||||
private readonly getAccessTokenErrorHandler: GetAccessTokenErrorHandler = (accessToken) => {
|
||||
const { errcode, errmsg } = accessToken;
|
||||
assert(
|
||||
errcode !== 40_029,
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, errmsg)
|
||||
);
|
||||
assert(
|
||||
errcode !== 40_163,
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, errmsg)
|
||||
);
|
||||
assert(
|
||||
errcode !== 42_003,
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, errmsg)
|
||||
);
|
||||
assert(!errcode, new ConnectorError(ConnectorErrorCodes.General, errmsg));
|
||||
|
||||
if (errcode) {
|
||||
assert(
|
||||
!invalidAuthCodeErrcode.includes(errcode),
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, errmsg)
|
||||
);
|
||||
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, { errorDescription: errmsg, errcode });
|
||||
}
|
||||
};
|
||||
|
||||
private readonly getUserInfoErrorHandler: GetUserInfoErrorHandler = (userInfo) => {
|
||||
const { errcode, errmsg } = userInfo;
|
||||
assert(
|
||||
!(errcode === 40_001 || errcode === 40_014),
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid, errmsg)
|
||||
);
|
||||
assert(!errcode, new Error(errmsg ?? ''));
|
||||
|
||||
if (errcode) {
|
||||
assert(
|
||||
!invalidAccessTokenErrcode.includes(errcode),
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid, errmsg)
|
||||
);
|
||||
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, { errorDescription: errmsg, errcode });
|
||||
}
|
||||
};
|
||||
|
||||
private readonly authorizationCallbackHandler = async (parameterObject: unknown) => {
|
||||
|
|
|
@ -5,6 +5,11 @@ export const accessTokenEndpoint = 'https://api.weixin.qq.com/sns/oauth2/access_
|
|||
export const userInfoEndpoint = 'https://api.weixin.qq.com/sns/userinfo';
|
||||
export const scope = 'snsapi_login';
|
||||
|
||||
// See https://developers.weixin.qq.com/doc/oplatform/Return_codes/Return_code_descriptions_new.html to know more about WeChat response error code
|
||||
export const invalidAuthCodeErrcode = [40_029, 40_163, 42_003];
|
||||
|
||||
export const invalidAccessTokenErrcode = [40_001, 40_014];
|
||||
|
||||
export const defaultMetadata: ConnectorMetadata = {
|
||||
id: 'wechat-web',
|
||||
target: 'wechat',
|
||||
|
|
|
@ -83,7 +83,10 @@ describe('getAccessToken', () => {
|
|||
.query(true)
|
||||
.reply(200, { errcode: -1, errmsg: 'system error' });
|
||||
await expect(wechatMethods.getAccessToken('wrong_code')).rejects.toMatchError(
|
||||
new ConnectorError(ConnectorErrorCodes.General, 'system error')
|
||||
new ConnectorError(ConnectorErrorCodes.General, {
|
||||
errorDescription: 'system error',
|
||||
errcode: -1,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -168,7 +171,10 @@ describe('getUserInfo', () => {
|
|||
errmsg: 'missing openid',
|
||||
});
|
||||
await expect(wechatMethods.getUserInfo({ code: 'code' })).rejects.toMatchError(
|
||||
new Error('missing openid')
|
||||
new ConnectorError(ConnectorErrorCodes.General, {
|
||||
errorDescription: 'missing openid',
|
||||
errcode: 41_009,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -193,7 +199,10 @@ describe('getUserInfo', () => {
|
|||
.query(parameters)
|
||||
.reply(200, { errcode: 40_003, errmsg: 'invalid openid' });
|
||||
await expect(wechatMethods.getUserInfo({ code: 'code' })).rejects.toMatchError(
|
||||
new Error('invalid openid')
|
||||
new ConnectorError(ConnectorErrorCodes.General, {
|
||||
errorDescription: 'invalid openid',
|
||||
errcode: 40_003,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
codeDataGuard,
|
||||
} from '@logto/connector-types';
|
||||
import { assert } from '@silverhand/essentials';
|
||||
import got, { RequestError as GotRequestError } from 'got';
|
||||
import got, { HTTPError } from 'got';
|
||||
|
||||
import {
|
||||
authorizationEndpoint,
|
||||
|
@ -24,6 +24,8 @@ import {
|
|||
scope,
|
||||
defaultMetadata,
|
||||
defaultTimeout,
|
||||
invalidAccessTokenErrcode,
|
||||
invalidAuthCodeErrcode,
|
||||
} from './constant';
|
||||
import {
|
||||
wechatConfigGuard,
|
||||
|
@ -80,11 +82,13 @@ export default class WechatConnector implements SocialConnector {
|
|||
const { access_token: accessToken, openid } = result.data;
|
||||
|
||||
this.getAccessTokenErrorHandler(result.data);
|
||||
|
||||
assert(accessToken && openid, new ConnectorError(ConnectorErrorCodes.InvalidResponse));
|
||||
|
||||
return { accessToken, openid };
|
||||
};
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
public getUserInfo: GetUserInfo = async (data) => {
|
||||
const { code } = await this.authorizationCallbackHandler(data);
|
||||
const { accessToken, openid } = await this.getAccessToken(code);
|
||||
|
@ -110,10 +114,15 @@ export default class WechatConnector implements SocialConnector {
|
|||
|
||||
return { id: unionid ?? openid, avatar: headimgurl, name: nickname };
|
||||
} catch (error: unknown) {
|
||||
assert(
|
||||
!(error instanceof GotRequestError && error.response?.statusCode === 401),
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid)
|
||||
);
|
||||
if (error instanceof HTTPError) {
|
||||
const { statusCode, body: rawBody } = error.response;
|
||||
|
||||
if (statusCode === 401) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid);
|
||||
}
|
||||
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(rawBody));
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
@ -122,28 +131,28 @@ export default class WechatConnector implements SocialConnector {
|
|||
// See https://developers.weixin.qq.com/doc/oplatform/Return_codes/Return_code_descriptions_new.html
|
||||
private readonly getAccessTokenErrorHandler: GetAccessTokenErrorHandler = (accessToken) => {
|
||||
const { errcode, errmsg } = accessToken;
|
||||
assert(
|
||||
errcode !== 40_029,
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, errmsg)
|
||||
);
|
||||
assert(
|
||||
errcode !== 40_163,
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, errmsg)
|
||||
);
|
||||
assert(
|
||||
errcode !== 42_003,
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, errmsg)
|
||||
);
|
||||
assert(!errcode, new ConnectorError(ConnectorErrorCodes.General, errmsg));
|
||||
|
||||
if (errcode) {
|
||||
assert(
|
||||
!invalidAuthCodeErrcode.includes(errcode),
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, errmsg)
|
||||
);
|
||||
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, { errorDescription: errmsg, errcode });
|
||||
}
|
||||
};
|
||||
|
||||
private readonly getUserInfoErrorHandler: GetUserInfoErrorHandler = (userInfo) => {
|
||||
const { errcode, errmsg } = userInfo;
|
||||
assert(
|
||||
!(errcode === 40_001 || errcode === 40_014),
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid, errmsg)
|
||||
);
|
||||
assert(!errcode, new Error(errmsg ?? ''));
|
||||
|
||||
if (errcode) {
|
||||
assert(
|
||||
!invalidAccessTokenErrcode.includes(errcode),
|
||||
new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid, errmsg)
|
||||
);
|
||||
|
||||
throw new ConnectorError(ConnectorErrorCodes.General, { errorDescription: errmsg, errcode });
|
||||
}
|
||||
};
|
||||
|
||||
private readonly authorizationCallbackHandler = async (parameterObject: unknown) => {
|
||||
|
|
|
@ -167,7 +167,7 @@ describe('koaConnectorErrorHandler middleware', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('General connector errors', async () => {
|
||||
it('General connector errors with string type messages', async () => {
|
||||
const message = 'Mock General connector errors';
|
||||
const error = new ConnectorError(ConnectorErrorCodes.General, message);
|
||||
next.mockImplementationOnce(() => {
|
||||
|
@ -184,4 +184,23 @@ describe('koaConnectorErrorHandler middleware', () => {
|
|||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('General connector errors with message objects', async () => {
|
||||
const message = { errorCode: 400, errorDescription: 'Mock General connector errors' };
|
||||
const error = new ConnectorError(ConnectorErrorCodes.General, message);
|
||||
next.mockImplementationOnce(() => {
|
||||
throw error;
|
||||
});
|
||||
|
||||
await expect(koaConnectorErrorHandler()(ctx, next)).rejects.toMatchError(
|
||||
new RequestError(
|
||||
{
|
||||
code: 'connector.general',
|
||||
status: 500,
|
||||
errorDescription: '\nMock General connector errors',
|
||||
},
|
||||
message
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-types';
|
||||
import { Middleware } from 'koa';
|
||||
import { z } from 'zod';
|
||||
|
||||
import RequestError from '@/errors/RequestError';
|
||||
|
||||
|
@ -14,6 +15,10 @@ export default function koaConnectorErrorHandler<StateT, ContextT>(): Middleware
|
|||
|
||||
const { code, data } = error;
|
||||
|
||||
const errorDescriptionGuard = z.object({ errorDescription: z.string() });
|
||||
const result = errorDescriptionGuard.safeParse(data);
|
||||
const errorMessage = result.success ? result.data.errorDescription : undefined;
|
||||
|
||||
switch (code) {
|
||||
case ConnectorErrorCodes.InsufficientRequestParameters:
|
||||
throw new RequestError(
|
||||
|
@ -85,6 +90,7 @@ export default function koaConnectorErrorHandler<StateT, ContextT>(): Middleware
|
|||
{
|
||||
code: 'connector.general',
|
||||
status: 500,
|
||||
errorDescription: errorMessage ? '\n' + errorMessage : undefined,
|
||||
},
|
||||
data
|
||||
);
|
||||
|
|
|
@ -599,7 +599,7 @@ const errors = {
|
|||
unsupported_prompt_name: 'Unsupported prompt name.',
|
||||
},
|
||||
connector: {
|
||||
general: 'An unexpected error occurred in connector.',
|
||||
general: 'An unexpected error occurred in connector.{{errorDescription}}',
|
||||
not_found: 'Cannot find any available connector for type: {{type}}.',
|
||||
not_enabled: 'The connector is not enabled.',
|
||||
insufficient_request_parameters: 'The request might miss some input parameters.',
|
||||
|
|
|
@ -577,7 +577,7 @@ const errors = {
|
|||
unsupported_prompt_name: '不支持的 prompt name',
|
||||
},
|
||||
connector: {
|
||||
general: '连接器发生未知错误',
|
||||
general: '连接器发生未知错误{{errorDescription}}',
|
||||
not_found: '找不到可用的 {{type}} 类型的连接器',
|
||||
not_enabled: '连接器尚未启用',
|
||||
insufficient_request_parameters: '请求参数缺失',
|
||||
|
|
Loading…
Add table
Reference in a new issue