mirror of
https://github.com/logto-io/logto.git
synced 2025-03-24 22:41:28 -05:00
feat(connector): connector queries and APIs (#178)
* feat(connector): connector queries and APIs * chore(connectors): remove type from DB schema design and fix code accordingly * chore(connectors): put connector as ConnectorInstance's property * chore(connector): put connector as optional property of ConnectorInstance
This commit is contained in:
parent
b9aec921c2
commit
9dc0ea32c0
12 changed files with 142 additions and 45 deletions
packages
core/src
connectors
queries
routes
schemas
|
@ -1,4 +1,3 @@
|
|||
import { ConnectorType } from '@logto/schemas';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
|
@ -7,6 +6,7 @@ import {
|
|||
ConnectorMetadata,
|
||||
EmailSendMessageFunction,
|
||||
ValidateConfig,
|
||||
ConnectorType,
|
||||
} from '../types';
|
||||
import { getConnectorConfig } from '../utilities';
|
||||
import { singleSendMail } from './single-send-mail';
|
||||
|
@ -54,10 +54,7 @@ const configGuard = z.object({
|
|||
export type AliyunDmConfig = z.infer<typeof configGuard>;
|
||||
|
||||
export const sendMessage: EmailSendMessageFunction = async (address, type, data) => {
|
||||
const config: AliyunDmConfig = await getConnectorConfig<AliyunDmConfig>(
|
||||
metadata.id,
|
||||
metadata.type
|
||||
);
|
||||
const config = await getConnectorConfig<AliyunDmConfig>(metadata.id);
|
||||
const template = config.templates.find((template) => template.type === type);
|
||||
|
||||
if (!template) {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { ConnectorType } from '@logto/schemas';
|
||||
import got from 'got';
|
||||
import { stringify } from 'query-string';
|
||||
import { z } from 'zod';
|
||||
|
@ -10,6 +9,7 @@ import {
|
|||
GetAuthorizationUri,
|
||||
ValidateConfig,
|
||||
GetUserInfo,
|
||||
ConnectorType,
|
||||
} from '../types';
|
||||
import { getConnectorConfig } from '../utilities';
|
||||
import { authorizationEndpoint, accessTokenEndpoint, scope, userInfoEndpoint } from './constant';
|
||||
|
@ -47,7 +47,7 @@ export const validateConfig: ValidateConfig = async (config: unknown) => {
|
|||
};
|
||||
|
||||
export const getAuthorizationUri: GetAuthorizationUri = async (redirectUri, state) => {
|
||||
const config = await getConnectorConfig<GithubConfig>(metadata.id, metadata.type);
|
||||
const config = await getConnectorConfig<GithubConfig>(metadata.id);
|
||||
return `${authorizationEndpoint}?${stringify({
|
||||
client_id: config.clientId,
|
||||
redirect_uri: redirectUri,
|
||||
|
@ -64,7 +64,7 @@ export const getAccessToken: GetAccessToken = async (code) => {
|
|||
};
|
||||
|
||||
const { clientId: client_id, clientSecret: client_secret } =
|
||||
await getConnectorConfig<GithubConfig>(metadata.id, metadata.type);
|
||||
await getConnectorConfig<GithubConfig>(metadata.id);
|
||||
const { access_token: accessToken } = await got
|
||||
.post({
|
||||
url: accessTokenEndpoint,
|
||||
|
|
|
@ -1,25 +1,44 @@
|
|||
import { findConnectorByIdAndType, insertConnector } from '@/queries/connector';
|
||||
import RequestError from '@/errors/RequestError';
|
||||
import { findConnectorById, insertConnector } from '@/queries/connector';
|
||||
|
||||
import * as AliyunDM from './aliyun-dm';
|
||||
import { ConnectorInstance } from './types';
|
||||
|
||||
const connectors: ConnectorInstance[] = [AliyunDM];
|
||||
const allConnectors: ConnectorInstance[] = [AliyunDM];
|
||||
|
||||
export const getConnectorById = (id: string): ConnectorInstance | null => {
|
||||
return connectors.find((connector) => connector.metadata.id === id) ?? null;
|
||||
export const getConnectorInstances = async (): Promise<ConnectorInstance[]> => {
|
||||
return Promise.all(
|
||||
allConnectors.map(async (element) => {
|
||||
const connector = await findConnectorById(element.metadata.id);
|
||||
return { connector, ...element };
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const getConnectorInstanceById = async (id: string): Promise<ConnectorInstance> => {
|
||||
const found = allConnectors.find((element) => element.metadata.id === id);
|
||||
if (!found) {
|
||||
throw new RequestError({
|
||||
code: 'entity.not_found',
|
||||
id,
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
|
||||
const connector = await findConnectorById(id);
|
||||
return { connector, ...found };
|
||||
};
|
||||
|
||||
export const initConnectors = async () => {
|
||||
await Promise.all(
|
||||
connectors.map(async ({ metadata: { id, type } }) => {
|
||||
const record = await findConnectorByIdAndType(id, type);
|
||||
allConnectors.map(async ({ metadata: { id } }) => {
|
||||
const record = await findConnectorById(id);
|
||||
if (record) {
|
||||
return;
|
||||
}
|
||||
|
||||
await insertConnector({
|
||||
id,
|
||||
type,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import { Languages } from '@logto/phrases';
|
||||
import { ConnectorConfig, ConnectorType } from '@logto/schemas';
|
||||
import { ConnectorConfig, Connector } from '@logto/schemas';
|
||||
|
||||
export enum ConnectorType {
|
||||
SMS = 'SMS',
|
||||
Email = 'Email',
|
||||
Social = 'Social',
|
||||
}
|
||||
export interface ConnectorMetadata {
|
||||
id: string;
|
||||
type: ConnectorType;
|
||||
|
@ -13,6 +18,7 @@ export interface ConnectorMetadata {
|
|||
export type ConnectorInstance = EmailConector | SocialConector;
|
||||
|
||||
export interface BaseConnector {
|
||||
connector?: Connector;
|
||||
metadata: ConnectorMetadata;
|
||||
validateConfig: ValidateConfig;
|
||||
}
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
import { ConnectorConfig, ConnectorType } from '@logto/schemas';
|
||||
import { ConnectorConfig } from '@logto/schemas';
|
||||
|
||||
import RequestError from '@/errors/RequestError';
|
||||
import { findConnectorByIdAndType, updateConnector } from '@/queries/connector';
|
||||
import { findConnectorById, updateConnector } from '@/queries/connector';
|
||||
|
||||
export const getConnectorConfig = async <T extends ConnectorConfig>(
|
||||
id: string,
|
||||
type: ConnectorType
|
||||
): Promise<T> => {
|
||||
const connector = await findConnectorByIdAndType(id, type);
|
||||
export const getConnectorConfig = async <T extends ConnectorConfig>(id: string): Promise<T> => {
|
||||
const connector = await findConnectorById(id);
|
||||
if (!connector) {
|
||||
throw new RequestError({
|
||||
code: 'entity.not_exists_with_id',
|
||||
|
@ -22,11 +19,10 @@ export const getConnectorConfig = async <T extends ConnectorConfig>(
|
|||
|
||||
export const updateConnectorConfig = async <T extends ConnectorConfig>(
|
||||
id: string,
|
||||
type: ConnectorType,
|
||||
config: T
|
||||
): Promise<void> => {
|
||||
await updateConnector({
|
||||
where: { id, type },
|
||||
where: { id },
|
||||
set: { config },
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Connector, CreateConnector, Connectors, ConnectorType } from '@logto/schemas';
|
||||
import { Connector, CreateConnector, Connectors } from '@logto/schemas';
|
||||
import { sql } from 'slonik';
|
||||
|
||||
import { buildInsertInto } from '@/database/insert-into';
|
||||
|
@ -8,11 +8,17 @@ import { convertToIdentifiers } from '@/database/utils';
|
|||
|
||||
const { table, fields } = convertToIdentifiers(Connectors);
|
||||
|
||||
export const findConnectorByIdAndType = async (id: string, type: ConnectorType) =>
|
||||
pool.maybeOne<Connector>(sql`
|
||||
export const findAllConnectors = async () =>
|
||||
pool.many<Connector>(sql`
|
||||
select ${sql.join(Object.values(fields), sql`, `)}
|
||||
from ${table}
|
||||
where ${fields.id}=${id} and ${fields.type}=${type}
|
||||
`);
|
||||
|
||||
export const findConnectorById = async (id: string) =>
|
||||
pool.one<Connector>(sql`
|
||||
select ${sql.join(Object.values(fields), sql`, `)}
|
||||
from ${table}
|
||||
where ${fields.id}=${id}
|
||||
`);
|
||||
|
||||
export const insertConnector = buildInsertInto<CreateConnector, Connector>(pool, Connectors, {
|
||||
|
|
84
packages/core/src/routes/connector.ts
Normal file
84
packages/core/src/routes/connector.ts
Normal file
|
@ -0,0 +1,84 @@
|
|||
import { Connectors } from '@logto/schemas';
|
||||
import { object, string } from 'zod';
|
||||
|
||||
import { getConnectorInstances, getConnectorInstanceById } from '@/connectors';
|
||||
import { ConnectorInstance } from '@/connectors/types';
|
||||
import koaGuard from '@/middleware/koa-guard';
|
||||
import { findConnectorById, updateConnector } from '@/queries/connector';
|
||||
|
||||
import { AuthedRouter } from './types';
|
||||
|
||||
const transpileConnectorInstance = ({ connector, metadata }: ConnectorInstance) => ({
|
||||
...connector,
|
||||
metadata,
|
||||
});
|
||||
|
||||
export default function connectorRoutes<T extends AuthedRouter>(router: T) {
|
||||
router.get('/connectors', async (ctx, next) => {
|
||||
const connectorInstances = await getConnectorInstances();
|
||||
ctx.body = connectorInstances.map((connectorInstance) => {
|
||||
return transpileConnectorInstance(connectorInstance);
|
||||
});
|
||||
|
||||
return next();
|
||||
});
|
||||
|
||||
router.get(
|
||||
'/connectors/:id',
|
||||
koaGuard({ params: object({ id: string().min(1) }) }),
|
||||
async (ctx, next) => {
|
||||
const {
|
||||
params: { id },
|
||||
} = ctx.guard;
|
||||
const connectorInstance = await getConnectorInstanceById(id);
|
||||
ctx.body = transpileConnectorInstance(connectorInstance);
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.patch(
|
||||
'/connectors/:id/enabled',
|
||||
koaGuard({
|
||||
params: object({ id: string().min(1) }),
|
||||
body: Connectors.createGuard.pick({ enabled: true }),
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const {
|
||||
params: { id },
|
||||
body: { enabled },
|
||||
} = ctx.guard;
|
||||
await findConnectorById(id);
|
||||
await updateConnector({ set: { enabled }, where: { id } });
|
||||
ctx.body = { enabled };
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.patch(
|
||||
'/connectors/:id',
|
||||
koaGuard({
|
||||
params: object({ id: string().min(1) }),
|
||||
body: Connectors.createGuard
|
||||
.omit({ id: true, type: true, enabled: true, createdAt: true })
|
||||
.partial(),
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const {
|
||||
params: { id },
|
||||
body,
|
||||
} = ctx.guard;
|
||||
const connectorInstance = await getConnectorInstanceById(id);
|
||||
|
||||
if (body.config) {
|
||||
await connectorInstance.validateConfig(body.config);
|
||||
}
|
||||
|
||||
await updateConnector({ set: body, where: { id } });
|
||||
ctx.body = transpileConnectorInstance(await getConnectorInstanceById(id));
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
}
|
|
@ -5,6 +5,7 @@ import { Provider } from 'oidc-provider';
|
|||
|
||||
import koaAuth from '@/middleware/koa-auth';
|
||||
import applicationRoutes from '@/routes/application';
|
||||
import connectorRoutes from '@/routes/connector';
|
||||
import resourceRoutes from '@/routes/resource';
|
||||
import sessionRoutes from '@/routes/session';
|
||||
import settingRoutes from '@/routes/setting';
|
||||
|
@ -26,6 +27,7 @@ const createRouters = (provider: Provider) => {
|
|||
router.use(koaAuth());
|
||||
applicationRoutes(router);
|
||||
settingRoutes(router);
|
||||
connectorRoutes(router);
|
||||
resourceRoutes(router);
|
||||
|
||||
return [anonymousRouter, router];
|
||||
|
|
|
@ -3,12 +3,10 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { ConnectorConfig, connectorConfigGuard, GeneratedSchema, Guard } from '../foundations';
|
||||
import { ConnectorType } from './custom-types';
|
||||
|
||||
export type CreateConnector = {
|
||||
id: string;
|
||||
enabled?: boolean;
|
||||
type: ConnectorType;
|
||||
config?: ConnectorConfig;
|
||||
createdAt?: number;
|
||||
};
|
||||
|
@ -16,7 +14,6 @@ export type CreateConnector = {
|
|||
export type Connector = {
|
||||
id: string;
|
||||
enabled: boolean;
|
||||
type: ConnectorType;
|
||||
config: ConnectorConfig;
|
||||
createdAt: number;
|
||||
};
|
||||
|
@ -24,7 +21,6 @@ export type Connector = {
|
|||
const createGuard: Guard<CreateConnector> = z.object({
|
||||
id: z.string(),
|
||||
enabled: z.boolean().optional(),
|
||||
type: z.nativeEnum(ConnectorType),
|
||||
config: connectorConfigGuard.optional(),
|
||||
createdAt: z.number().optional(),
|
||||
});
|
||||
|
@ -35,10 +31,9 @@ export const Connectors: GeneratedSchema<CreateConnector> = Object.freeze({
|
|||
fields: {
|
||||
id: 'id',
|
||||
enabled: 'enabled',
|
||||
type: 'type',
|
||||
config: 'config',
|
||||
createdAt: 'created_at',
|
||||
},
|
||||
fieldKeys: ['id', 'enabled', 'type', 'config', 'createdAt'],
|
||||
fieldKeys: ['id', 'enabled', 'config', 'createdAt'],
|
||||
createGuard,
|
||||
});
|
||||
|
|
|
@ -5,11 +5,6 @@ export enum ApplicationType {
|
|||
SPA = 'SPA',
|
||||
Traditional = 'Traditional',
|
||||
}
|
||||
export enum ConnectorType {
|
||||
SMS = 'SMS',
|
||||
Email = 'Email',
|
||||
Social = 'Social',
|
||||
}
|
||||
export enum UserLogType {
|
||||
SignInUsernameAndPassword = 'SignInUsernameAndPassword',
|
||||
ExchangeAccessToken = 'ExchangeAccessToken',
|
||||
|
|
|
@ -45,7 +45,7 @@ export const userLogPayloadGuard = z.object({
|
|||
export type UserLogPayload = z.infer<typeof userLogPayloadGuard>;
|
||||
|
||||
// TODO: support empty shape of object
|
||||
export const connectorConfigGuard = z.object({});
|
||||
export const connectorConfigGuard = z.object({}).catchall(z.unknown());
|
||||
|
||||
export type ConnectorConfig = z.infer<typeof connectorConfigGuard>;
|
||||
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
create type connector_type as enum ('SMS', 'Email', 'Social');
|
||||
|
||||
create table connectors (
|
||||
id varchar(128) not null,
|
||||
enabled boolean not null default TRUE,
|
||||
type connector_type not null,
|
||||
config jsonb /* @use ConnectorConfig */ not null default '{}'::jsonb,
|
||||
created_at timestamptz not null default(now()),
|
||||
primary key (id, type)
|
||||
primary key (id)
|
||||
);
|
||||
|
|
Loading…
Add table
Reference in a new issue