mirror of
https://github.com/logto-io/logto.git
synced 2025-03-31 22:51:25 -05:00
refactor(core,phrases): add send/verify passcode via email/sms routes and corresponding UTs (#2035)
* refactor(core,phrases): add send/verify passcode via email/sms routes and corresponding UTs * refactor(core,phrases): flow to reuse PasscodeType
This commit is contained in:
parent
753e8ebdfd
commit
0960afc97d
13 changed files with 449 additions and 3 deletions
|
@ -89,6 +89,7 @@
|
|||
"@types/rimraf": "^3.0.2",
|
||||
"@types/supertest": "^2.0.11",
|
||||
"@types/tar": "^6.1.2",
|
||||
"camelcase": "^6.2.0",
|
||||
"copyfiles": "^2.4.1",
|
||||
"eslint": "^8.21.0",
|
||||
"http-errors": "^1.6.3",
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
export const forgotPasswordVerificationTimeout = 10 * 60; // 10 mins.
|
||||
export const verificationTimeout = 10 * 60; // 10 mins.
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import { User } from '@logto/schemas';
|
||||
/* eslint-disable max-lines */
|
||||
import { PasscodeType, User } from '@logto/schemas';
|
||||
import dayjs from 'dayjs';
|
||||
import { Provider } from 'oidc-provider';
|
||||
|
||||
import { mockUser } from '@/__mocks__';
|
||||
import RequestError from '@/errors/RequestError';
|
||||
import { createRequester } from '@/utils/test-utils';
|
||||
|
||||
import { verificationTimeout } from './consts';
|
||||
import passwordlessRoutes, { registerRoute, signInRoute } from './passwordless';
|
||||
|
||||
const insertUser = jest.fn(async (..._args: unknown[]) => ({ id: 'id' }));
|
||||
|
@ -27,8 +30,9 @@ jest.mock('@/queries/user', () => ({
|
|||
}));
|
||||
|
||||
const sendPasscode = jest.fn(async () => ({ dbEntry: { id: 'connectorIdValue' } }));
|
||||
const createPasscode = jest.fn(async (..._args: unknown[]) => ({ id: 'id' }));
|
||||
jest.mock('@/lib/passcode', () => ({
|
||||
createPasscode: async () => ({ id: 'id' }),
|
||||
createPasscode: async (..._args: unknown[]) => createPasscode(..._args),
|
||||
sendPasscode: async () => sendPasscode(),
|
||||
verifyPasscode: async (_a: unknown, _b: unknown, code: string) => {
|
||||
if (code !== '1234') {
|
||||
|
@ -65,6 +69,196 @@ describe('session -> passwordlessRoutes', () => {
|
|||
],
|
||||
});
|
||||
|
||||
describe('POST /session/passwordless/sms/send', () => {
|
||||
beforeEach(() => {
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
jti: 'jti',
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.resetModules();
|
||||
});
|
||||
it('should call sendPasscode (with flow `sign-in`)', async () => {
|
||||
const response = await sessionRequest
|
||||
.post('/session/passwordless/sms/send')
|
||||
.send({ phone: '13000000000', flow: PasscodeType.SignIn });
|
||||
expect(response.statusCode).toEqual(204);
|
||||
expect(createPasscode).toHaveBeenCalledWith('jti', PasscodeType.SignIn, {
|
||||
phone: '13000000000',
|
||||
});
|
||||
expect(sendPasscode).toHaveBeenCalled();
|
||||
});
|
||||
it('should call sendPasscode (with flow `register`)', async () => {
|
||||
const response = await sessionRequest
|
||||
.post('/session/passwordless/sms/send')
|
||||
.send({ phone: '13000000000', flow: PasscodeType.Register });
|
||||
expect(response.statusCode).toEqual(204);
|
||||
expect(createPasscode).toHaveBeenCalledWith('jti', PasscodeType.Register, {
|
||||
phone: '13000000000',
|
||||
});
|
||||
expect(sendPasscode).toHaveBeenCalled();
|
||||
});
|
||||
it('throw when phone not given in input params', async () => {
|
||||
const response = await sessionRequest
|
||||
.post('/session/passwordless/sms/send')
|
||||
.send({ flow: PasscodeType.Register });
|
||||
expect(response.statusCode).toEqual(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /session/passwordless/email/send', () => {
|
||||
beforeEach(() => {
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
jti: 'jti',
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.resetModules();
|
||||
});
|
||||
it('should call sendPasscode (with flow `sign-in`)', async () => {
|
||||
const response = await sessionRequest
|
||||
.post('/session/passwordless/email/send')
|
||||
.send({ email: 'a@a.com', flow: PasscodeType.SignIn });
|
||||
expect(response.statusCode).toEqual(204);
|
||||
expect(createPasscode).toHaveBeenCalledWith('jti', PasscodeType.SignIn, {
|
||||
email: 'a@a.com',
|
||||
});
|
||||
expect(sendPasscode).toHaveBeenCalled();
|
||||
});
|
||||
it('should call sendPasscode (with flow `register`)', async () => {
|
||||
const response = await sessionRequest
|
||||
.post('/session/passwordless/email/send')
|
||||
.send({ email: 'a@a.com', flow: PasscodeType.Register });
|
||||
expect(response.statusCode).toEqual(204);
|
||||
expect(createPasscode).toHaveBeenCalledWith('jti', PasscodeType.Register, {
|
||||
email: 'a@a.com',
|
||||
});
|
||||
expect(sendPasscode).toHaveBeenCalled();
|
||||
});
|
||||
it('throw when email not given in input params', async () => {
|
||||
const response = await sessionRequest
|
||||
.post('/session/passwordless/email/send')
|
||||
.send({ flow: PasscodeType.Register });
|
||||
expect(response.statusCode).toEqual(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /session/passwordless/sms/verify', () => {
|
||||
beforeEach(() => {
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
jti: 'jti',
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
jest.clearAllMocks();
|
||||
jest.resetModules();
|
||||
});
|
||||
it('should call interactionResult (with flow `sign-in`)', async () => {
|
||||
const fakeTime = new Date();
|
||||
jest.useFakeTimers().setSystemTime(fakeTime);
|
||||
const response = await sessionRequest
|
||||
.post('/session/passwordless/sms/verify')
|
||||
.send({ phone: '13000000000', code: '1234', flow: PasscodeType.SignIn });
|
||||
expect(response.statusCode).toEqual(204);
|
||||
expect(interactionResult).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
verification: {
|
||||
flow: PasscodeType.SignIn,
|
||||
phone: '13000000000',
|
||||
expiresAt: dayjs(fakeTime).add(verificationTimeout, 'second').toISOString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
it('should call interactionResult (with flow `register`)', async () => {
|
||||
const fakeTime = new Date();
|
||||
jest.useFakeTimers().setSystemTime(fakeTime);
|
||||
const response = await sessionRequest
|
||||
.post('/session/passwordless/sms/verify')
|
||||
.send({ phone: '13000000000', code: '1234', flow: PasscodeType.Register });
|
||||
expect(response.statusCode).toEqual(204);
|
||||
expect(interactionResult).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
verification: {
|
||||
flow: PasscodeType.Register,
|
||||
phone: '13000000000',
|
||||
expiresAt: dayjs(fakeTime).add(verificationTimeout, 'second').toISOString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
it('throw when code is wrong', async () => {
|
||||
const response = await sessionRequest
|
||||
.post('/session/passwordless/sms/verify')
|
||||
.send({ phone: '13000000000', code: '1231', flow: PasscodeType.SignIn });
|
||||
expect(response.statusCode).toEqual(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /session/passwordless/email/verify', () => {
|
||||
beforeEach(() => {
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
jti: 'jti',
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
jest.clearAllMocks();
|
||||
jest.resetModules();
|
||||
});
|
||||
it('should call interactionResult (with flow `sign-in`)', async () => {
|
||||
const fakeTime = new Date();
|
||||
jest.useFakeTimers().setSystemTime(fakeTime);
|
||||
const response = await sessionRequest
|
||||
.post('/session/passwordless/email/verify')
|
||||
.send({ email: 'a@a.com', code: '1234', flow: PasscodeType.SignIn });
|
||||
expect(response.statusCode).toEqual(204);
|
||||
expect(interactionResult).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
verification: {
|
||||
flow: PasscodeType.SignIn,
|
||||
email: 'a@a.com',
|
||||
expiresAt: dayjs(fakeTime).add(verificationTimeout, 'second').toISOString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
it('should call interactionResult (with flow `register`)', async () => {
|
||||
const fakeTime = new Date();
|
||||
jest.useFakeTimers().setSystemTime(fakeTime);
|
||||
const response = await sessionRequest
|
||||
.post('/session/passwordless/email/verify')
|
||||
.send({ email: 'a@a.com', code: '1234', flow: PasscodeType.Register });
|
||||
expect(response.statusCode).toEqual(204);
|
||||
expect(interactionResult).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
verification: {
|
||||
flow: PasscodeType.Register,
|
||||
email: 'a@a.com',
|
||||
expiresAt: dayjs(fakeTime).add(verificationTimeout, 'second').toISOString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
it('throw when code is wrong', async () => {
|
||||
const response = await sessionRequest
|
||||
.post('/session/passwordless/email/verify')
|
||||
.send({ email: 'a@a.com', code: '1231', flow: 'sign-in' });
|
||||
expect(response.statusCode).toEqual(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /session/sign-in/passwordless/sms/send-passcode', () => {
|
||||
beforeAll(() => {
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
|
@ -317,3 +511,4 @@ describe('session -> passwordlessRoutes', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
/* eslint-enable max-lines */
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { emailRegEx, phoneRegEx } from '@logto/core-kit';
|
||||
import { PasscodeType } from '@logto/schemas';
|
||||
import dayjs from 'dayjs';
|
||||
import { Provider } from 'oidc-provider';
|
||||
import { object, string } from 'zod';
|
||||
|
||||
|
@ -15,10 +16,12 @@ import {
|
|||
findUserByEmail,
|
||||
findUserByPhone,
|
||||
} from '@/queries/user';
|
||||
import { passcodeTypeGuard } from '@/routes/session/types';
|
||||
import assertThat from '@/utils/assert-that';
|
||||
|
||||
import { AnonymousRouter } from '../types';
|
||||
import { getRoutePrefix } from './utils';
|
||||
import { verificationTimeout } from './consts';
|
||||
import { getPasswordlessRelatedLogType, getRoutePrefix } from './utils';
|
||||
|
||||
export const registerRoute = getRoutePrefix('register', 'passwordless');
|
||||
export const signInRoute = getRoutePrefix('sign-in', 'passwordless');
|
||||
|
@ -27,6 +30,124 @@ export default function passwordlessRoutes<T extends AnonymousRouter>(
|
|||
router: T,
|
||||
provider: Provider
|
||||
) {
|
||||
router.post(
|
||||
'/session/passwordless/sms/send',
|
||||
koaGuard({
|
||||
body: object({
|
||||
phone: string().regex(phoneRegEx),
|
||||
flow: passcodeTypeGuard,
|
||||
}),
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { jti } = await provider.interactionDetails(ctx.req, ctx.res);
|
||||
const {
|
||||
body: { phone, flow },
|
||||
} = ctx.guard;
|
||||
|
||||
const type = getPasswordlessRelatedLogType(flow, 'sms', 'send');
|
||||
ctx.log(type, { phone });
|
||||
|
||||
const passcode = await createPasscode(jti, flow, { phone });
|
||||
const { dbEntry } = await sendPasscode(passcode);
|
||||
ctx.log(type, { connectorId: dbEntry.id });
|
||||
ctx.status = 204;
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/session/passwordless/email/send',
|
||||
koaGuard({
|
||||
body: object({
|
||||
email: string().regex(emailRegEx),
|
||||
flow: passcodeTypeGuard,
|
||||
}),
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { jti } = await provider.interactionDetails(ctx.req, ctx.res);
|
||||
const {
|
||||
body: { email, flow },
|
||||
} = ctx.guard;
|
||||
|
||||
const type = getPasswordlessRelatedLogType(flow, 'email', 'send');
|
||||
ctx.log(type, { email });
|
||||
|
||||
const passcode = await createPasscode(jti, flow, { email });
|
||||
const { dbEntry } = await sendPasscode(passcode);
|
||||
ctx.log(type, { connectorId: dbEntry.id });
|
||||
ctx.status = 204;
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/session/passwordless/sms/verify',
|
||||
koaGuard({
|
||||
body: object({
|
||||
phone: string().regex(phoneRegEx),
|
||||
code: string(),
|
||||
flow: passcodeTypeGuard,
|
||||
}),
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { jti } = await provider.interactionDetails(ctx.req, ctx.res);
|
||||
const {
|
||||
body: { phone, code, flow },
|
||||
} = ctx.guard;
|
||||
|
||||
const type = getPasswordlessRelatedLogType(flow, 'sms', 'verify');
|
||||
ctx.log(type, { phone });
|
||||
|
||||
await verifyPasscode(jti, flow, code, { phone });
|
||||
|
||||
await provider.interactionResult(ctx.req, ctx.res, {
|
||||
verification: {
|
||||
flow,
|
||||
expiresAt: dayjs().add(verificationTimeout, 'second').toISOString(),
|
||||
phone,
|
||||
},
|
||||
});
|
||||
ctx.status = 204;
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/session/passwordless/email/verify',
|
||||
koaGuard({
|
||||
body: object({
|
||||
email: string().regex(emailRegEx),
|
||||
code: string(),
|
||||
flow: passcodeTypeGuard,
|
||||
}),
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { jti } = await provider.interactionDetails(ctx.req, ctx.res);
|
||||
const {
|
||||
body: { email, code, flow },
|
||||
} = ctx.guard;
|
||||
|
||||
const type = getPasswordlessRelatedLogType(flow, 'email', 'verify');
|
||||
ctx.log(type, { email });
|
||||
|
||||
await verifyPasscode(jti, flow, code, { email });
|
||||
|
||||
await provider.interactionResult(ctx.req, ctx.res, {
|
||||
verification: {
|
||||
flow,
|
||||
expiresAt: dayjs().add(verificationTimeout, 'second').toISOString(),
|
||||
email,
|
||||
},
|
||||
});
|
||||
ctx.status = 204;
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
`${signInRoute}/sms/send-passcode`,
|
||||
koaGuard({ body: object({ phone: string().regex(phoneRegEx) }) }),
|
||||
|
|
23
packages/core/src/routes/session/types.ts
Normal file
23
packages/core/src/routes/session/types.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { PasscodeType } from '@logto/schemas';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const passcodeTypeGuard = z.nativeEnum(PasscodeType);
|
||||
|
||||
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 verificationStorageGuard = z.object({
|
||||
email: z.string().optional(),
|
||||
phone: z.string().optional(),
|
||||
flow: passcodeTypeGuard,
|
||||
expiresAt: z.string(),
|
||||
});
|
||||
|
||||
export type VerificationStorage = z.infer<typeof verificationStorageGuard>;
|
|
@ -1,4 +1,12 @@
|
|||
import { logTypeGuard, LogType, PasscodeType } from '@logto/schemas';
|
||||
import { Truthy } from '@silverhand/essentials';
|
||||
import dayjs from 'dayjs';
|
||||
import { z } from 'zod';
|
||||
|
||||
import RequestError from '@/errors/RequestError';
|
||||
import assertThat from '@/utils/assert-that';
|
||||
|
||||
import { verificationStorageGuard, Operation, VerificationStorage, Via } from './types';
|
||||
|
||||
export const getRoutePrefix = (
|
||||
type: 'sign-in' | 'register' | 'forgot-password',
|
||||
|
@ -9,3 +17,52 @@ export const getRoutePrefix = (
|
|||
.map((value) => '/' + value)
|
||||
.join('');
|
||||
};
|
||||
|
||||
export const getPasswordlessRelatedLogType = (
|
||||
flow: PasscodeType,
|
||||
via: Via,
|
||||
operation?: Operation
|
||||
): LogType => {
|
||||
const body = via === 'email' ? 'Email' : 'Sms';
|
||||
const suffix = operation === 'send' ? 'SendPasscode' : '';
|
||||
|
||||
const result = logTypeGuard.safeParse(flow + body + suffix);
|
||||
assertThat(result.success, new RequestError('log.invalid_type'));
|
||||
|
||||
return result.data;
|
||||
};
|
||||
|
||||
export const parseVerificationStorage = (data: unknown): VerificationStorage => {
|
||||
const verificationResult = z
|
||||
.object({
|
||||
verification: verificationStorageGuard,
|
||||
})
|
||||
.safeParse(data);
|
||||
|
||||
assertThat(
|
||||
verificationResult.success,
|
||||
new RequestError({
|
||||
code: 'session.verification_session_not_found',
|
||||
status: 404,
|
||||
})
|
||||
);
|
||||
|
||||
return verificationResult.data.verification;
|
||||
};
|
||||
|
||||
export const verificationSessionCheckByFlow = (
|
||||
currentFlow: PasscodeType,
|
||||
payload: Pick<VerificationStorage, 'flow' | 'expiresAt'>
|
||||
) => {
|
||||
const { flow, expiresAt } = payload;
|
||||
|
||||
assertThat(
|
||||
flow === currentFlow,
|
||||
new RequestError({ code: 'session.passwordless_not_verified', status: 401 })
|
||||
);
|
||||
|
||||
assertThat(
|
||||
dayjs(expiresAt).isValid() && dayjs(expiresAt).isAfter(dayjs()),
|
||||
new RequestError({ code: 'session.verification_expired', status: 401 })
|
||||
);
|
||||
};
|
||||
|
|
|
@ -60,6 +60,11 @@ 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.',
|
||||
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.',
|
||||
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 +126,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,11 @@ 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
|
||||
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
|
||||
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 +134,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,11 @@ 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
|
||||
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
|
||||
verification_expired: 'Passwordless verification has expired. Please go back and verify again.', // UNTRANSLATED
|
||||
unauthorized: '로그인을 먼저 해주세요.',
|
||||
unsupported_prompt_name: '지원하지 않는 Prompt 이름이예요.',
|
||||
},
|
||||
|
@ -118,6 +123,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,11 @@ 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
|
||||
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
|
||||
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 +129,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,11 @@ 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
|
||||
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
|
||||
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 +128,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: '忘记密码验证已过期,请尝试重新验证。',
|
||||
verification_session_not_found: '无法找到无密码流程验证信息,请尝试重新验证。',
|
||||
passwordless_not_verified: '无密码验证 {{flow}} 流程没找到。请返回并验证。',
|
||||
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;
|
||||
|
|
2
pnpm-lock.yaml
generated
2
pnpm-lock.yaml
generated
|
@ -177,6 +177,7 @@ importers:
|
|||
'@types/rimraf': ^3.0.2
|
||||
'@types/supertest': ^2.0.11
|
||||
'@types/tar': ^6.1.2
|
||||
camelcase: ^6.2.0
|
||||
chalk: ^4
|
||||
copyfiles: ^2.4.1
|
||||
dayjs: ^1.10.5
|
||||
|
@ -294,6 +295,7 @@ importers:
|
|||
'@types/rimraf': 3.0.2
|
||||
'@types/supertest': 2.0.11
|
||||
'@types/tar': 6.1.2
|
||||
camelcase: 6.3.0
|
||||
copyfiles: 2.4.1
|
||||
eslint: 8.21.0
|
||||
http-errors: 1.8.1
|
||||
|
|
Loading…
Add table
Reference in a new issue