0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-06 20:40:08 -05:00

Merge pull request #2359 from logto-io/charles-log-4570-improve-admin-patch-user-api

refactor(core): allowing more editable properties for patch user API
This commit is contained in:
Charles Zhao 2022-11-10 12:42:52 +08:00 committed by GitHub
commit 970e7a626a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 247 additions and 93 deletions

View file

@ -1,5 +1,5 @@
import type { CreateUser, Role, User } from '@logto/schemas';
import { userInfoSelectFields } from '@logto/schemas';
import { SignUpIdentifier, userInfoSelectFields } from '@logto/schemas';
import pick from 'lodash.pick';
import { mockUser, mockUserList, mockUserListResponse, mockUserResponse } from '@/__mocks__';
@ -23,6 +23,21 @@ const filterUsersWithSearch = (users: User[], search: string) =>
)
);
const mockFindDefaultSignInExperience = jest.fn(async () => ({
signUp: {
identifier: SignUpIdentifier.None,
password: false,
verify: false,
},
}));
jest.mock('@/queries/sign-in-experience', () => ({
findDefaultSignInExperience: jest.fn(async () => mockFindDefaultSignInExperience()),
}));
const mockHasUser = jest.fn(async () => false);
const mockHasUserWithEmail = jest.fn(async () => false);
const mockHasUserWithPhone = jest.fn(async () => false);
jest.mock('@/queries/user', () => ({
countUsers: jest.fn(async (search) => ({
count: search ? filterUsersWithSearch(mockUserList, search).length : mockUserList.length,
@ -32,7 +47,9 @@ jest.mock('@/queries/user', () => ({
search ? filterUsersWithSearch(mockUserList, search) : mockUserList
),
findUserById: jest.fn(async (): Promise<User> => mockUser),
hasUser: jest.fn(async () => false),
hasUser: jest.fn(async () => mockHasUser()),
hasUserWithEmail: jest.fn(async () => mockHasUserWithEmail()),
hasUserWithPhone: jest.fn(async () => mockHasUserWithPhone()),
updateUserById: jest.fn(
async (_, data: Partial<CreateUser>): Promise<User> => ({
...mockUser,
@ -99,7 +116,7 @@ describe('adminUserRoutes', () => {
it('POST /users', async () => {
const username = 'MJAtLogto';
const password = 'PASSWORD';
const name = 'Micheal';
const name = 'Michael';
const response = await userRequest.post('/users').send({ username, password, name });
expect(response.status).toEqual(200);
@ -114,7 +131,7 @@ describe('adminUserRoutes', () => {
it('POST /users should throw with invalid input params', async () => {
const username = 'MJAtLogto';
const password = 'PASSWORD';
const name = 'Micheal';
const name = 'Michael';
// Missing input
await expect(userRequest.post('/users').send({})).resolves.toHaveProperty('status', 400);
@ -137,13 +154,13 @@ describe('adminUserRoutes', () => {
).resolves.toHaveProperty('status', 400);
});
it('POST /users should throw if username exist', async () => {
it('POST /users should throw if username exists', async () => {
const mockHasUser = hasUser as jest.Mock;
mockHasUser.mockImplementationOnce(async () => true);
const username = 'MJAtLogto';
const password = 'PASSWORD';
const name = 'Micheal';
const name = 'Michael';
await expect(
userRequest.post('/users').send({ username, password, name })
@ -151,20 +168,29 @@ describe('adminUserRoutes', () => {
});
it('PATCH /users/:userId', async () => {
const name = 'Micheal';
const avatar = 'http://www.micheal.png';
const name = 'Michael';
const avatar = 'http://www.michael.png';
const primaryEmail = 'bar@logto.io';
const primaryPhone = '222222';
const username = 'bar';
const response = await userRequest
.patch('/users/foo')
.send({ username, name, avatar, primaryEmail, primaryPhone });
const response = await userRequest.patch('/users/foo').send({ name, avatar });
expect(response.status).toEqual(200);
expect(response.body).toEqual({
...mockUserResponse,
primaryEmail,
primaryPhone,
username,
name,
avatar,
});
});
it('PATCH /users/:userId should allow updated with empty avatar', async () => {
const name = 'Micheal';
it('PATCH /users/:userId should allow empty avatar URL', async () => {
const name = 'Michael';
const avatar = '';
const response = await userRequest.patch('/users/foo').send({ name, avatar });
@ -176,8 +202,8 @@ describe('adminUserRoutes', () => {
});
});
it('PATCH /users/:userId should updated with one field if the other is undefined', async () => {
const name = 'Micheal';
it('PATCH /users/:userId should allow partial update', async () => {
const name = 'Michael';
const updateNameResponse = await userRequest.patch('/users/foo').send({ name });
expect(updateNameResponse.status).toEqual(200);
@ -186,7 +212,7 @@ describe('adminUserRoutes', () => {
name,
});
const avatar = 'https://www.miceal.png';
const avatar = 'https://www.michael.png';
const updateAvatarResponse = await userRequest.patch('/users/foo').send({ avatar });
expect(updateAvatarResponse.status).toEqual(200);
expect(updateAvatarResponse.body).toEqual({
@ -195,9 +221,9 @@ describe('adminUserRoutes', () => {
});
});
it('PATCH /users/:userId throw with invalid input params', async () => {
const name = 'Micheal';
const avatar = 'http://www.micheal.png';
it('PATCH /users/:userId should throw when avatar URL is invalid', async () => {
const name = 'Michael';
const avatar = 'http://www.michael.png';
await expect(userRequest.patch('/users/foo').send({ avatar })).resolves.toHaveProperty(
'status',
@ -209,9 +235,9 @@ describe('adminUserRoutes', () => {
).resolves.toHaveProperty('status', 400);
});
it('PATCH /users/:userId throw if user not found', async () => {
const name = 'Micheal';
const avatar = 'http://www.micheal.png';
it('PATCH /users/:userId should throw if user cannot be found', async () => {
const name = 'Michael';
const avatar = 'http://www.michael.png';
const mockFindUserById = findUserById as jest.Mock;
mockFindUserById.mockImplementationOnce(() => {
@ -225,6 +251,88 @@ describe('adminUserRoutes', () => {
expect(updateUserById).not.toBeCalled();
});
it('PATCH /users/:userId should throw if required sign-up identifier username is missing', async () => {
mockFindDefaultSignInExperience.mockImplementationOnce(async () => ({
signUp: {
identifier: SignUpIdentifier.Username,
password: false,
verify: false,
},
}));
await expect(
userRequest
.patch('/users/foo')
.send({ primaryEmail: 'test@abc.com', primaryPhone: '18688886666' })
).resolves.toHaveProperty('status', 422);
});
it('PATCH /users/:userId should throw if required sign-up identifier email is missing', async () => {
mockFindDefaultSignInExperience.mockImplementationOnce(async () => ({
signUp: {
identifier: SignUpIdentifier.Email,
password: false,
verify: false,
},
}));
await expect(
userRequest.patch('/users/foo').send({ username: 'test', primaryPhone: '18688886666' })
).resolves.toHaveProperty('status', 422);
});
it('PATCH /users/:userId should throw if required sign-up identifier phone is missing', async () => {
mockFindDefaultSignInExperience.mockImplementationOnce(async () => ({
signUp: {
identifier: SignUpIdentifier.Sms,
password: false,
verify: false,
},
}));
await expect(
userRequest.patch('/users/foo').send({ username: 'test', primaryEmail: 'test@abc.com' })
).resolves.toHaveProperty('status', 422);
});
it('PATCH /users/:userId should throw if required sign-up identifiers email and phone are both missing', async () => {
mockFindDefaultSignInExperience.mockImplementationOnce(async () => ({
signUp: {
identifier: SignUpIdentifier.EmailOrSms,
password: false,
verify: false,
},
}));
await expect(
userRequest.patch('/users/foo').send({ username: 'test' })
).resolves.toHaveProperty('status', 422);
});
it('PATCH /users/:userId should throw if new username is already in use', async () => {
mockHasUser.mockImplementationOnce(async () => true);
await expect(
userRequest.patch('/users/foo').send({ username: 'test' })
).resolves.toHaveProperty('status', 422);
});
it('PATCH /users/:userId should throw if new email has already linked to other accounts', async () => {
mockHasUserWithEmail.mockImplementationOnce(async () => true);
await expect(
userRequest.patch('/users/foo').send({ primaryEmail: 'test@email.com' })
).resolves.toHaveProperty('status', 422);
});
it('PATCH /users/:userId should throw if new phone number has already linked to other accounts', async () => {
mockHasUserWithPhone.mockImplementationOnce(async () => true);
await expect(
userRequest.patch('/users/foo').send({ primaryPhone: '18688886666' })
).resolves.toHaveProperty('status', 422);
});
it('PATCH /users/:userId should throw if role names are invalid', async () => {
const mockedFindRolesByRoleNames = findRolesByRoleNames as jest.Mock;
mockedFindRolesByRoleNames.mockImplementationOnce(
@ -263,7 +371,7 @@ describe('adminUserRoutes', () => {
});
});
it('PATCH /users/:userId/password throw if user not found', async () => {
it('PATCH /users/:userId/password should throw if user cannot be found', async () => {
const notExistedUserId = 'notExistedUserId';
const dummyPassword = '123456';
const mockedFindUserById = findUserById as jest.Mock;
@ -293,8 +401,8 @@ describe('adminUserRoutes', () => {
expect(deleteUserIdentity).not.toHaveBeenCalled();
});
it('DELETE /users/:userId should throw if user not found', async () => {
const notExistedUserId = 'notExisitedUserId';
it('DELETE /users/:userId should throw if user cannot be found', async () => {
const notExistedUserId = 'notExistedUserId';
const mockedFindUserById = findUserById as jest.Mock;
mockedFindUserById.mockImplementationOnce((userId) => {
if (userId === notExistedUserId) {
@ -308,8 +416,8 @@ describe('adminUserRoutes', () => {
expect(deleteUserById).not.toHaveBeenCalled();
});
it('DELETE /users/:userId/identities/:target should throw if user not found', async () => {
const notExistedUserId = 'notExisitedUserId';
it('DELETE /users/:userId/identities/:target should throw if user cannot be found', async () => {
const notExistedUserId = 'notExistedUserId';
const arbitraryTarget = 'arbitraryTarget';
const mockedFindUserById = findUserById as jest.Mock;
mockedFindUserById.mockImplementationOnce((userId) => {
@ -323,9 +431,9 @@ describe('adminUserRoutes', () => {
expect(deleteUserIdentity).not.toHaveBeenCalled();
});
it('DELETE /users/:userId/identities/:target should throw if user found and connector is not found', async () => {
it('DELETE /users/:userId/identities/:target should throw if user is found but connector cannot be found', async () => {
const arbitraryUserId = 'arbitraryUserId';
const nonexistentTarget = 'nonexistentTarget';
const nonExistedTarget = 'nonExistedTarget';
const mockedFindUserById = findUserById as jest.Mock;
mockedFindUserById.mockImplementationOnce((userId) => {
if (userId === arbitraryUserId) {
@ -333,7 +441,7 @@ describe('adminUserRoutes', () => {
}
});
await expect(
userRequest.delete(`/users/${arbitraryUserId}/identities/${nonexistentTarget}`)
userRequest.delete(`/users/${arbitraryUserId}/identities/${nonExistedTarget}`)
).resolves.toHaveProperty('status', 404);
expect(deleteUserIdentity).not.toHaveBeenCalled();
});

View file

@ -1,4 +1,4 @@
import { passwordRegEx, usernameRegEx } from '@logto/core-kit';
import { emailRegEx, passwordRegEx, phoneRegEx, usernameRegEx } from '@logto/core-kit';
import { arbitraryObjectGuard, userInfoSelectFields } from '@logto/schemas';
import { has } from '@silverhand/essentials';
import pick from 'lodash.pick';
@ -21,6 +21,7 @@ import {
} from '@/queries/user';
import assertThat from '@/utils/assert-that';
import { checkExistingSignUpIdentifiers, checkRequiredSignUpIdentifiers } from './session/utils';
import type { AuthedRouter } from './types';
export default function adminUserRoutes<T extends AuthedRouter>(router: T) {
@ -158,6 +159,9 @@ export default function adminUserRoutes<T extends AuthedRouter>(router: T) {
koaGuard({
params: object({ userId: string() }),
body: object({
username: string().regex(usernameRegEx).optional(),
primaryEmail: string().regex(emailRegEx).optional(),
primaryPhone: string().regex(phoneRegEx).optional(),
name: string().nullable().optional(),
avatar: string().url().or(literal('')).nullable().optional(),
customData: arbitraryObjectGuard.optional(),
@ -171,6 +175,8 @@ export default function adminUserRoutes<T extends AuthedRouter>(router: T) {
} = ctx.guard;
await findUserById(userId);
await checkRequiredSignUpIdentifiers(body);
await checkExistingSignUpIdentifiers(body);
// Temp solution to validate the existence of input roleNames
if (body.roleNames?.length) {
@ -191,13 +197,7 @@ export default function adminUserRoutes<T extends AuthedRouter>(router: T) {
}
}
const user = await updateUserById(
userId,
{
...body,
},
'replace'
);
const user = await updateUserById(userId, body, 'replace');
ctx.body = pick(user, ...userInfoSelectFields);

View file

@ -19,7 +19,7 @@ import { assignInteractionResults } from '@/lib/session';
import { verifyUserPassword } from '@/lib/user';
import type { LogContext } from '@/middleware/koa-log';
import { findDefaultSignInExperience } from '@/queries/sign-in-experience';
import { updateUserById } from '@/queries/user';
import { hasUser, hasUserWithEmail, hasUserWithPhone, updateUserById } from '@/queries/user';
import assertThat from '@/utils/assert-that';
import { continueSignInTimeout, verificationTimeout } from './consts';
@ -189,8 +189,54 @@ export const checkRequiredProfile = async (
throw new RequestError({ code: 'user.require_email_or_sms', status: 422 });
}
};
export const checkRequiredSignUpIdentifiers = async (identifiers: {
username?: string;
primaryEmail?: string;
primaryPhone?: string;
}) => {
const { username, primaryEmail, primaryPhone } = identifiers;
const { signUp } = await findDefaultSignInExperience();
if (signUp.identifier === SignUpIdentifier.Username && !username) {
throw new RequestError({ code: 'user.require_username', status: 422 });
}
if (signUp.identifier === SignUpIdentifier.Email && !primaryEmail) {
throw new RequestError({ code: 'user.require_email', status: 422 });
}
if (signUp.identifier === SignUpIdentifier.Sms && !primaryPhone) {
throw new RequestError({ code: 'user.require_sms', status: 422 });
}
if (signUp.identifier === SignUpIdentifier.EmailOrSms && !primaryEmail && !primaryPhone) {
throw new RequestError({ code: 'user.require_email_or_sms', status: 422 });
}
};
/* eslint-enable complexity */
export const checkExistingSignUpIdentifiers = async (identifiers: {
username?: string;
primaryEmail?: string;
primaryPhone?: string;
}) => {
const { username, primaryEmail, primaryPhone } = identifiers;
if (username && (await hasUser(username))) {
throw new RequestError({ code: 'user.username_exists', status: 422 });
}
if (primaryEmail && (await hasUserWithEmail(primaryEmail))) {
throw new RequestError({ code: 'user.email_exists', status: 422 });
}
if (primaryPhone && (await hasUserWithPhone(primaryPhone))) {
throw new RequestError({ code: 'user.sms_exists', status: 422 });
}
};
type SignInWithPasswordParameter = {
identifier: SignInIdentifier;
password: string;

View file

@ -46,15 +46,15 @@ const errors = {
sign_up_method_not_enabled: 'This sign up method is not enabled.', // UNTRANSLATED
sign_in_method_not_enabled: 'This sign in method is not enabled.', // UNTRANSLATED
same_password: 'Das neue Passwort muss sich vom alten unterscheiden.',
require_password: 'You need to set a password before sign in.', // UNTRANSLATED
require_password: 'You need to set a password before signing-in.', // UNTRANSLATED
password_exists: 'Your password has been set.', // UNTRANSLATED
require_username: 'You need to set a username before sign in.', // UNTRANSLATED
username_exists: 'Your username has been set.', // UNTRANSLATED
require_email: 'You need to set an email before sign in.', // UNTRANSLATED
email_exists: 'Your email has been set.', // UNTRANSLATED
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
require_username: 'You need to set a username before signing-in.', // UNTRANSLATED
username_exists: 'This username is already in use.', // UNTRANSLATED
require_email: 'You need to add an email address before signing-in.', // UNTRANSLATED
email_exists: 'This email is associated with an existing account.', // UNTRANSLATED
require_sms: 'You need to add a phone number before signing-in.', // UNTRANSLATED
sms_exists: 'This phone number is associated with an existing account.', // UNTRANSLATED
require_email_or_sms: 'You need to add an email address or phone number before signing-in.', // UNTRANSLATED
},
password: {
unsupported_encryption_method: 'Die Verschlüsselungsmethode {{name}} wird nicht unterstützt.',

View file

@ -45,16 +45,16 @@ const errors = {
cannot_delete_self: 'You cannot delete yourself.',
sign_up_method_not_enabled: 'This sign up method is not enabled.',
sign_in_method_not_enabled: 'This sign in method is not enabled.',
same_password: 'Your new password cant be the same as your current password.',
require_password: 'You need to set a password before sign in.',
same_password: 'New password cannot be the same as your old password.',
require_password: 'You need to set a password before signing-in.',
password_exists: 'Your password has been set.',
require_username: 'You need to set a username before sign in.',
username_exists: 'Your username has been set.',
require_email: 'You need to set an email before sign in.',
email_exists: 'Your email has been set.',
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.',
require_username: 'You need to set a username before signing-in.',
username_exists: 'This username is already in use.',
require_email: 'You need to add an email address before signing-in.',
email_exists: 'This email is associated with an existing account.',
require_sms: 'You need to add a phone number before signing-in.',
sms_exists: 'This phone number is associated with an existing account.',
require_email_or_sms: 'You need to add an email address or phone number before signing-in.',
},
password: {
unsupported_encryption_method: 'The encryption method {{name}} is not supported.',

View file

@ -46,16 +46,16 @@ const errors = {
cannot_delete_self: 'You cannot delete yourself.', // UNTRANSLATED
sign_up_method_not_enabled: 'This sign up method is not enabled.', // UNTRANSLATED
sign_in_method_not_enabled: 'This sign in method is not enabled.', // UNTRANSLATED
same_password: 'Your new password cant be the same as your current password.', // UNTRANSLATED
require_password: 'You need to set a password before sign in.', // UNTRANSLATED
same_password: 'New password cannot be the same as your old password.', // UNTRANSLATED
require_password: 'You need to set a password before signing-in.', // UNTRANSLATED
password_exists: 'Your password has been set.', // UNTRANSLATED
require_username: 'You need to set a username before sign in.', // UNTRANSLATED
username_exists: 'Your username has been set.', // UNTRANSLATED
require_email: 'You need to set an email before sign in.', // UNTRANSLATED
email_exists: 'Your email has been set.', // UNTRANSLATED
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
require_username: 'You need to set a username before signing-in.', // UNTRANSLATED
username_exists: 'This username is already in use.', // UNTRANSLATED
require_email: 'You need to add an email address before signing-in.', // UNTRANSLATED
email_exists: 'This email is associated with an existing account.', // UNTRANSLATED
require_sms: 'You need to add a phone number before signing-in.', // UNTRANSLATED
sms_exists: 'This phone number is associated with an existing account.', // UNTRANSLATED
require_email_or_sms: 'You need to add an email address or phone number before signing-in.', // UNTRANSLATED
},
password: {
unsupported_encryption_method: "La méthode de cryptage {{name}} n'est pas prise en charge.",

View file

@ -44,16 +44,16 @@ const errors = {
cannot_delete_self: 'You cannot delete yourself.', // UNTRANSLATED
sign_up_method_not_enabled: 'This sign up method is not enabled.', // UNTRANSLATED
sign_in_method_not_enabled: 'This sign in method is not enabled.', // UNTRANSLATED
same_password: 'Your new password cant be the same as your current password.', // UNTRANSLATED
require_password: 'You need to set a password before sign in.', // UNTRANSLATED
same_password: 'New password cannot be the same as your old password.', // UNTRANSLATED
require_password: 'You need to set a password before signing-in.', // UNTRANSLATED
password_exists: 'Your password has been set.', // UNTRANSLATED
require_username: 'You need to set a username before sign in.', // UNTRANSLATED
username_exists: 'Your username has been set.', // UNTRANSLATED
require_email: 'You need to set an email before sign in.', // UNTRANSLATED
email_exists: 'Your email has been set.', // UNTRANSLATED
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
require_username: 'You need to set a username before signing-in.', // UNTRANSLATED
username_exists: 'This username is already in use.', // UNTRANSLATED
require_email: 'You need to add an email address before signing-in.', // UNTRANSLATED
email_exists: 'This email is associated with an existing account.', // UNTRANSLATED
require_sms: 'You need to add a phone number before signing-in.', // UNTRANSLATED
sms_exists: 'This phone number is associated with an existing account.', // UNTRANSLATED
require_email_or_sms: 'You need to add an email address or phone number before signing-in.', // UNTRANSLATED
},
password: {
unsupported_encryption_method: '{{name}} 암호화 방법을 지원하지 않아요.',

View file

@ -44,16 +44,16 @@ const errors = {
cannot_delete_self: 'Não se pode remover a si mesmo.',
sign_up_method_not_enabled: 'This sign up method is not enabled.', // UNTRANSLATED
sign_in_method_not_enabled: 'This sign in method is not enabled.', // UNTRANSLATED
same_password: 'Your new password cant be the same as your current password.', // UNTRANSLATED
require_password: 'You need to set a password before sign in.', // UNTRANSLATED
same_password: 'New password cannot be the same as your old password.', // UNTRANSLATED
require_password: 'You need to set a password before signing-in.', // UNTRANSLATED
password_exists: 'Your password has been set.', // UNTRANSLATED
require_username: 'You need to set a username before sign in.', // UNTRANSLATED
username_exists: 'Your username has been set.', // UNTRANSLATED
require_email: 'You need to set an email before sign in.', // UNTRANSLATED
email_exists: 'Your email has been set.', // UNTRANSLATED
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
require_username: 'You need to set a username before signing-in.', // UNTRANSLATED
username_exists: 'This username is already in use.', // UNTRANSLATED
require_email: 'You need to add an email address before signing-in.', // UNTRANSLATED
email_exists: 'This email is associated with an existing account.', // UNTRANSLATED
require_sms: 'You need to add a phone number before signing-in.', // UNTRANSLATED
sms_exists: 'This phone number is associated with an existing account.', // UNTRANSLATED
require_email_or_sms: 'You need to add an email address or phone number before signing-in.', // UNTRANSLATED
},
password: {
unsupported_encryption_method: 'O método de enncriptação {{name}} não é suportado.',

View file

@ -45,16 +45,16 @@ const errors = {
cannot_delete_self: 'You cannot delete yourself.', // UNTRANSLATED
sign_up_method_not_enabled: 'This sign up method is not enabled.', // UNTRANSLATED
sign_in_method_not_enabled: 'This sign in method is not enabled.', // UNTRANSLATED
same_password: 'Your new password cant be the same as your current password.', // UNTRANSLATED
require_password: 'You need to set a password before sign in.', // UNTRANSLATED
same_password: 'New password cannot be the same as your old password.', // UNTRANSLATED
require_password: 'You need to set a password before signing-in.', // UNTRANSLATED
password_exists: 'Your password has been set.', // UNTRANSLATED
require_username: 'You need to set a username before sign in.', // UNTRANSLATED
username_exists: 'Your username has been set.', // UNTRANSLATED
require_email: 'You need to set an email before sign in.', // UNTRANSLATED
email_exists: 'Your email has been set.', // UNTRANSLATED
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
require_username: 'You need to set a username before signing-in.', // UNTRANSLATED
username_exists: 'This username is already in use.', // UNTRANSLATED
require_email: 'You need to add an email address before signing-in.', // UNTRANSLATED
email_exists: 'This email is associated with an existing account.', // UNTRANSLATED
require_sms: 'You need to add a phone number before signing-in.', // UNTRANSLATED
sms_exists: 'This phone number is associated with an existing account.', // UNTRANSLATED
require_email_or_sms: 'You need to add an email address or phone number before signing-in.', // UNTRANSLATED
},
password: {
unsupported_encryption_method: '{{name}} şifreleme metodu desteklenmiyor.',

View file

@ -48,11 +48,11 @@ const errors = {
require_password: '请设置密码',
password_exists: '密码已设置过',
require_username: '请设置用户名',
username_exists: '用户名已设置过',
username_exists: '该用户名已存在',
require_email: '请绑定邮箱地址',
email_exists: '已绑定邮箱地址',
email_exists: '该邮箱地址已被其它账户绑定',
require_sms: '请绑定手机号码',
sms_exists: '已绑定手机号码',
sms_exists: '该手机号码已被其它账户绑定',
require_email_or_sms: '请绑定邮箱地址或手机号码',
},
password: {