0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00
logto/packages/connectors/connector-feishu-web/src/index.ts

199 lines
5.2 KiB
TypeScript

import { assert, conditional } from '@silverhand/essentials';
import { got, HTTPError } from 'got';
import type {
CreateConnector,
GetAuthorizationUri,
GetConnectorConfig,
GetUserInfo,
SocialConnector,
} from '@logto/connector-kit';
import {
ConnectorError,
ConnectorErrorCodes,
ConnectorPlatform,
ConnectorType,
jsonGuard,
validateConfig,
} from '@logto/connector-kit';
import {
accessTokenEndpoint,
codeEndpoint,
defaultMetadata,
userInfoEndpoint,
} from './constant.js';
import {
feishuAccessTokenResponse,
feishuAuthCodeGuard,
feishuConfigGuard,
feishuErrorResponse,
feishuUserInfoResponse,
} from './types.js';
export function buildAuthorizationUri(
clientId: string,
redirectUri: string,
state: string
): string {
const queryParameters = new URLSearchParams({
client_id: clientId,
redirect_uri: encodeURI(redirectUri),
response_type: 'code',
state,
});
return `${codeEndpoint}?${queryParameters.toString()}`;
}
export function getAuthorizationUri(getConfig: GetConnectorConfig): GetAuthorizationUri {
return async function ({ state, redirectUri }) {
const config = await getConfig(defaultMetadata.id);
validateConfig(config, feishuConfigGuard);
const { appId } = config;
return buildAuthorizationUri(appId, redirectUri, state);
};
}
export async function authorizationCallbackHandler(data: unknown) {
const result = feishuAuthCodeGuard.safeParse(data);
assert(
result.success,
new ConnectorError(ConnectorErrorCodes.InvalidResponse, JSON.stringify(data))
);
return result.data;
}
export async function getAccessToken(
code: string,
appId: string,
appSecret: string,
redirectUri: string
) {
try {
const response = await got.post(accessTokenEndpoint, {
headers: {
contentType: 'application/www-form-urlencoded',
},
form: {
grant_type: 'authorization_code',
code,
client_id: appId,
client_secret: appSecret,
redirect_uri: redirectUri,
},
responseType: 'json',
});
const result = feishuAccessTokenResponse.safeParse(response.body);
assert(
result.success,
new ConnectorError(ConnectorErrorCodes.InvalidResponse, JSON.stringify(response.body))
);
if (result.data.access_token.length === 0) {
throw new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, 'access_token is empty');
}
return { accessToken: result.data.access_token };
} catch (error: unknown) {
if (error instanceof ConnectorError) {
throw error;
}
if (error instanceof HTTPError) {
const result = feishuErrorResponse.safeParse(error.response.body);
assert(
result.success,
new ConnectorError(ConnectorErrorCodes.InvalidResponse, JSON.stringify(error.response.body))
);
throw new ConnectorError(
ConnectorErrorCodes.SocialAuthCodeInvalid,
result.data.error_description
);
}
throw new ConnectorError(ConnectorErrorCodes.General, {
errorDescription: 'Failed to get access token',
});
}
}
export function getUserInfo(getConfig: GetConnectorConfig): GetUserInfo {
return async function (data) {
const { code, redirectUri } = await authorizationCallbackHandler(data);
const config = await getConfig(defaultMetadata.id);
validateConfig(config, feishuConfigGuard);
const { accessToken } = await getAccessToken(code, config.appId, config.appSecret, redirectUri);
try {
const response = await got.get(userInfoEndpoint, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
responseType: 'json',
});
const result = feishuUserInfoResponse.safeParse(response.body);
assert(
result.success,
new ConnectorError(ConnectorErrorCodes.InvalidResponse, `invalid user response`)
);
const { sub, user_id, name, email, avatar_url: avatar, mobile } = result.data;
return {
id: sub,
name,
avatar,
email: conditional(email),
userId: conditional(user_id),
phone: conditional(mobile?.replace('+', '')),
rawData: jsonGuard.parse(response.body),
};
} catch (error: unknown) {
if (error instanceof ConnectorError) {
throw error;
}
if (error instanceof HTTPError) {
const result = feishuErrorResponse.safeParse(error.response.body);
assert(
result.success,
new ConnectorError(
ConnectorErrorCodes.InvalidResponse,
JSON.stringify(error.response.body)
)
);
throw new ConnectorError(
ConnectorErrorCodes.SocialAccessTokenInvalid,
result.data.error_description
);
}
throw new ConnectorError(ConnectorErrorCodes.General, {
errorDescription: 'Failed to get user info',
});
}
};
}
const createFeishuConnector: CreateConnector<SocialConnector> = async ({ getConfig }) => {
return {
metadata: defaultMetadata,
type: ConnectorType.Social,
platform: ConnectorPlatform.Web,
configGuard: feishuConfigGuard,
getAuthorizationUri: getAuthorizationUri(getConfig),
getUserInfo: getUserInfo(getConfig),
};
};
export default createFeishuConnector;