mirror of
https://github.com/logto-io/logto.git
synced 2025-01-06 20:40:08 -05:00
feat(core): block suspended user from sign in (#2366)
This commit is contained in:
parent
fc44c9a503
commit
f75d5e605e
14 changed files with 312 additions and 61 deletions
|
@ -59,7 +59,8 @@ export const smsSignInAction = <StateT, ContextT extends WithLogContext, Respons
|
|||
);
|
||||
|
||||
const user = await findUserByPhone(phone);
|
||||
const { id } = user;
|
||||
const { id, isSuspended } = user;
|
||||
assertThat(!isSuspended, new RequestError({ code: 'user.suspended', status: 401 }));
|
||||
ctx.log(type, { userId: id });
|
||||
|
||||
await checkRequiredProfile(ctx, provider, user, signInExperience);
|
||||
|
@ -105,7 +106,8 @@ export const emailSignInAction = <StateT, ContextT extends WithLogContext, Respo
|
|||
);
|
||||
|
||||
const user = await findUserByEmail(email);
|
||||
const { id } = user;
|
||||
const { id, isSuspended } = user;
|
||||
assertThat(!isSuspended, new RequestError({ code: 'user.suspended', status: 401 }));
|
||||
ctx.log(type, { userId: id });
|
||||
|
||||
await checkRequiredProfile(ctx, provider, user, signInExperience);
|
||||
|
|
|
@ -15,6 +15,7 @@ import passwordlessRoutes, { registerRoute, signInRoute } from './passwordless';
|
|||
const insertUser = jest.fn(async (..._args: unknown[]) => mockUser);
|
||||
const findUserById = jest.fn(async (): Promise<User> => mockUser);
|
||||
const findUserByEmail = jest.fn(async (): Promise<User> => mockUser);
|
||||
const findUserByPhone = jest.fn(async (): Promise<User> => mockUser);
|
||||
const updateUserById = jest.fn(async (..._args: unknown[]) => mockUser);
|
||||
const findDefaultSignInExperience = jest.fn(async () => ({
|
||||
...mockSignInExperience,
|
||||
|
@ -34,7 +35,7 @@ jest.mock('@/lib/user', () => ({
|
|||
|
||||
jest.mock('@/queries/user', () => ({
|
||||
findUserById: async () => findUserById(),
|
||||
findUserByPhone: async () => mockUser,
|
||||
findUserByPhone: async () => findUserByPhone(),
|
||||
findUserByEmail: async () => findUserByEmail(),
|
||||
updateUserById: async (...args: unknown[]) => updateUserById(...args),
|
||||
hasUser: async (username: string) => username === 'username1',
|
||||
|
@ -503,6 +504,24 @@ describe('session -> passwordlessRoutes', () => {
|
|||
expect(response.statusCode).toEqual(404);
|
||||
});
|
||||
|
||||
it('throw when user is suspended', async () => {
|
||||
findUserByPhone.mockResolvedValueOnce({
|
||||
...mockUser,
|
||||
isSuspended: true,
|
||||
});
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
result: {
|
||||
verification: {
|
||||
phone: '13000000000',
|
||||
flow: PasscodeType.SignIn,
|
||||
expiresAt: getTomorrowIsoString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
const response = await sessionRequest.post(`${signInRoute}/sms`);
|
||||
expect(response.statusCode).toEqual(401);
|
||||
});
|
||||
|
||||
it('throw error if sign in method is not enabled', async () => {
|
||||
findDefaultSignInExperience.mockResolvedValueOnce({
|
||||
...mockSignInExperience,
|
||||
|
@ -639,6 +658,24 @@ describe('session -> passwordlessRoutes', () => {
|
|||
expect(response.statusCode).toEqual(404);
|
||||
});
|
||||
|
||||
it('throw when user is suspended', async () => {
|
||||
findUserByEmail.mockResolvedValueOnce({
|
||||
...mockUser,
|
||||
isSuspended: true,
|
||||
});
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
result: {
|
||||
verification: {
|
||||
email: 'a@a.com',
|
||||
flow: PasscodeType.SignIn,
|
||||
expiresAt: getTomorrowIsoString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
const response = await sessionRequest.post(`${signInRoute}/email`);
|
||||
expect(response.statusCode).toEqual(401);
|
||||
});
|
||||
|
||||
it('throw error if sign in method is not enabled', async () => {
|
||||
findDefaultSignInExperience.mockResolvedValueOnce({
|
||||
...mockSignInExperience,
|
||||
|
|
193
packages/core/src/routes/session/social.bind-social.test.ts
Normal file
193
packages/core/src/routes/session/social.bind-social.test.ts
Normal file
|
@ -0,0 +1,193 @@
|
|||
import { ConnectorType } from '@logto/connector-kit';
|
||||
import type { User } from '@logto/schemas';
|
||||
import { SignUpIdentifier } from '@logto/schemas';
|
||||
import { Provider } from 'oidc-provider';
|
||||
|
||||
import { mockLogtoConnectorList, mockSignInExperience, mockUser } from '@/__mocks__';
|
||||
import { getLogtoConnectorById } from '@/connectors';
|
||||
import RequestError from '@/errors/RequestError';
|
||||
import { createRequester } from '@/utils/test-utils';
|
||||
|
||||
import socialRoutes, { registerRoute } from './social';
|
||||
|
||||
const findSocialRelatedUser = jest.fn(async () => [
|
||||
'phone',
|
||||
{ id: 'user1', identities: {}, isSuspended: false },
|
||||
]);
|
||||
jest.mock('@/lib/social', () => ({
|
||||
...jest.requireActual('@/lib/social'),
|
||||
findSocialRelatedUser: async () => findSocialRelatedUser(),
|
||||
async getUserInfoByAuthCode(connectorId: string, data: { code: string }) {
|
||||
if (connectorId === '_connectorId') {
|
||||
throw new RequestError({
|
||||
code: 'session.invalid_connector_id',
|
||||
status: 422,
|
||||
connectorId,
|
||||
});
|
||||
}
|
||||
|
||||
if (data.code === '123456') {
|
||||
return { id: mockUser.id };
|
||||
}
|
||||
|
||||
// This mocks the case that can not get userInfo with access token and auth code
|
||||
// (most likely third-party social connectors' problem).
|
||||
throw new Error(' ');
|
||||
},
|
||||
}));
|
||||
const insertUser = jest.fn(async (..._args: unknown[]) => mockUser);
|
||||
const findUserById = jest.fn(async (): Promise<User> => mockUser);
|
||||
const updateUserById = jest.fn(async (..._args: unknown[]) => mockUser);
|
||||
const findUserByIdentity = jest.fn(async () => mockUser);
|
||||
|
||||
jest.mock('@/queries/user', () => ({
|
||||
findUserById: async () => findUserById(),
|
||||
findUserByIdentity: async () => findUserByIdentity(),
|
||||
updateUserById: async (...args: unknown[]) => updateUserById(...args),
|
||||
hasUserWithIdentity: async (target: string, userId: string) =>
|
||||
target === 'connectorTarget' && userId === mockUser.id,
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/user', () => ({
|
||||
generateUserId: () => 'user1',
|
||||
insertUser: async (...args: unknown[]) => insertUser(...args),
|
||||
}));
|
||||
|
||||
jest.mock('@/queries/sign-in-experience', () => ({
|
||||
findDefaultSignInExperience: async () => ({
|
||||
...mockSignInExperience,
|
||||
signUp: {
|
||||
...mockSignInExperience.signUp,
|
||||
identifier: SignUpIdentifier.None,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
const getLogtoConnectorByIdHelper = jest.fn(async (connectorId: string) => {
|
||||
const database = {
|
||||
enabled: connectorId === 'social_enabled',
|
||||
};
|
||||
const metadata = {
|
||||
id:
|
||||
connectorId === 'social_enabled'
|
||||
? 'social_enabled'
|
||||
: connectorId === 'social_disabled'
|
||||
? 'social_disabled'
|
||||
: 'others',
|
||||
};
|
||||
|
||||
return {
|
||||
dbEntry: database,
|
||||
metadata,
|
||||
type: connectorId.startsWith('social') ? ConnectorType.Social : ConnectorType.Sms,
|
||||
getAuthorizationUri: jest.fn(async () => ''),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('@/connectors', () => ({
|
||||
getLogtoConnectors: jest.fn(async () => mockLogtoConnectorList),
|
||||
getLogtoConnectorById: jest.fn(async (connectorId: string) => {
|
||||
const connector = await getLogtoConnectorByIdHelper(connectorId);
|
||||
|
||||
if (connector.type !== ConnectorType.Social) {
|
||||
throw new RequestError({
|
||||
code: 'entity.not_found',
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
|
||||
return connector;
|
||||
}),
|
||||
}));
|
||||
|
||||
const interactionResult = jest.fn(async () => 'redirectTo');
|
||||
const interactionDetails: jest.MockedFunction<() => Promise<unknown>> = jest.fn(async () => ({}));
|
||||
|
||||
jest.mock('oidc-provider', () => ({
|
||||
Provider: jest.fn(() => ({
|
||||
interactionDetails,
|
||||
interactionResult,
|
||||
})),
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
interactionResult.mockClear();
|
||||
});
|
||||
|
||||
describe('session -> socialRoutes', () => {
|
||||
const sessionRequest = createRequester({
|
||||
anonymousRoutes: socialRoutes,
|
||||
provider: new Provider(''),
|
||||
middlewares: [
|
||||
async (ctx, next) => {
|
||||
ctx.addLogContext = jest.fn();
|
||||
ctx.log = jest.fn();
|
||||
|
||||
return next();
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
describe('POST /session/register/social', () => {
|
||||
beforeEach(() => {
|
||||
const mockGetLogtoConnectorById = getLogtoConnectorById as jest.Mock;
|
||||
mockGetLogtoConnectorById.mockResolvedValueOnce({
|
||||
metadata: { target: 'connectorTarget' },
|
||||
});
|
||||
});
|
||||
|
||||
it('register with social, assign result and redirect', async () => {
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
jti: 'jti',
|
||||
result: {
|
||||
socialUserInfo: { connectorId: 'connectorId', userInfo: { id: 'user1' } },
|
||||
},
|
||||
});
|
||||
const response = await sessionRequest
|
||||
.post(`${registerRoute}`)
|
||||
.send({ connectorId: 'connectorId' });
|
||||
expect(insertUser).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 'user1',
|
||||
identities: { connectorTarget: { userId: 'user1', details: { id: 'user1' } } },
|
||||
})
|
||||
);
|
||||
expect(response.body).toHaveProperty('redirectTo');
|
||||
expect(interactionResult).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.objectContaining({ login: { accountId: 'user1' } }),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
|
||||
it('throw error if no result can be found in interactionResults', async () => {
|
||||
interactionDetails.mockResolvedValueOnce({});
|
||||
const response = await sessionRequest
|
||||
.post(`${registerRoute}`)
|
||||
.send({ connectorId: 'connectorId' });
|
||||
expect(response.statusCode).toEqual(400);
|
||||
});
|
||||
|
||||
it('throw error if result parsing fails', async () => {
|
||||
interactionDetails.mockResolvedValueOnce({ result: { login: { accountId: mockUser.id } } });
|
||||
const response = await sessionRequest
|
||||
.post(`${registerRoute}`)
|
||||
.send({ connectorId: 'connectorId' });
|
||||
expect(response.statusCode).toEqual(400);
|
||||
});
|
||||
|
||||
it('throw error when user with identity exists', async () => {
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
result: {
|
||||
login: { accountId: 'user1' },
|
||||
socialUserInfo: { connectorId: 'connectorId', userInfo: { id: mockUser.id } },
|
||||
},
|
||||
});
|
||||
const response = await sessionRequest
|
||||
.post(`${registerRoute}`)
|
||||
.send({ connectorId: 'connectorId' });
|
||||
expect(response.statusCode).toEqual(400);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -10,11 +10,13 @@ import { createRequester } from '@/utils/test-utils';
|
|||
|
||||
import socialRoutes, { registerRoute, signInRoute } from './social';
|
||||
|
||||
const findSocialRelatedUser = jest.fn(async () => [
|
||||
'phone',
|
||||
{ id: 'user1', identities: {}, isSuspended: false },
|
||||
]);
|
||||
jest.mock('@/lib/social', () => ({
|
||||
...jest.requireActual('@/lib/social'),
|
||||
async findSocialRelatedUser() {
|
||||
return ['phone', { id: 'user1', identities: {} }];
|
||||
},
|
||||
findSocialRelatedUser: async () => findSocialRelatedUser(),
|
||||
async getUserInfoByAuthCode(connectorId: string, data: { code: string }) {
|
||||
if (connectorId === '_connectorId') {
|
||||
throw new RequestError({
|
||||
|
@ -36,10 +38,11 @@ jest.mock('@/lib/social', () => ({
|
|||
const insertUser = jest.fn(async (..._args: unknown[]) => mockUser);
|
||||
const findUserById = jest.fn(async (): Promise<User> => mockUser);
|
||||
const updateUserById = jest.fn(async (..._args: unknown[]) => mockUser);
|
||||
const findUserByIdentity = jest.fn(async () => mockUser);
|
||||
|
||||
jest.mock('@/queries/user', () => ({
|
||||
findUserById: async () => findUserById(),
|
||||
findUserByIdentity: async () => mockUser,
|
||||
findUserByIdentity: async () => findUserByIdentity(),
|
||||
updateUserById: async (...args: unknown[]) => updateUserById(...args),
|
||||
hasUserWithIdentity: async (target: string, userId: string) =>
|
||||
target === 'connectorTarget' && userId === mockUser.id,
|
||||
|
@ -238,6 +241,25 @@ describe('session -> socialRoutes', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('throw error when user is suspended', async () => {
|
||||
(getLogtoConnectorById as jest.Mock).mockResolvedValueOnce({
|
||||
metadata: { target: connectorTarget },
|
||||
});
|
||||
findUserByIdentity.mockResolvedValueOnce({
|
||||
...mockUser,
|
||||
isSuspended: true,
|
||||
});
|
||||
const response = await sessionRequest.post(`${signInRoute}/auth`).send({
|
||||
connectorId: 'connectorId',
|
||||
data: {
|
||||
state: 'state',
|
||||
redirectUri: 'https://logto.dev',
|
||||
code: '123456',
|
||||
},
|
||||
});
|
||||
expect(response.statusCode).toEqual(401);
|
||||
});
|
||||
|
||||
it('throw error when identity exists', async () => {
|
||||
const wrongConnectorTarget = 'wrongConnectorTarget';
|
||||
(getLogtoConnectorById as jest.Mock).mockResolvedValueOnce({
|
||||
|
@ -287,6 +309,28 @@ describe('session -> socialRoutes', () => {
|
|||
.send({ connectorId: 'connectorId' })
|
||||
).resolves.toHaveProperty('statusCode', 400);
|
||||
});
|
||||
it('throw error when user is suspended', async () => {
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
result: {
|
||||
login: { accountId: 'user1' },
|
||||
socialUserInfo: {
|
||||
connectorId: 'connectorId',
|
||||
userInfo: { id: 'connectorUser', phone: 'phone' },
|
||||
},
|
||||
},
|
||||
});
|
||||
findSocialRelatedUser.mockResolvedValueOnce([
|
||||
'phone',
|
||||
{
|
||||
...mockUser,
|
||||
isSuspended: true,
|
||||
},
|
||||
]);
|
||||
const response = await sessionRequest.post('/session/sign-in/bind-social-related-user').send({
|
||||
connectorId: 'connectorId',
|
||||
});
|
||||
expect(response.statusCode).toEqual(401);
|
||||
});
|
||||
it('updates user identities and sign in', async () => {
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
result: {
|
||||
|
@ -384,55 +428,4 @@ describe('session -> socialRoutes', () => {
|
|||
expect(response.statusCode).toEqual(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /session/bind-social', () => {
|
||||
beforeEach(() => {
|
||||
const mockGetLogtoConnectorById = getLogtoConnectorById as jest.Mock;
|
||||
mockGetLogtoConnectorById.mockResolvedValueOnce({
|
||||
metadata: { target: 'connectorTarget' },
|
||||
});
|
||||
});
|
||||
it('throw if session is not authorized', async () => {
|
||||
interactionDetails.mockResolvedValueOnce({});
|
||||
await expect(
|
||||
sessionRequest.post('/session/bind-social').send({ connectorId: 'connectorId' })
|
||||
).resolves.toHaveProperty('statusCode', 400);
|
||||
});
|
||||
it('throw if no social info in session', async () => {
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
result: { login: { accountId: 'user1' } },
|
||||
});
|
||||
await expect(
|
||||
sessionRequest.post('/session/bind-social').send({ connectorId: 'connectorId' })
|
||||
).resolves.toHaveProperty('statusCode', 400);
|
||||
});
|
||||
it('updates user identities', async () => {
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
jti: 'jti',
|
||||
result: {
|
||||
login: { accountId: 'user1' },
|
||||
socialUserInfo: {
|
||||
connectorId: 'connectorId',
|
||||
userInfo: { id: 'connectorUser', phone: 'phone' },
|
||||
},
|
||||
},
|
||||
});
|
||||
const response = await sessionRequest.post('/session/bind-social').send({
|
||||
connectorId: 'connectorId',
|
||||
});
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(updateUserById).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
identities: {
|
||||
connectorTarget: {
|
||||
details: { id: 'connectorUser', phone: 'phone' },
|
||||
userId: 'connectorUser',
|
||||
},
|
||||
connector1: { userId: 'connector1', details: {} },
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -94,7 +94,8 @@ export default function socialRoutes<T extends AnonymousRouter>(router: T, provi
|
|||
}
|
||||
|
||||
const user = await findUserByIdentity(target, userInfo.id);
|
||||
const { id, identities } = user;
|
||||
const { id, identities, isSuspended } = user;
|
||||
assertThat(!isSuspended, new RequestError({ code: 'user.suspended', status: 401 }));
|
||||
ctx.log(type, { userId: id });
|
||||
|
||||
// Update social connector's user info
|
||||
|
@ -133,7 +134,8 @@ export default function socialRoutes<T extends AnonymousRouter>(router: T, provi
|
|||
const relatedInfo = await findSocialRelatedUser(userInfo);
|
||||
assertThat(relatedInfo, 'session.connector_session_not_found');
|
||||
|
||||
const { id, identities } = relatedInfo[1];
|
||||
const { id, identities, isSuspended } = relatedInfo[1];
|
||||
assertThat(!isSuspended, new RequestError({ code: 'user.suspended', status: 401 }));
|
||||
ctx.log(type, { userId: id });
|
||||
|
||||
const user = await updateUserById(id, {
|
||||
|
|
|
@ -152,6 +152,22 @@ describe('signInWithPassword()', () => {
|
|||
).rejects.toThrowError(new RequestError('session.invalid_credentials'));
|
||||
});
|
||||
|
||||
it('throw if user is suspended', async () => {
|
||||
interactionDetails.mockResolvedValueOnce({ params: {} });
|
||||
await expect(
|
||||
signInWithPassword(createContext(), createProvider(), {
|
||||
identifier: SignInIdentifier.Username,
|
||||
password: 'password',
|
||||
findUser: jest.fn(async () => ({
|
||||
...mockUser,
|
||||
isSuspended: true,
|
||||
})),
|
||||
logType: 'SignInUsernamePassword',
|
||||
logPayload: { username: 'username' },
|
||||
})
|
||||
).rejects.toThrowError(new RequestError('user.suspended'));
|
||||
});
|
||||
|
||||
it('throw if sign in method is not enabled', async () => {
|
||||
findDefaultSignInExperience.mockResolvedValueOnce({
|
||||
...mockSignInExperience,
|
||||
|
|
|
@ -219,7 +219,8 @@ export const signInWithPassword = async (
|
|||
ctx.log(logType, logPayload);
|
||||
|
||||
const user = await findUser();
|
||||
const { id } = await verifyUserPassword(user, password);
|
||||
const { id, isSuspended } = await verifyUserPassword(user, password);
|
||||
assertThat(!isSuspended, new RequestError({ code: 'user.suspended', status: 401 }));
|
||||
|
||||
ctx.log(logType, { userId: id });
|
||||
await updateUserById(id, { lastSignInAt: Date.now() });
|
||||
|
|
|
@ -54,6 +54,7 @@ const errors = {
|
|||
require_sms: 'You need to set a phone before sign in.', // UNTRANSLATED
|
||||
sms_exists: 'Your phone has been set.', // UNTRANSLATED
|
||||
require_email_or_sms: 'You need to set a phone or email before sign in.', // UNTRANSLATED
|
||||
suspended: 'This account is suspended.', // UNTRANSLATED
|
||||
},
|
||||
password: {
|
||||
unsupported_encryption_method: 'Die Verschlüsselungsmethode {{name}} wird nicht unterstützt.',
|
||||
|
|
|
@ -54,6 +54,7 @@ const errors = {
|
|||
require_sms: 'You need to set a phone before sign in.',
|
||||
sms_exists: 'Your phone has been set.',
|
||||
require_email_or_sms: 'You need to set a phone or email before sign in.',
|
||||
suspended: 'This account is suspended.',
|
||||
},
|
||||
password: {
|
||||
unsupported_encryption_method: 'The encryption method {{name}} is not supported.',
|
||||
|
|
|
@ -55,6 +55,7 @@ const errors = {
|
|||
require_sms: 'You need to set a phone before sign in.', // UNTRANSLATED
|
||||
sms_exists: 'Your phone has been set.', // UNTRANSLATED
|
||||
require_email_or_sms: 'You need to set a phone or email before sign in.', // UNTRANSLATED
|
||||
suspended: 'This account is suspended.', // UNTRANSLATED
|
||||
},
|
||||
password: {
|
||||
unsupported_encryption_method: "La méthode de cryptage {{name}} n'est pas prise en charge.",
|
||||
|
|
|
@ -53,6 +53,7 @@ const errors = {
|
|||
require_sms: 'You need to set a phone before sign in.', // UNTRANSLATED
|
||||
sms_exists: 'Your phone has been set.', // UNTRANSLATED
|
||||
require_email_or_sms: 'You need to set a phone or email before sign in.', // UNTRANSLATED
|
||||
suspended: 'This account is suspended.', // UNTRANSLATED
|
||||
},
|
||||
password: {
|
||||
unsupported_encryption_method: '{{name}} 암호화 방법을 지원하지 않아요.',
|
||||
|
|
|
@ -53,6 +53,7 @@ const errors = {
|
|||
require_sms: 'You need to set a phone before sign in.', // UNTRANSLATED
|
||||
sms_exists: 'Your phone has been set.', // UNTRANSLATED
|
||||
require_email_or_sms: 'You need to set a phone or email before sign in.', // UNTRANSLATED
|
||||
suspended: 'This account is suspended.', // UNTRANSLATED
|
||||
},
|
||||
password: {
|
||||
unsupported_encryption_method: 'O método de enncriptação {{name}} não é suportado.',
|
||||
|
|
|
@ -54,6 +54,7 @@ const errors = {
|
|||
require_sms: 'You need to set a phone before sign in.', // UNTRANSLATED
|
||||
sms_exists: 'Your phone has been set.', // UNTRANSLATED
|
||||
require_email_or_sms: 'You need to set a phone or email before sign in.', // UNTRANSLATED
|
||||
suspended: 'This account is suspended.', // UNTRANSLATED
|
||||
},
|
||||
password: {
|
||||
unsupported_encryption_method: '{{name}} şifreleme metodu desteklenmiyor.',
|
||||
|
|
|
@ -53,6 +53,7 @@ const errors = {
|
|||
require_sms: '请绑定手机号码',
|
||||
sms_exists: '已绑定手机号码',
|
||||
require_email_or_sms: '请绑定邮箱地址或手机号码',
|
||||
suspended: '账号已被禁用',
|
||||
},
|
||||
password: {
|
||||
unsupported_encryption_method: '不支持的加密方法 {{name}}',
|
||||
|
|
Loading…
Reference in a new issue