mirror of
https://github.com/logto-io/logto.git
synced 2025-04-07 23:01:25 -05:00
refactor(core): decouple passwordless verification flow w/o fixing tests
This commit is contained in:
parent
ee2e3f7e1d
commit
6f18d890ca
11 changed files with 269 additions and 104 deletions
|
@ -0,0 +1,70 @@
|
|||
import dayjs from 'dayjs';
|
||||
import { Provider } from 'oidc-provider';
|
||||
|
||||
import RequestError from '@/errors/RequestError';
|
||||
import { assignInteractionResults } from '@/lib/session';
|
||||
import { generateUserId, insertUser } from '@/lib/user';
|
||||
import koaLog from '@/middleware/koa-log';
|
||||
import { hasUserWithPhone, hasUserWithEmail } from '@/queries/user';
|
||||
import { passwordlessVerificationGuard, Via } from '@/routes/session/types';
|
||||
import { getPasswordlessRelatedLogType } from '@/routes/session/utils';
|
||||
import assertThat from '@/utils/assert-that';
|
||||
|
||||
type MiddlewareReturnType = ReturnType<typeof koaLog>;
|
||||
|
||||
export default function koaRegisterAction<StateT, ContextT, ResponseBodyT>(
|
||||
provider: Provider,
|
||||
via: Via
|
||||
): MiddlewareReturnType {
|
||||
return async (ctx, next) => {
|
||||
const { result } = await provider.interactionDetails(ctx.req, ctx.res);
|
||||
|
||||
const passwordlessVerificationResult = passwordlessVerificationGuard.safeParse(result);
|
||||
assertThat(
|
||||
passwordlessVerificationResult.success,
|
||||
new RequestError({
|
||||
code: 'session.passwordless_verification_session_not_found',
|
||||
status: 404,
|
||||
})
|
||||
);
|
||||
|
||||
const {
|
||||
passwordlessVerification: { email, phone, flow, expiresAt },
|
||||
} = passwordlessVerificationResult.data;
|
||||
|
||||
const type = getPasswordlessRelatedLogType('register', via);
|
||||
ctx.log(type, { email, phone, flow, expiresAt });
|
||||
|
||||
assertThat(
|
||||
flow === 'register',
|
||||
new RequestError({ code: 'session.passwordless_not_verified', status: 401 })
|
||||
);
|
||||
|
||||
assertThat(
|
||||
dayjs(expiresAt).isValid() && dayjs(expiresAt).isAfter(dayjs()),
|
||||
new RequestError({ code: 'session.passwordless_verification_expired', status: 401 })
|
||||
);
|
||||
|
||||
if (via === 'sms') {
|
||||
assertThat(
|
||||
phone && !(await hasUserWithPhone(phone)),
|
||||
new RequestError({ code: 'user.phone_exists_register', status: 422 })
|
||||
);
|
||||
} else {
|
||||
assertThat(
|
||||
email && !(await hasUserWithEmail(email)),
|
||||
new RequestError({ code: 'user.email_exists_register', status: 422 })
|
||||
);
|
||||
}
|
||||
|
||||
const id = await generateUserId();
|
||||
ctx.log(type, { userId: id });
|
||||
|
||||
await (via === 'sms'
|
||||
? insertUser({ id, primaryPhone: phone, lastSignInAt: Date.now() })
|
||||
: insertUser({ id, primaryEmail: email, lastSignInAt: Date.now() }));
|
||||
await assignInteractionResults(ctx, provider, { login: { accountId: id } }, true);
|
||||
|
||||
return next();
|
||||
};
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
import { User } from '@logto/schemas';
|
||||
import dayjs from 'dayjs';
|
||||
import { Provider } from 'oidc-provider';
|
||||
|
||||
import RequestError from '@/errors/RequestError';
|
||||
import { assignInteractionResults } from '@/lib/session';
|
||||
import koaLog from '@/middleware/koa-log';
|
||||
import {
|
||||
updateUserById,
|
||||
hasUserWithPhone,
|
||||
findUserByPhone,
|
||||
hasUserWithEmail,
|
||||
findUserByEmail,
|
||||
} from '@/queries/user';
|
||||
import { passwordlessVerificationGuard, Via } from '@/routes/session/types';
|
||||
import { getPasswordlessRelatedLogType } from '@/routes/session/utils';
|
||||
import assertThat from '@/utils/assert-that';
|
||||
|
||||
type MiddlewareReturnType = ReturnType<typeof koaLog>;
|
||||
|
||||
export default function koaSignInAction<StateT, ContextT, ResponseBodyT>(
|
||||
provider: Provider,
|
||||
via: Via
|
||||
): MiddlewareReturnType {
|
||||
return async (ctx, next) => {
|
||||
const { result } = await provider.interactionDetails(ctx.req, ctx.res);
|
||||
|
||||
const passwordlessVerificationResult = passwordlessVerificationGuard.safeParse(result);
|
||||
assertThat(
|
||||
passwordlessVerificationResult.success,
|
||||
new RequestError({
|
||||
code: 'session.passwordless_verification_session_not_found',
|
||||
status: 404,
|
||||
})
|
||||
);
|
||||
|
||||
const {
|
||||
passwordlessVerification: { email, phone, flow, expiresAt },
|
||||
} = passwordlessVerificationResult.data;
|
||||
|
||||
const type = getPasswordlessRelatedLogType('sign-in', via);
|
||||
ctx.log(type, { email, phone, flow, expiresAt });
|
||||
|
||||
assertThat(
|
||||
flow === 'sign-in',
|
||||
new RequestError({ code: 'session.passwordless_not_verified', status: 401 })
|
||||
);
|
||||
|
||||
assertThat(
|
||||
dayjs(expiresAt).isValid() && dayjs(expiresAt).isAfter(dayjs()),
|
||||
new RequestError({ code: 'session.passwordless_verification_expired', status: 401 })
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @silverhand/fp/no-let
|
||||
let user: User;
|
||||
|
||||
if (via === 'sms') {
|
||||
assertThat(
|
||||
phone && (await hasUserWithPhone(phone)),
|
||||
new RequestError({ code: 'user.phone_not_exists', status: 422 })
|
||||
);
|
||||
// eslint-disable-next-line @silverhand/fp/no-mutation
|
||||
user = await findUserByPhone(phone);
|
||||
} else {
|
||||
assertThat(
|
||||
email && (await hasUserWithEmail(email)),
|
||||
new RequestError({ code: 'user.email_not_exists', status: 422 })
|
||||
);
|
||||
// eslint-disable-next-line @silverhand/fp/no-mutation
|
||||
user = await findUserByEmail(email);
|
||||
}
|
||||
|
||||
const { id } = user;
|
||||
ctx.log(type, { userId: id });
|
||||
|
||||
await updateUserById(id, { lastSignInAt: Date.now() });
|
||||
await assignInteractionResults(ctx, provider, { login: { accountId: id } }, true);
|
||||
|
||||
return next();
|
||||
};
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
import { emailRegEx, phoneRegEx } from '@logto/core-kit';
|
||||
import { PasscodeType, LogType, User } from '@logto/schemas';
|
||||
import camelcase from 'camelcase';
|
||||
import { PasscodeType } from '@logto/schemas';
|
||||
import dayjs from 'dayjs';
|
||||
import { Provider } from 'oidc-provider';
|
||||
import { object, string } from 'zod';
|
||||
|
@ -21,8 +20,10 @@ import assertThat from '@/utils/assert-that';
|
|||
|
||||
import { AnonymousRouter } from '../types';
|
||||
import { passwordlessVerificationTimeout } from './consts';
|
||||
import { flowTypeGuard, viaGuard, passwordlessVerificationGuard, PasscodePayload } from './types';
|
||||
import { getRoutePrefix, getPasscodeType } from './utils';
|
||||
import { flowTypeGuard, viaGuard, PasscodePayload } from './types';
|
||||
import { getRoutePrefix, getPasscodeType, getPasswordlessRelatedLogType } from './utils';
|
||||
// Import koaSignInAction from './middleware/koa-sign-in-action';
|
||||
// import koaRegisterAction from './middleware/koa-register-action';
|
||||
|
||||
export const registerRoute = getRoutePrefix('register', 'passwordless');
|
||||
export const signInRoute = getRoutePrefix('sign-in', 'passwordless');
|
||||
|
@ -31,6 +32,11 @@ export default function passwordlessRoutes<T extends AnonymousRouter>(
|
|||
router: T,
|
||||
provider: Provider
|
||||
) {
|
||||
// Router.use(`${signInRoute}/sms`, koaSignInAction(provider, 'sms'));
|
||||
// router.use(`${signInRoute}/email`, koaSignInAction(provider, 'email'));
|
||||
// router.use(`${registerRoute}/sms`, koaRegisterAction(provider, 'sms'));
|
||||
// router.use(`${registerRoute}/email`, koaRegisterAction(provider, 'email'));
|
||||
|
||||
router.post(
|
||||
`/passwordless/:via/send`,
|
||||
koaGuard({
|
||||
|
@ -61,9 +67,7 @@ export default function passwordlessRoutes<T extends AnonymousRouter>(
|
|||
payload = { phone };
|
||||
}
|
||||
|
||||
const type: LogType = `${camelcase(flow, { pascalCase: true })}${camelcase(via, {
|
||||
pascalCase: true,
|
||||
})}SendPasscode`;
|
||||
const type = getPasswordlessRelatedLogType(flow, via, 'send');
|
||||
ctx.log(type, payload);
|
||||
|
||||
const passcodeType = getPasscodeType(flow);
|
||||
|
@ -107,9 +111,7 @@ export default function passwordlessRoutes<T extends AnonymousRouter>(
|
|||
payload = { phone };
|
||||
}
|
||||
|
||||
const type: LogType = `${camelcase(flow, { pascalCase: true })}${camelcase(via, {
|
||||
pascalCase: true,
|
||||
})}`;
|
||||
const type = getPasswordlessRelatedLogType(flow, via, 'verify');
|
||||
ctx.log(type, payload);
|
||||
|
||||
const passcodeType = getPasscodeType(flow);
|
||||
|
@ -129,58 +131,44 @@ export default function passwordlessRoutes<T extends AnonymousRouter>(
|
|||
);
|
||||
|
||||
router.post(
|
||||
`${signInRoute}/:via`,
|
||||
koaGuard({ params: object({ via: viaGuard }) }),
|
||||
`${signInRoute}/sms/send-passcode`,
|
||||
koaGuard({ body: object({ phone: string().regex(phoneRegEx) }) }),
|
||||
async (ctx, next) => {
|
||||
const { result } = await provider.interactionDetails(ctx.req, ctx.res);
|
||||
const { via } = ctx.guard.params;
|
||||
|
||||
const passwordlessVerificationResult = passwordlessVerificationGuard.safeParse(result);
|
||||
assertThat(
|
||||
passwordlessVerificationResult.success,
|
||||
new RequestError({
|
||||
code: 'session.passwordless_verification_session_not_found',
|
||||
status: 404,
|
||||
})
|
||||
);
|
||||
|
||||
const {
|
||||
passwordlessVerification: { email, phone, flow, expiresAt },
|
||||
} = passwordlessVerificationResult.data;
|
||||
|
||||
const type = `SignIn${camelcase(via, { pascalCase: true })}`;
|
||||
ctx.log(type, { email, phone, flow, expiresAt });
|
||||
const { jti } = await provider.interactionDetails(ctx.req, ctx.res);
|
||||
const { phone } = ctx.guard.body;
|
||||
const type = 'SignInSmsSendPasscode';
|
||||
ctx.log(type, { phone });
|
||||
|
||||
assertThat(
|
||||
flow === 'sign-in',
|
||||
new RequestError({ code: 'session.passwordless_not_verified', status: 401 })
|
||||
await hasUserWithPhone(phone),
|
||||
new RequestError({ code: 'user.phone_not_exists', status: 422 })
|
||||
);
|
||||
|
||||
const passcode = await createPasscode(jti, PasscodeType.SignIn, { phone });
|
||||
const { dbEntry } = await sendPasscode(passcode);
|
||||
ctx.log(type, { connectorId: dbEntry.id });
|
||||
ctx.status = 204;
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
`${signInRoute}/sms/verify-passcode`,
|
||||
koaGuard({ body: object({ phone: string().regex(phoneRegEx), code: string() }) }),
|
||||
async (ctx, next) => {
|
||||
const { jti } = await provider.interactionDetails(ctx.req, ctx.res);
|
||||
const { phone, code } = ctx.guard.body;
|
||||
const type = 'SignInSms';
|
||||
ctx.log(type, { phone, code });
|
||||
|
||||
assertThat(
|
||||
dayjs(expiresAt).isValid() && dayjs(expiresAt).isAfter(dayjs()),
|
||||
new RequestError({ code: 'session.passwordless_verification_expired', status: 401 })
|
||||
await hasUserWithPhone(phone),
|
||||
new RequestError({ code: 'user.phone_not_exists', status: 422 })
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @silverhand/fp/no-let
|
||||
let user: User;
|
||||
|
||||
if (via === 'sms') {
|
||||
assertThat(
|
||||
phone && (await hasUserWithPhone(phone)),
|
||||
new RequestError({ code: 'user.phone_not_exists', status: 422 })
|
||||
);
|
||||
// eslint-disable-next-line @silverhand/fp/no-mutation
|
||||
user = await findUserByPhone(phone);
|
||||
} else {
|
||||
assertThat(
|
||||
email && (await hasUserWithEmail(email)),
|
||||
new RequestError({ code: 'user.email_not_exists', status: 422 })
|
||||
);
|
||||
// eslint-disable-next-line @silverhand/fp/no-mutation
|
||||
user = await findUserByEmail(email);
|
||||
}
|
||||
|
||||
const { id } = user;
|
||||
await verifyPasscode(jti, PasscodeType.SignIn, code, { phone });
|
||||
const { id } = await findUserByPhone(phone);
|
||||
ctx.log(type, { userId: id });
|
||||
|
||||
await updateUserById(id, { lastSignInAt: Date.now() });
|
||||
|
@ -190,54 +178,6 @@ export default function passwordlessRoutes<T extends AnonymousRouter>(
|
|||
}
|
||||
);
|
||||
|
||||
// Router.post(
|
||||
// `${signInRoute}/sms/send-passcode`,
|
||||
// koaGuard({ body: object({ phone: string().regex(phoneRegEx) }) }),
|
||||
// async (ctx, next) => {
|
||||
// const { jti } = await provider.interactionDetails(ctx.req, ctx.res);
|
||||
// const { phone } = ctx.guard.body;
|
||||
// const type = 'SignInSmsSendPasscode';
|
||||
// ctx.log(type, { phone });
|
||||
|
||||
// assertThat(
|
||||
// await hasUserWithPhone(phone),
|
||||
// new RequestError({ code: 'user.phone_not_exists', status: 422 })
|
||||
// );
|
||||
|
||||
// const passcode = await createPasscode(jti, PasscodeType.SignIn, { phone });
|
||||
// const { dbEntry } = await sendPasscode(passcode);
|
||||
// ctx.log(type, { connectorId: dbEntry.id });
|
||||
// ctx.status = 204;
|
||||
|
||||
// return next();
|
||||
// }
|
||||
// );
|
||||
|
||||
// router.post(
|
||||
// `${signInRoute}/sms/verify-passcode`,
|
||||
// koaGuard({ body: object({ phone: string().regex(phoneRegEx), code: string() }) }),
|
||||
// async (ctx, next) => {
|
||||
// const { jti } = await provider.interactionDetails(ctx.req, ctx.res);
|
||||
// const { phone, code } = ctx.guard.body;
|
||||
// const type = 'SignInSms';
|
||||
// ctx.log(type, { phone, code });
|
||||
|
||||
// assertThat(
|
||||
// await hasUserWithPhone(phone),
|
||||
// new RequestError({ code: 'user.phone_not_exists', status: 422 })
|
||||
// );
|
||||
|
||||
// await verifyPasscode(jti, PasscodeType.SignIn, code, { phone });
|
||||
// const { id } = await findUserByPhone(phone);
|
||||
// ctx.log(type, { userId: id });
|
||||
|
||||
// await updateUserById(id, { lastSignInAt: Date.now() });
|
||||
// await assignInteractionResults(ctx, provider, { login: { accountId: id } }, true);
|
||||
|
||||
// return next();
|
||||
// }
|
||||
// );
|
||||
|
||||
router.post(
|
||||
`${signInRoute}/email/send-passcode`,
|
||||
koaGuard({ body: object({ email: string().regex(emailRegEx) }) }),
|
||||
|
|
|
@ -8,6 +8,10 @@ export const viaGuard = z.enum(['email', 'sms']);
|
|||
|
||||
export type Via = z.infer<typeof viaGuard>;
|
||||
|
||||
export const operationGuard = z.enum(['send', 'verify']);
|
||||
|
||||
export type Operation = z.infer<typeof operationGuard>;
|
||||
|
||||
export type PasscodePayload = { email: string } | { phone: string };
|
||||
|
||||
export const passwordlessVerificationGuard = z.object({
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import { PasscodeType } from '@logto/schemas';
|
||||
import { logTypeGuard, LogType, PasscodeType } from '@logto/schemas';
|
||||
import { Truthy } from '@silverhand/essentials';
|
||||
|
||||
import { FlowType } from './types';
|
||||
import RequestError from '@/errors/RequestError';
|
||||
import assertThat from '@/utils/assert-that';
|
||||
|
||||
import { FlowType, Operation, Via } from './types';
|
||||
|
||||
export const getRoutePrefix = (
|
||||
type: FlowType,
|
||||
|
@ -20,3 +23,19 @@ export const getPasscodeType = (type: FlowType) => {
|
|||
? PasscodeType.Register
|
||||
: PasscodeType.ForgotPassword;
|
||||
};
|
||||
|
||||
export const getPasswordlessRelatedLogType = (
|
||||
flow: FlowType,
|
||||
via: Via,
|
||||
operation?: Operation
|
||||
): LogType => {
|
||||
const prefix =
|
||||
flow === 'register' ? 'Register' : flow === 'sign-in' ? 'SignIn' : 'ForgotPassword';
|
||||
const body = via === 'email' ? 'Email' : 'Sms';
|
||||
const suffix = operation === 'send' ? 'SendPasscode' : '';
|
||||
|
||||
const result = logTypeGuard.safeParse(prefix + body + suffix);
|
||||
assertThat(result.success, new RequestError('log.invalid_type'));
|
||||
|
||||
return result.data;
|
||||
};
|
||||
|
|
|
@ -60,6 +60,12 @@ const errors = {
|
|||
'Forgot password session not found. Please go back and verify.',
|
||||
forgot_password_verification_expired:
|
||||
'Forgot password verification has expired. Please go back and verify again.',
|
||||
passwordless_verification_session_not_found:
|
||||
'Passwordless verification session not found. Please go back and retry.',
|
||||
passwordless_not_verified:
|
||||
'Passwordless of {{flow}} flow is not verified. Please go back and verify.',
|
||||
passwordless_verification_expired:
|
||||
'Passwordless verification has expired. Please go back and verify again.',
|
||||
unauthorized: 'Please sign in first.',
|
||||
unsupported_prompt_name: 'Unsupported prompt name.',
|
||||
},
|
||||
|
@ -121,6 +127,9 @@ const errors = {
|
|||
not_exists_with_id: 'The {{name}} with ID `{{id}}` does not exist.',
|
||||
not_found: 'The resource does not exist.',
|
||||
},
|
||||
log: {
|
||||
invalid_type: 'The log type is invalid.',
|
||||
},
|
||||
};
|
||||
|
||||
export default errors;
|
||||
|
|
|
@ -65,6 +65,12 @@ const errors = {
|
|||
'Forgot password session not found. Please go back and verify.', // UNTRANSLATED
|
||||
forgot_password_verification_expired:
|
||||
'Forgot password verification has expired. Please go back and verify again.', // UNTRANSLATED
|
||||
passwordless_verification_session_not_found:
|
||||
'Passwordless verification session not found. Please go back and retry.', // UNTRANSLATED
|
||||
passwordless_not_verified:
|
||||
'Passwordless of {{flow}} flow is not verified. Please go back and verify.', // UNTRANSLATED
|
||||
passwordless_verification_expired:
|
||||
'Passwordless verification has expired. Please go back and verify again.', // UNTRANSLATED
|
||||
unauthorized: "Veuillez vous enregistrer d'abord.",
|
||||
unsupported_prompt_name: "Nom d'invite non supporté.",
|
||||
},
|
||||
|
@ -129,6 +135,9 @@ const errors = {
|
|||
not_exists_with_id: "Le {{name}} avec l'ID `{{id}}` n'existe pas.",
|
||||
not_found: "La ressource n'existe pas.",
|
||||
},
|
||||
log: {
|
||||
invalid_type: 'The log type is invalid.', // UNTRANSLATED
|
||||
},
|
||||
};
|
||||
|
||||
export default errors;
|
||||
|
|
|
@ -59,6 +59,12 @@ const errors = {
|
|||
'Forgot password session not found. Please go back and verify.', // UNTRANSLATED
|
||||
forgot_password_verification_expired:
|
||||
'Forgot password verification has expired. Please go back and verify again.', // UNTRANSLATED
|
||||
passwordless_verification_session_not_found:
|
||||
'Passwordless verification session not found. Please go back and retry.', // UNTRANSLATED
|
||||
passwordless_not_verified:
|
||||
'Passwordless of {{flow}} flow is not verified. Please go back and verify.', // UNTRANSLATED
|
||||
passwordless_verification_expired:
|
||||
'Passwordless verification has expired. Please go back and verify again.', // UNTRANSLATED
|
||||
unauthorized: '로그인을 먼저 해주세요.',
|
||||
unsupported_prompt_name: '지원하지 않는 Prompt 이름이예요.',
|
||||
},
|
||||
|
@ -118,6 +124,9 @@ const errors = {
|
|||
not_exists_with_id: '{{id}} ID를 가진 {{name}}는 존재하지 않아요.',
|
||||
not_found: '리소스가 존재하지 않아요.',
|
||||
},
|
||||
log: {
|
||||
invalid_type: 'The log type is invalid.', // UNTRANSLATED
|
||||
},
|
||||
};
|
||||
|
||||
export default errors;
|
||||
|
|
|
@ -61,6 +61,12 @@ const errors = {
|
|||
'Forgot password session not found. Please go back and verify.', // UNTRANSLATED
|
||||
forgot_password_verification_expired:
|
||||
'Forgot password verification has expired. Please go back and verify again.', // UNTRANSLATED
|
||||
passwordless_verification_session_not_found:
|
||||
'Passwordless verification session not found. Please go back and retry.', // UNTRANSLATED
|
||||
passwordless_not_verified:
|
||||
'Passwordless of {{flow}} flow is not verified. Please go back and verify.', // UNTRANSLATED
|
||||
passwordless_verification_expired:
|
||||
'Passwordless verification has expired. Please go back and verify again.', // UNTRANSLATED
|
||||
unauthorized: 'Faça login primeiro.',
|
||||
unsupported_prompt_name: 'Nome de prompt não suportado.',
|
||||
},
|
||||
|
@ -124,6 +130,9 @@ const errors = {
|
|||
not_exists_with_id: '{{name}} com o ID `{{id}}` não existe.',
|
||||
not_found: 'O recurso não existe.',
|
||||
},
|
||||
log: {
|
||||
invalid_type: 'The log type is invalid.', // UNTRANSLATED
|
||||
},
|
||||
};
|
||||
|
||||
export default errors;
|
||||
|
|
|
@ -61,6 +61,12 @@ const errors = {
|
|||
'Forgot password session not found. Please go back and verify.', // UNTRANSLATED
|
||||
forgot_password_verification_expired:
|
||||
'Forgot password verification has expired. Please go back and verify again.', // UNTRANSLATED
|
||||
passwordless_verification_session_not_found:
|
||||
'Passwordless verification session not found. Please go back and retry.', // UNTRANSLATED
|
||||
passwordless_not_verified:
|
||||
'Passwordless of {{flow}} flow is not verified. Please go back and verify.', // UNTRANSLATED
|
||||
passwordless_verification_expired:
|
||||
'Passwordless verification has expired. Please go back and verify again.', // UNTRANSLATED
|
||||
unauthorized: 'Lütfen önce oturum açın.',
|
||||
unsupported_prompt_name: 'Desteklenmeyen prompt adı.',
|
||||
},
|
||||
|
@ -123,6 +129,9 @@ const errors = {
|
|||
not_exists_with_id: ' `{{id}}` id kimliğine sahip {{name}} mevcut değil.',
|
||||
not_found: 'Kaynak mevcut değil.',
|
||||
},
|
||||
log: {
|
||||
invalid_type: 'The log type is invalid.', // UNTRANSLATED
|
||||
},
|
||||
};
|
||||
|
||||
export default errors;
|
||||
|
|
|
@ -57,6 +57,9 @@ const errors = {
|
|||
connector_session_not_found: '无法找到连接器登录信息,请尝试重新登录。',
|
||||
forgot_password_session_not_found: '无法找到忘记密码验证信息,请尝试重新验证。',
|
||||
forgot_password_verification_expired: '忘记密码验证已过期,请尝试重新验证。',
|
||||
passwordless_verification_session_not_found: '无法找到无密码流程验证信息,请尝试重新验证。',
|
||||
passwordless_not_verified: '无密码验证 {{flow}} 流程没找到。请返回并验证。',
|
||||
passwordless_verification_expired: '无密码验证已过期。请返回重新验证。',
|
||||
unauthorized: '请先登录',
|
||||
unsupported_prompt_name: '不支持的 prompt name',
|
||||
},
|
||||
|
@ -112,6 +115,9 @@ const errors = {
|
|||
not_exists_with_id: 'ID 为 `{{id}}` 的 {{name}} 不存在',
|
||||
not_found: '该资源不存在',
|
||||
},
|
||||
log: {
|
||||
invalid_type: 'The log type is invalid.', // UNTRANSLATED
|
||||
},
|
||||
};
|
||||
|
||||
export default errors;
|
||||
|
|
Loading…
Add table
Reference in a new issue