mirror of
https://github.com/logto-io/logto.git
synced 2025-02-17 22:04:19 -05:00
feat: expose zod error (#1474)
This commit is contained in:
parent
bb790ce4d1
commit
81b63f07bb
10 changed files with 56 additions and 22 deletions
|
@ -23,7 +23,7 @@ export default class AppleConnector implements SocialConnector {
|
||||||
const result = appleConfigGuard.safeParse(config);
|
const result = appleConfigGuard.safeParse(config);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error.message);
|
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -41,10 +41,13 @@ export enum ConnectorErrorCodes {
|
||||||
|
|
||||||
export class ConnectorError extends Error {
|
export class ConnectorError extends Error {
|
||||||
public code: ConnectorErrorCodes;
|
public code: ConnectorErrorCodes;
|
||||||
|
public data: unknown;
|
||||||
|
|
||||||
constructor(code: ConnectorErrorCodes, message?: string) {
|
constructor(code: ConnectorErrorCodes, data?: unknown) {
|
||||||
|
const message = typeof data === 'string' ? data : 'Connector error occurred.';
|
||||||
super(message);
|
super(message);
|
||||||
this.code = code;
|
this.code = code;
|
||||||
|
this.data = typeof data === 'string' ? { message: data } : data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,5 +31,6 @@ div.toast {
|
||||||
&.error {
|
&.error {
|
||||||
border: 1px solid var(--color-error);
|
border: 1px solid var(--color-error);
|
||||||
background-color: var(--color-danger-toast-background);
|
background-color: var(--color-danger-toast-background);
|
||||||
|
white-space: pre-line;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,9 @@ export class RequestError extends Error {
|
||||||
const toastError = async (response: Response) => {
|
const toastError = async (response: Response) => {
|
||||||
try {
|
try {
|
||||||
const data = await response.json<RequestErrorBody>();
|
const data = await response.json<RequestErrorBody>();
|
||||||
toast.error(data.message || t('admin_console.errors.unknown_server_error'));
|
toast.error(
|
||||||
|
[data.message, data.details].join('\n') || t('admin_console.errors.unknown_server_error')
|
||||||
|
);
|
||||||
} catch {
|
} catch {
|
||||||
toast.error(t('admin_console.errors.unknown_server_error'));
|
toast.error(t('admin_console.errors.unknown_server_error'));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,22 @@
|
||||||
import { LogtoErrorCode, LogtoErrorI18nKey } from '@logto/phrases';
|
import { LogtoErrorCode, LogtoErrorI18nKey } from '@logto/phrases';
|
||||||
import { RequestErrorBody, RequestErrorMetadata } from '@logto/schemas';
|
import { RequestErrorBody, RequestErrorMetadata } from '@logto/schemas';
|
||||||
|
import { conditional, Optional } from '@silverhand/essentials';
|
||||||
import i18next from 'i18next';
|
import i18next from 'i18next';
|
||||||
import pick from 'lodash.pick';
|
import pick from 'lodash.pick';
|
||||||
|
import { ZodError } from 'zod';
|
||||||
|
|
||||||
|
const formatZodError = ({ issues }: ZodError): string[] =>
|
||||||
|
issues.map((issue) => {
|
||||||
|
const base = `Error in key path "${issue.path.map((node) => String(node)).join('.')}": (${
|
||||||
|
issue.code
|
||||||
|
}) `;
|
||||||
|
|
||||||
|
if (issue.code === 'invalid_type') {
|
||||||
|
return base + `Expected ${issue.expected} but received ${issue.received}.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return base + issue.message;
|
||||||
|
});
|
||||||
export default class RequestError extends Error {
|
export default class RequestError extends Error {
|
||||||
code: LogtoErrorCode;
|
code: LogtoErrorCode;
|
||||||
status: number;
|
status: number;
|
||||||
|
@ -27,6 +41,10 @@ export default class RequestError extends Error {
|
||||||
}
|
}
|
||||||
|
|
||||||
get body(): RequestErrorBody {
|
get body(): RequestErrorBody {
|
||||||
return pick(this, 'message', 'code', 'data');
|
return pick(this, 'message', 'code', 'data', 'details');
|
||||||
|
}
|
||||||
|
|
||||||
|
get details(): Optional<string> {
|
||||||
|
return conditional(this.data instanceof ZodError && formatZodError(this.data).join('\n'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,10 +12,7 @@ export default function koaConnectorErrorHandler<StateT, ContextT>(): Middleware
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { code, message } = error;
|
const { code, data } = error;
|
||||||
|
|
||||||
// Original OIDCProvider Error description and details are provided in the data field
|
|
||||||
const data = { message };
|
|
||||||
|
|
||||||
switch (code) {
|
switch (code) {
|
||||||
case ConnectorErrorCodes.InsufficientRequestParameters:
|
case ConnectorErrorCodes.InsufficientRequestParameters:
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { has } from '@silverhand/essentials';
|
import { has, Optional } from '@silverhand/essentials';
|
||||||
import { MiddlewareType } from 'koa';
|
import { MiddlewareType } from 'koa';
|
||||||
import koaBody from 'koa-body';
|
import koaBody from 'koa-body';
|
||||||
import { IMiddleware, IRouterParamContext } from 'koa-router';
|
import { IMiddleware, IRouterParamContext } from 'koa-router';
|
||||||
|
@ -41,6 +41,18 @@ export const isGuardMiddleware = <Type extends IMiddleware>(
|
||||||
): function_ is WithGuardConfig<Type> =>
|
): function_ is WithGuardConfig<Type> =>
|
||||||
function_.name === 'guardMiddleware' && has(function_, 'config');
|
function_.name === 'guardMiddleware' && has(function_, 'config');
|
||||||
|
|
||||||
|
const tryParse = <Output, Definition, Input>(
|
||||||
|
type: 'query' | 'body' | 'params',
|
||||||
|
guard: Optional<ZodType<Output, Definition, Input>>,
|
||||||
|
data: unknown
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
return guard?.parse(data);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
throw new RequestError({ code: 'guard.invalid_input', type }, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default function koaGuard<
|
export default function koaGuard<
|
||||||
StateT,
|
StateT,
|
||||||
ContextT extends IRouterParamContext,
|
ContextT extends IRouterParamContext,
|
||||||
|
@ -62,16 +74,12 @@ export default function koaGuard<
|
||||||
WithGuardedContext<ContextT, GuardQueryT, GuardBodyT, GuardParametersT>,
|
WithGuardedContext<ContextT, GuardQueryT, GuardBodyT, GuardParametersT>,
|
||||||
ResponseBodyT
|
ResponseBodyT
|
||||||
> = async (ctx, next) => {
|
> = async (ctx, next) => {
|
||||||
try {
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
ctx.guard = {
|
||||||
ctx.guard = {
|
query: tryParse('query', query, ctx.request.query),
|
||||||
query: query?.parse(ctx.request.query),
|
body: tryParse('body', body, ctx.request.body),
|
||||||
body: body?.parse(ctx.request.body),
|
params: tryParse('params', params, ctx.params),
|
||||||
params: params?.parse(ctx.params),
|
} as Guarded<GuardQueryT, GuardBodyT, GuardParametersT>; // Have to do this since it's too complicated for TS
|
||||||
} as Guarded<GuardQueryT, GuardBodyT, GuardParametersT>; // Have to do this since it's too complicated for TS
|
|
||||||
} catch (error: unknown) {
|
|
||||||
throw new RequestError('guard.invalid_input', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
};
|
};
|
||||||
|
|
|
@ -546,7 +546,7 @@ const errors = {
|
||||||
jwt_sub_missing: 'Missing `sub` in JWT.',
|
jwt_sub_missing: 'Missing `sub` in JWT.',
|
||||||
},
|
},
|
||||||
guard: {
|
guard: {
|
||||||
invalid_input: 'The request input is invalid.',
|
invalid_input: 'The request {{type}} is invalid.',
|
||||||
invalid_pagination: 'The request pagination value is invalid.',
|
invalid_pagination: 'The request pagination value is invalid.',
|
||||||
},
|
},
|
||||||
oidc: {
|
oidc: {
|
||||||
|
|
|
@ -524,7 +524,7 @@ const errors = {
|
||||||
jwt_sub_missing: 'JWT 缺失 `sub`',
|
jwt_sub_missing: 'JWT 缺失 `sub`',
|
||||||
},
|
},
|
||||||
guard: {
|
guard: {
|
||||||
invalid_input: '请求输入无效',
|
invalid_input: '请求中 {{type}} 无效',
|
||||||
invalid_pagination: '分页参数无效',
|
invalid_pagination: '分页参数无效',
|
||||||
},
|
},
|
||||||
oidc: {
|
oidc: {
|
||||||
|
|
|
@ -6,4 +6,9 @@ export type RequestErrorMetadata = Record<string, unknown> & {
|
||||||
expose?: boolean;
|
expose?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RequestErrorBody = { message: string; data: unknown; code: LogtoErrorCode };
|
export type RequestErrorBody = {
|
||||||
|
message: string;
|
||||||
|
data: unknown;
|
||||||
|
code: LogtoErrorCode;
|
||||||
|
details?: string;
|
||||||
|
};
|
||||||
|
|
Loading…
Add table
Reference in a new issue