mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(core): remove identities (#6809)
This commit is contained in:
parent
f8d21e49fa
commit
3cfa6843bd
7 changed files with 464 additions and 100 deletions
192
packages/core/src/routes/profile/email-and-phone.ts
Normal file
192
packages/core/src/routes/profile/email-and-phone.ts
Normal file
|
@ -0,0 +1,192 @@
|
|||
import { emailRegEx, phoneRegEx, UserScope } from '@logto/core-kit';
|
||||
import { VerificationType, AccountCenterControlValue, SignInIdentifier } from '@logto/schemas';
|
||||
import { z } from 'zod';
|
||||
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
|
||||
import RequestError from '../../errors/RequestError/index.js';
|
||||
import { buildVerificationRecordByIdAndType } from '../../libraries/verification.js';
|
||||
import assertThat from '../../utils/assert-that.js';
|
||||
import type { UserRouter, RouterInitArgs } from '../types.js';
|
||||
|
||||
export default function emailAndPhoneRoutes<T extends UserRouter>(...args: RouterInitArgs<T>) {
|
||||
const [router, { queries, libraries }] = args;
|
||||
const {
|
||||
users: { updateUserById, findUserById },
|
||||
signInExperiences: { findDefaultSignInExperience },
|
||||
} = queries;
|
||||
|
||||
const {
|
||||
users: { checkIdentifierCollision },
|
||||
} = libraries;
|
||||
|
||||
router.post(
|
||||
'/profile/primary-email',
|
||||
koaGuard({
|
||||
body: z.object({
|
||||
email: z.string().regex(emailRegEx),
|
||||
newIdentifierVerificationRecordId: z.string(),
|
||||
}),
|
||||
status: [204, 400, 401],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { id: userId, scopes, identityVerified } = ctx.auth;
|
||||
assertThat(
|
||||
identityVerified,
|
||||
new RequestError({ code: 'verification_record.permission_denied', status: 401 })
|
||||
);
|
||||
const { email, newIdentifierVerificationRecordId } = ctx.guard.body;
|
||||
const { fields } = ctx.accountCenter;
|
||||
assertThat(
|
||||
fields.email === AccountCenterControlValue.Edit,
|
||||
'account_center.filed_not_editable'
|
||||
);
|
||||
|
||||
assertThat(scopes.has(UserScope.Email), 'auth.unauthorized');
|
||||
|
||||
// Check new identifier
|
||||
const newVerificationRecord = await buildVerificationRecordByIdAndType({
|
||||
type: VerificationType.EmailVerificationCode,
|
||||
id: newIdentifierVerificationRecordId,
|
||||
queries,
|
||||
libraries,
|
||||
});
|
||||
assertThat(newVerificationRecord.isVerified, 'verification_record.not_found');
|
||||
assertThat(newVerificationRecord.identifier.value === email, 'verification_record.not_found');
|
||||
|
||||
await checkIdentifierCollision({ primaryEmail: email }, userId);
|
||||
|
||||
const updatedUser = await updateUserById(userId, { primaryEmail: email });
|
||||
|
||||
ctx.appendDataHookContext('User.Data.Updated', { user: updatedUser });
|
||||
|
||||
ctx.status = 204;
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/profile/primary-email',
|
||||
koaGuard({
|
||||
status: [204, 400, 401],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { id: userId, scopes, identityVerified } = ctx.auth;
|
||||
assertThat(
|
||||
identityVerified,
|
||||
new RequestError({ code: 'verification_record.permission_denied', status: 401 })
|
||||
);
|
||||
const { fields } = ctx.accountCenter;
|
||||
assertThat(
|
||||
fields.email === AccountCenterControlValue.Edit,
|
||||
'account_center.filed_not_editable'
|
||||
);
|
||||
|
||||
assertThat(scopes.has(UserScope.Email), 'auth.unauthorized');
|
||||
|
||||
const { signUp } = await findDefaultSignInExperience();
|
||||
|
||||
if (signUp.identifiers.includes(SignInIdentifier.Email)) {
|
||||
// If email is the only sign-up identifier, we need to keep the email
|
||||
assertThat(signUp.identifiers.includes(SignInIdentifier.Phone), 'user.email_required');
|
||||
// If phone is also a sign-up identifier, check if phone is set
|
||||
const user = await findUserById(userId);
|
||||
assertThat(user.primaryPhone, 'user.email_or_phone_required');
|
||||
}
|
||||
|
||||
const updatedUser = await updateUserById(userId, { primaryEmail: null });
|
||||
|
||||
ctx.appendDataHookContext('User.Data.Updated', { user: updatedUser });
|
||||
|
||||
ctx.status = 204;
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/profile/primary-phone',
|
||||
koaGuard({
|
||||
body: z.object({
|
||||
phone: z.string().regex(phoneRegEx),
|
||||
newIdentifierVerificationRecordId: z.string(),
|
||||
}),
|
||||
status: [204, 400, 401],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { id: userId, scopes, identityVerified } = ctx.auth;
|
||||
assertThat(
|
||||
identityVerified,
|
||||
new RequestError({ code: 'verification_record.permission_denied', status: 401 })
|
||||
);
|
||||
const { phone, newIdentifierVerificationRecordId } = ctx.guard.body;
|
||||
const { fields } = ctx.accountCenter;
|
||||
assertThat(
|
||||
fields.phone === AccountCenterControlValue.Edit,
|
||||
'account_center.filed_not_editable'
|
||||
);
|
||||
|
||||
assertThat(scopes.has(UserScope.Phone), 'auth.unauthorized');
|
||||
|
||||
// Check new identifier
|
||||
const newVerificationRecord = await buildVerificationRecordByIdAndType({
|
||||
type: VerificationType.PhoneVerificationCode,
|
||||
id: newIdentifierVerificationRecordId,
|
||||
queries,
|
||||
libraries,
|
||||
});
|
||||
assertThat(newVerificationRecord.isVerified, 'verification_record.not_found');
|
||||
assertThat(newVerificationRecord.identifier.value === phone, 'verification_record.not_found');
|
||||
|
||||
await checkIdentifierCollision({ primaryPhone: phone }, userId);
|
||||
|
||||
const updatedUser = await updateUserById(userId, { primaryPhone: phone });
|
||||
|
||||
ctx.appendDataHookContext('User.Data.Updated', { user: updatedUser });
|
||||
|
||||
ctx.status = 204;
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/profile/primary-phone',
|
||||
koaGuard({
|
||||
status: [204, 400, 401],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { id: userId, scopes, identityVerified } = ctx.auth;
|
||||
assertThat(
|
||||
identityVerified,
|
||||
new RequestError({ code: 'verification_record.permission_denied', status: 401 })
|
||||
);
|
||||
const { fields } = ctx.accountCenter;
|
||||
assertThat(
|
||||
fields.phone === AccountCenterControlValue.Edit,
|
||||
'account_center.filed_not_editable'
|
||||
);
|
||||
|
||||
assertThat(scopes.has(UserScope.Phone), 'auth.unauthorized');
|
||||
|
||||
const { signUp } = await findDefaultSignInExperience();
|
||||
|
||||
if (signUp.identifiers.includes(SignInIdentifier.Phone)) {
|
||||
// If phone is the only sign-up identifier, we need to keep the phone
|
||||
assertThat(signUp.identifiers.includes(SignInIdentifier.Email), 'user.phone_required');
|
||||
// If email is also a sign-up identifier, check if email is set
|
||||
const user = await findUserById(userId);
|
||||
assertThat(user.primaryEmail, 'user.email_or_phone_required');
|
||||
}
|
||||
|
||||
const updatedUser = await updateUserById(userId, { primaryPhone: null });
|
||||
|
||||
ctx.appendDataHookContext('User.Data.Updated', { user: updatedUser });
|
||||
|
||||
ctx.status = 204;
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
}
|
|
@ -174,6 +174,16 @@
|
|||
"description": "Permission denied, the verification record is invalid."
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"operationId": "DeletePrimaryEmail",
|
||||
"summary": "Delete primary email",
|
||||
"description": "Delete primary email for the user, a verification-record-id in header is required for checking sensitive permissions.",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "The primary email was deleted successfully."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/profile/primary-phone": {
|
||||
|
@ -208,6 +218,16 @@
|
|||
"description": "Permission denied, the verification record is invalid."
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"operationId": "DeletePrimaryPhone",
|
||||
"summary": "Delete primary phone",
|
||||
"description": "Delete primary phone for the user, a verification-record-id in header is required for checking sensitive permissions.",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "The primary phone was deleted successfully."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/profile/identities": {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { emailRegEx, phoneRegEx, usernameRegEx, UserScope } from '@logto/core-kit';
|
||||
import { usernameRegEx, UserScope } from '@logto/core-kit';
|
||||
import {
|
||||
VerificationType,
|
||||
userProfileResponseGuard,
|
||||
userProfileGuard,
|
||||
AccountCenterControlValue,
|
||||
SignInIdentifier,
|
||||
} from '@logto/schemas';
|
||||
import { z } from 'zod';
|
||||
|
||||
|
@ -12,11 +12,11 @@ import koaGuard from '#src/middleware/koa-guard.js';
|
|||
import { EnvSet } from '../../env-set/index.js';
|
||||
import RequestError from '../../errors/RequestError/index.js';
|
||||
import { encryptUserPassword } from '../../libraries/user.utils.js';
|
||||
import { buildVerificationRecordByIdAndType } from '../../libraries/verification.js';
|
||||
import assertThat from '../../utils/assert-that.js';
|
||||
import { PasswordValidator } from '../experience/classes/libraries/password-validator.js';
|
||||
import type { UserRouter, RouterInitArgs } from '../types.js';
|
||||
|
||||
import emailAndPhoneRoutes from './email-and-phone.js';
|
||||
import identitiesRoutes from './identities.js';
|
||||
import koaAccountCenter from './middlewares/koa-account-center.js';
|
||||
import { getAccountCenterFilteredProfile, getScopedProfile } from './utils/get-scoped-profile.js';
|
||||
|
@ -24,7 +24,7 @@ import { getAccountCenterFilteredProfile, getScopedProfile } from './utils/get-s
|
|||
export default function profileRoutes<T extends UserRouter>(...args: RouterInitArgs<T>) {
|
||||
const [router, { queries, libraries }] = args;
|
||||
const {
|
||||
users: { updateUserById, findUserById, deleteUserIdentity },
|
||||
users: { updateUserById, findUserById },
|
||||
signInExperiences: { findDefaultSignInExperience },
|
||||
} = queries;
|
||||
|
||||
|
@ -58,7 +58,7 @@ export default function profileRoutes<T extends UserRouter>(...args: RouterInitA
|
|||
body: z.object({
|
||||
name: z.string().nullable().optional(),
|
||||
avatar: z.string().url().nullable().optional(),
|
||||
username: z.string().regex(usernameRegEx).optional(),
|
||||
username: z.string().regex(usernameRegEx).nullable().optional(),
|
||||
}),
|
||||
response: userProfileResponseGuard.partial(),
|
||||
status: [200, 400, 422],
|
||||
|
@ -84,7 +84,15 @@ export default function profileRoutes<T extends UserRouter>(...args: RouterInitA
|
|||
assertThat(scopes.has(UserScope.Profile), 'auth.unauthorized');
|
||||
|
||||
if (username !== undefined) {
|
||||
await checkIdentifierCollision({ username }, userId);
|
||||
if (username === null) {
|
||||
const { signUp } = await findDefaultSignInExperience();
|
||||
assertThat(
|
||||
!signUp.identifiers.includes(SignInIdentifier.Username),
|
||||
'user.username_required'
|
||||
);
|
||||
} else {
|
||||
await checkIdentifierCollision({ username }, userId);
|
||||
}
|
||||
}
|
||||
|
||||
const updatedUser = await updateUserById(userId, {
|
||||
|
@ -175,97 +183,6 @@ export default function profileRoutes<T extends UserRouter>(...args: RouterInitA
|
|||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/profile/primary-email',
|
||||
koaGuard({
|
||||
body: z.object({
|
||||
email: z.string().regex(emailRegEx),
|
||||
newIdentifierVerificationRecordId: z.string(),
|
||||
}),
|
||||
status: [204, 400, 401],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { id: userId, scopes, identityVerified } = ctx.auth;
|
||||
assertThat(
|
||||
identityVerified,
|
||||
new RequestError({ code: 'verification_record.permission_denied', status: 401 })
|
||||
);
|
||||
const { email, newIdentifierVerificationRecordId } = ctx.guard.body;
|
||||
const { fields } = ctx.accountCenter;
|
||||
assertThat(
|
||||
fields.email === AccountCenterControlValue.Edit,
|
||||
'account_center.filed_not_editable'
|
||||
);
|
||||
|
||||
assertThat(scopes.has(UserScope.Email), 'auth.unauthorized');
|
||||
|
||||
// Check new identifier
|
||||
const newVerificationRecord = await buildVerificationRecordByIdAndType({
|
||||
type: VerificationType.EmailVerificationCode,
|
||||
id: newIdentifierVerificationRecordId,
|
||||
queries,
|
||||
libraries,
|
||||
});
|
||||
assertThat(newVerificationRecord.isVerified, 'verification_record.not_found');
|
||||
assertThat(newVerificationRecord.identifier.value === email, 'verification_record.not_found');
|
||||
|
||||
await checkIdentifierCollision({ primaryEmail: email }, userId);
|
||||
|
||||
const updatedUser = await updateUserById(userId, { primaryEmail: email });
|
||||
|
||||
ctx.appendDataHookContext('User.Data.Updated', { user: updatedUser });
|
||||
|
||||
ctx.status = 204;
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/profile/primary-phone',
|
||||
koaGuard({
|
||||
body: z.object({
|
||||
phone: z.string().regex(phoneRegEx),
|
||||
newIdentifierVerificationRecordId: z.string(),
|
||||
}),
|
||||
status: [204, 400, 401],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { id: userId, scopes, identityVerified } = ctx.auth;
|
||||
assertThat(
|
||||
identityVerified,
|
||||
new RequestError({ code: 'verification_record.permission_denied', status: 401 })
|
||||
);
|
||||
const { phone, newIdentifierVerificationRecordId } = ctx.guard.body;
|
||||
const { fields } = ctx.accountCenter;
|
||||
assertThat(
|
||||
fields.phone === AccountCenterControlValue.Edit,
|
||||
'account_center.filed_not_editable'
|
||||
);
|
||||
|
||||
assertThat(scopes.has(UserScope.Phone), 'auth.unauthorized');
|
||||
|
||||
// Check new identifier
|
||||
const newVerificationRecord = await buildVerificationRecordByIdAndType({
|
||||
type: VerificationType.PhoneVerificationCode,
|
||||
id: newIdentifierVerificationRecordId,
|
||||
queries,
|
||||
libraries,
|
||||
});
|
||||
assertThat(newVerificationRecord.isVerified, 'verification_record.not_found');
|
||||
assertThat(newVerificationRecord.identifier.value === phone, 'verification_record.not_found');
|
||||
|
||||
await checkIdentifierCollision({ primaryPhone: phone }, userId);
|
||||
|
||||
const updatedUser = await updateUserById(userId, { primaryPhone: phone });
|
||||
|
||||
ctx.appendDataHookContext('User.Data.Updated', { user: updatedUser });
|
||||
|
||||
ctx.status = 204;
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
emailAndPhoneRoutes(...args);
|
||||
identitiesRoutes(...args);
|
||||
}
|
||||
|
|
|
@ -24,6 +24,11 @@ export const updatePrimaryEmail = async (
|
|||
headers: { [verificationRecordIdHeader]: verificationRecordId },
|
||||
});
|
||||
|
||||
export const deletePrimaryEmail = async (api: KyInstance, verificationRecordId: string) =>
|
||||
api.delete('api/profile/primary-email', {
|
||||
headers: { [verificationRecordIdHeader]: verificationRecordId },
|
||||
});
|
||||
|
||||
export const updatePrimaryPhone = async (
|
||||
api: KyInstance,
|
||||
phone: string,
|
||||
|
@ -35,6 +40,11 @@ export const updatePrimaryPhone = async (
|
|||
headers: { [verificationRecordIdHeader]: verificationRecordId },
|
||||
});
|
||||
|
||||
export const deletePrimaryPhone = async (api: KyInstance, verificationRecordId: string) =>
|
||||
api.delete('api/profile/primary-phone', {
|
||||
headers: { [verificationRecordIdHeader]: verificationRecordId },
|
||||
});
|
||||
|
||||
export const updateIdentities = async (
|
||||
api: KyInstance,
|
||||
verificationRecordId: string,
|
||||
|
|
|
@ -3,7 +3,13 @@ import { SignInIdentifier } from '@logto/schemas';
|
|||
|
||||
import { enableAllAccountCenterFields } from '#src/api/account-center.js';
|
||||
import { authedAdminApi } from '#src/api/api.js';
|
||||
import { getUserInfo, updatePrimaryEmail, updatePrimaryPhone } from '#src/api/profile.js';
|
||||
import {
|
||||
deletePrimaryEmail,
|
||||
deletePrimaryPhone,
|
||||
getUserInfo,
|
||||
updatePrimaryEmail,
|
||||
updatePrimaryPhone,
|
||||
} from '#src/api/profile.js';
|
||||
import {
|
||||
createAndVerifyVerificationCode,
|
||||
createVerificationRecordByPassword,
|
||||
|
@ -135,6 +141,102 @@ describe('profile (email and phone)', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('DELETE /profile/primary-email', () => {
|
||||
it('should fail if scope is missing', async () => {
|
||||
const { user, username, password } = await createDefaultTenantUserWithPassword();
|
||||
const api = await signInAndGetUserApi(username, password);
|
||||
const verificationRecordId = await createVerificationRecordByPassword(api, password);
|
||||
|
||||
await expectRejects(deletePrimaryEmail(api, verificationRecordId), {
|
||||
code: 'auth.unauthorized',
|
||||
status: 400,
|
||||
});
|
||||
|
||||
await deleteDefaultTenantUser(user.id);
|
||||
});
|
||||
|
||||
it('should fail if verification record is invalid', async () => {
|
||||
const { user, username, password } = await createDefaultTenantUserWithPassword();
|
||||
const api = await signInAndGetUserApi(username, password, {
|
||||
scopes: [UserScope.Profile, UserScope.Email],
|
||||
});
|
||||
|
||||
await expectRejects(deletePrimaryEmail(api, 'invalid-verification-record-id'), {
|
||||
code: 'verification_record.permission_denied',
|
||||
status: 401,
|
||||
});
|
||||
|
||||
await deleteDefaultTenantUser(user.id);
|
||||
});
|
||||
|
||||
it('should fail if email is the only sign-up identifier', async () => {
|
||||
const { user, username, password } = await createDefaultTenantUserWithPassword();
|
||||
const api = await signInAndGetUserApi(username, password, {
|
||||
scopes: [UserScope.Profile, UserScope.Email],
|
||||
});
|
||||
const verificationRecordId = await createVerificationRecordByPassword(api, password);
|
||||
await enableAllPasswordSignInMethods({
|
||||
identifiers: [SignInIdentifier.Email],
|
||||
password: true,
|
||||
verify: true,
|
||||
});
|
||||
|
||||
await expectRejects(deletePrimaryEmail(api, verificationRecordId), {
|
||||
code: 'user.email_required',
|
||||
status: 400,
|
||||
});
|
||||
|
||||
await enableAllPasswordSignInMethods();
|
||||
await deleteDefaultTenantUser(user.id);
|
||||
});
|
||||
|
||||
it('should fail if email or phone is the sign-up identifier', async () => {
|
||||
const { user, username, password } = await createDefaultTenantUserWithPassword();
|
||||
const api = await signInAndGetUserApi(username, password, {
|
||||
scopes: [UserScope.Profile, UserScope.Email],
|
||||
});
|
||||
const verificationRecordId = await createVerificationRecordByPassword(api, password);
|
||||
await enableAllPasswordSignInMethods({
|
||||
identifiers: [SignInIdentifier.Email, SignInIdentifier.Phone],
|
||||
password: true,
|
||||
verify: true,
|
||||
});
|
||||
|
||||
await expectRejects(deletePrimaryEmail(api, verificationRecordId), {
|
||||
code: 'user.email_or_phone_required',
|
||||
status: 400,
|
||||
});
|
||||
|
||||
await enableAllPasswordSignInMethods();
|
||||
await deleteDefaultTenantUser(user.id);
|
||||
});
|
||||
|
||||
it('should be able to delete primary email', async () => {
|
||||
const { user, username, password } = await createDefaultTenantUserWithPassword();
|
||||
const api = await signInAndGetUserApi(username, password, {
|
||||
scopes: [UserScope.Profile, UserScope.Email],
|
||||
});
|
||||
const verificationRecordId = await createVerificationRecordByPassword(api, password);
|
||||
const newEmail = generateEmail();
|
||||
const newVerificationRecordId = await createAndVerifyVerificationCode(api, {
|
||||
type: SignInIdentifier.Email,
|
||||
value: newEmail,
|
||||
});
|
||||
|
||||
await updatePrimaryEmail(api, newEmail, verificationRecordId, newVerificationRecordId);
|
||||
|
||||
const userInfo = await getUserInfo(api);
|
||||
expect(userInfo).toHaveProperty('primaryEmail', newEmail);
|
||||
|
||||
await deletePrimaryEmail(api, verificationRecordId);
|
||||
|
||||
const userInfoAfterDelete = await getUserInfo(api);
|
||||
expect(userInfoAfterDelete).toHaveProperty('primaryEmail', null);
|
||||
|
||||
await deleteDefaultTenantUser(user.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /profile/primary-phone', () => {
|
||||
it('should fail if scope is missing', async () => {
|
||||
const { user, username, password } = await createDefaultTenantUserWithPassword();
|
||||
|
@ -241,4 +343,100 @@ describe('profile (email and phone)', () => {
|
|||
await deleteDefaultTenantUser(user.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /profile/primary-phone', () => {
|
||||
it('should fail if scope is missing', async () => {
|
||||
const { user, username, password } = await createDefaultTenantUserWithPassword();
|
||||
const api = await signInAndGetUserApi(username, password);
|
||||
const verificationRecordId = await createVerificationRecordByPassword(api, password);
|
||||
|
||||
await expectRejects(deletePrimaryPhone(api, verificationRecordId), {
|
||||
code: 'auth.unauthorized',
|
||||
status: 400,
|
||||
});
|
||||
|
||||
await deleteDefaultTenantUser(user.id);
|
||||
});
|
||||
|
||||
it('should fail if verification record is invalid', async () => {
|
||||
const { user, username, password } = await createDefaultTenantUserWithPassword();
|
||||
const api = await signInAndGetUserApi(username, password, {
|
||||
scopes: [UserScope.Profile, UserScope.Phone],
|
||||
});
|
||||
|
||||
await expectRejects(deletePrimaryPhone(api, 'invalid-verification-record-id'), {
|
||||
code: 'verification_record.permission_denied',
|
||||
status: 401,
|
||||
});
|
||||
|
||||
await deleteDefaultTenantUser(user.id);
|
||||
});
|
||||
|
||||
it('should fail if phone is the only sign-up identifier', async () => {
|
||||
const { user, username, password } = await createDefaultTenantUserWithPassword();
|
||||
const api = await signInAndGetUserApi(username, password, {
|
||||
scopes: [UserScope.Profile, UserScope.Phone],
|
||||
});
|
||||
const verificationRecordId = await createVerificationRecordByPassword(api, password);
|
||||
await enableAllPasswordSignInMethods({
|
||||
identifiers: [SignInIdentifier.Phone],
|
||||
password: true,
|
||||
verify: true,
|
||||
});
|
||||
|
||||
await expectRejects(deletePrimaryPhone(api, verificationRecordId), {
|
||||
code: 'user.phone_required',
|
||||
status: 400,
|
||||
});
|
||||
|
||||
await enableAllPasswordSignInMethods();
|
||||
await deleteDefaultTenantUser(user.id);
|
||||
});
|
||||
|
||||
it('should fail if email or phone is the sign-up identifier', async () => {
|
||||
const { user, username, password } = await createDefaultTenantUserWithPassword();
|
||||
const api = await signInAndGetUserApi(username, password, {
|
||||
scopes: [UserScope.Profile, UserScope.Phone],
|
||||
});
|
||||
const verificationRecordId = await createVerificationRecordByPassword(api, password);
|
||||
await enableAllPasswordSignInMethods({
|
||||
identifiers: [SignInIdentifier.Email, SignInIdentifier.Phone],
|
||||
password: true,
|
||||
verify: true,
|
||||
});
|
||||
|
||||
await expectRejects(deletePrimaryPhone(api, verificationRecordId), {
|
||||
code: 'user.email_or_phone_required',
|
||||
status: 400,
|
||||
});
|
||||
|
||||
await enableAllPasswordSignInMethods();
|
||||
await deleteDefaultTenantUser(user.id);
|
||||
});
|
||||
|
||||
it('should be able to delete primary phone', async () => {
|
||||
const { user, username, password } = await createDefaultTenantUserWithPassword();
|
||||
const api = await signInAndGetUserApi(username, password, {
|
||||
scopes: [UserScope.Profile, UserScope.Phone],
|
||||
});
|
||||
const verificationRecordId = await createVerificationRecordByPassword(api, password);
|
||||
const newPhone = generatePhone();
|
||||
const newVerificationRecordId = await createAndVerifyVerificationCode(api, {
|
||||
type: SignInIdentifier.Phone,
|
||||
value: newPhone,
|
||||
});
|
||||
|
||||
await updatePrimaryPhone(api, newPhone, verificationRecordId, newVerificationRecordId);
|
||||
|
||||
const userInfo = await getUserInfo(api);
|
||||
expect(userInfo).toHaveProperty('primaryPhone', newPhone);
|
||||
|
||||
await deletePrimaryPhone(api, verificationRecordId);
|
||||
|
||||
const userInfoAfterDelete = await getUserInfo(api);
|
||||
expect(userInfoAfterDelete).toHaveProperty('primaryPhone', null);
|
||||
|
||||
await deleteDefaultTenantUser(user.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { UserScope } from '@logto/core-kit';
|
||||
import { hookEvents } from '@logto/schemas';
|
||||
import { hookEvents, SignInIdentifier } from '@logto/schemas';
|
||||
|
||||
import { enableAllAccountCenterFields } from '#src/api/account-center.js';
|
||||
import { getUserInfo, updateOtherProfile, updatePassword, updateUser } from '#src/api/profile.js';
|
||||
import { updateSignInExperience } from '#src/api/sign-in-experience.js';
|
||||
import { createVerificationRecordByPassword } from '#src/api/verification-record.js';
|
||||
import { WebHookApiTest } from '#src/helpers/hook.js';
|
||||
import { expectRejects } from '#src/helpers/index.js';
|
||||
|
@ -174,6 +175,27 @@ describe('profile', () => {
|
|||
await deleteDefaultTenantUser(user.id);
|
||||
});
|
||||
|
||||
it('should be able to update username to null', async () => {
|
||||
const { user, username, password } = await createDefaultTenantUserWithPassword();
|
||||
const api = await signInAndGetUserApi(username, password);
|
||||
|
||||
await updateSignInExperience({
|
||||
signUp: {
|
||||
identifiers: [SignInIdentifier.Email],
|
||||
password: true,
|
||||
verify: true,
|
||||
},
|
||||
});
|
||||
const response = await updateUser(api, { username: null });
|
||||
expect(response).toMatchObject({ username: null });
|
||||
await enableAllPasswordSignInMethods();
|
||||
|
||||
const userInfo = await getUserInfo(api);
|
||||
expect(userInfo).toHaveProperty('username', null);
|
||||
|
||||
await deleteDefaultTenantUser(user.id);
|
||||
});
|
||||
|
||||
it('should fail if username is already in use', async () => {
|
||||
const { user, username, password } = await createDefaultTenantUserWithPassword();
|
||||
const { user: user2, username: username2 } = await createDefaultTenantUserWithPassword();
|
||||
|
|
|
@ -37,6 +37,11 @@ const user = {
|
|||
personal_access_token_name_exists: 'Personal access token name already exists.',
|
||||
totp_secret_invalid: 'Invalid TOTP secret supplied.',
|
||||
wrong_backup_code_format: 'Backup code format is invalid.',
|
||||
username_required: 'Username is a required identifier, you can not set it to null.',
|
||||
email_or_phone_required:
|
||||
'Email address or phone number is a required identifier, at least one is required.',
|
||||
email_required: 'Email address is a required identifier, you can not set it to null.',
|
||||
phone_required: 'Phone number is a required identifier, you can not set it to null.',
|
||||
};
|
||||
|
||||
export default Object.freeze(user);
|
||||
|
|
Loading…
Reference in a new issue