0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

refactor(core): rename prefix to account (#6814)

This commit is contained in:
wangsijie 2024-11-18 14:02:04 +08:00 committed by GitHub
parent 3cfa6843bd
commit 4658bd2623
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 321 additions and 142 deletions

View file

@ -11,6 +11,7 @@
"paths": {
"/api/account-center": {
"get": {
"operationId": "GetAccountCenterSettings",
"summary": "Get account center settings",
"description": "Get the account center settings.",
"responses": {
@ -20,6 +21,7 @@
}
},
"patch": {
"operationId": "UpdateAccountCenterSettings",
"summary": "Update account center settings",
"description": "Update the account center settings with the provided settings.",
"requestBody": {

View file

@ -0,0 +1 @@
export const accountApiPrefix = '/account';

View file

@ -9,6 +9,8 @@ import { buildVerificationRecordByIdAndType } from '../../libraries/verification
import assertThat from '../../utils/assert-that.js';
import type { UserRouter, RouterInitArgs } from '../types.js';
import { accountApiPrefix } from './constants.js';
export default function emailAndPhoneRoutes<T extends UserRouter>(...args: RouterInitArgs<T>) {
const [router, { queries, libraries }] = args;
const {
@ -21,7 +23,7 @@ export default function emailAndPhoneRoutes<T extends UserRouter>(...args: Route
} = libraries;
router.post(
'/profile/primary-email',
`${accountApiPrefix}/primary-email`,
koaGuard({
body: z.object({
email: z.string().regex(emailRegEx),
@ -67,7 +69,7 @@ export default function emailAndPhoneRoutes<T extends UserRouter>(...args: Route
);
router.delete(
'/profile/primary-email',
`${accountApiPrefix}/primary-email`,
koaGuard({
status: [204, 400, 401],
}),
@ -106,7 +108,7 @@ export default function emailAndPhoneRoutes<T extends UserRouter>(...args: Route
);
router.post(
'/profile/primary-phone',
`${accountApiPrefix}/primary-phone`,
koaGuard({
body: z.object({
phone: z.string().regex(phoneRegEx),
@ -152,7 +154,7 @@ export default function emailAndPhoneRoutes<T extends UserRouter>(...args: Route
);
router.delete(
'/profile/primary-phone',
`${accountApiPrefix}/primary-phone`,
koaGuard({
status: [204, 400, 401],
}),

View file

@ -9,6 +9,8 @@ import { buildVerificationRecordByIdAndType } from '../../libraries/verification
import assertThat from '../../utils/assert-that.js';
import type { UserRouter, RouterInitArgs } from '../types.js';
import { accountApiPrefix } from './constants.js';
export default function identitiesRoutes<T extends UserRouter>(
...[router, { queries, libraries }]: RouterInitArgs<T>
) {
@ -21,7 +23,7 @@ export default function identitiesRoutes<T extends UserRouter>(
} = libraries;
router.post(
'/profile/identities',
`${accountApiPrefix}/identities`,
koaGuard({
body: z.object({
newIdentifierVerificationRecordId: z.string(),
@ -81,7 +83,7 @@ export default function identitiesRoutes<T extends UserRouter>(
);
router.delete(
'/profile/identities/:target',
`${accountApiPrefix}/identities/:target`,
koaGuard({
params: z.object({ target: z.string() }),
status: [204, 400, 401, 404],

View file

@ -1,15 +1,15 @@
{
"tags": [
{
"name": "Profile",
"description": "Profile routes provide functionality for managing user profiles for the end user to interact directly with access tokens."
"name": "Account",
"description": "Account routes provide functionality for managing user profile for the end user to interact directly with access tokens."
},
{
"name": "Dev feature"
}
],
"paths": {
"/api/profile": {
"/api/account": {
"get": {
"operationId": "GetProfile",
"summary": "Get profile",
@ -56,7 +56,7 @@
}
}
},
"/api/profile/profile": {
"/api/account/profile": {
"patch": {
"operationId": "UpdateOtherProfile",
"summary": "Update other profile",
@ -114,7 +114,7 @@
}
}
},
"/api/profile/password": {
"/api/account/password": {
"post": {
"operationId": "UpdatePassword",
"summary": "Update password",
@ -142,7 +142,7 @@
}
}
},
"/api/profile/primary-email": {
"/api/account/primary-email": {
"post": {
"operationId": "UpdatePrimaryEmail",
"summary": "Update primary email",
@ -186,7 +186,7 @@
}
}
},
"/api/profile/primary-phone": {
"/api/account/primary-phone": {
"post": {
"operationId": "UpdatePrimaryPhone",
"summary": "Update primary phone",
@ -230,7 +230,7 @@
}
}
},
"/api/profile/identities": {
"/api/account/identities": {
"post": {
"operationId": "AddUserIdentities",
"summary": "Add a user identity",
@ -255,7 +255,7 @@
}
}
},
"/api/profile/identities/{target}": {
"/api/account/identities/{target}": {
"delete": {
"operationId": "DeleteIdentity",
"summary": "Delete a user identity",

View file

@ -16,12 +16,13 @@ import assertThat from '../../utils/assert-that.js';
import { PasswordValidator } from '../experience/classes/libraries/password-validator.js';
import type { UserRouter, RouterInitArgs } from '../types.js';
import { accountApiPrefix } from './constants.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';
export default function profileRoutes<T extends UserRouter>(...args: RouterInitArgs<T>) {
export default function accountRoutes<T extends UserRouter>(...args: RouterInitArgs<T>) {
const [router, { queries, libraries }] = args;
const {
users: { updateUserById, findUserById },
@ -39,7 +40,7 @@ export default function profileRoutes<T extends UserRouter>(...args: RouterInitA
}
router.get(
'/profile',
`${accountApiPrefix}`,
koaGuard({
response: userProfileResponseGuard.partial(),
status: [200],
@ -53,7 +54,7 @@ export default function profileRoutes<T extends UserRouter>(...args: RouterInitA
);
router.patch(
'/profile',
`${accountApiPrefix}`,
koaGuard({
body: z.object({
name: z.string().nullable().optional(),
@ -111,7 +112,7 @@ export default function profileRoutes<T extends UserRouter>(...args: RouterInitA
);
router.patch(
'/profile/profile',
`${accountApiPrefix}/profile`,
koaGuard({
body: userProfileGuard,
response: userProfileGuard,
@ -146,7 +147,7 @@ export default function profileRoutes<T extends UserRouter>(...args: RouterInitA
);
router.post(
'/profile/password',
`${accountApiPrefix}/password`,
koaGuard({
body: z.object({ password: z.string().min(1) }),
status: [204, 400, 401, 422],

View file

@ -13,6 +13,8 @@ import koaAuth from '../middleware/koa-auth/index.js';
import koaOidcAuth from '../middleware/koa-auth/koa-oidc-auth.js';
import koaCors from '../middleware/koa-cors.js';
import { accountApiPrefix } from './account/constants.js';
import accountRoutes from './account/index.js';
import accountCentersRoutes from './account-center/index.js';
import adminUserRoutes from './admin-user/index.js';
import applicationOrganizationRoutes from './applications/application-organization.js';
@ -34,7 +36,6 @@ import interactionRoutes from './interaction/index.js';
import logRoutes from './log.js';
import logtoConfigRoutes from './logto-config/index.js';
import organizationRoutes from './organization/index.js';
import profileRoutes from './profile/index.js';
import resourceRoutes from './resource.js';
import resourceScopeRoutes from './resource.scope.js';
import roleRoutes from './role.js';
@ -47,7 +48,7 @@ import swaggerRoutes from './swagger/index.js';
import systemRoutes from './system.js';
import type { AnonymousRouter, ManagementApiRouter, UserRouter } from './types.js';
import userAssetsRoutes from './user-assets.js';
import verificationRoutes from './verification/index.js';
import verificationRoutes, { verificationApiPrefix } from './verification/index.js';
import verificationCodeRoutes from './verification-code.js';
import wellKnownRoutes from './well-known/index.js';
import wellKnownOpenApiRoutes from './well-known/well-known.openapi.js';
@ -107,7 +108,7 @@ const createRouters = (tenant: TenantContext) => {
userRouter.use(koaOidcAuth(tenant));
// TODO(LOG-10147): Rename to koaApiHooks, this middleware is used for both management API and user API
userRouter.use(koaManagementApiHooks(tenant.libraries.hooks));
profileRoutes(userRouter, tenant);
accountRoutes(userRouter, tenant);
verificationRoutes(userRouter, tenant);
wellKnownRoutes(anonymousRouter, tenant);
@ -136,7 +137,7 @@ const createRouters = (tenant: TenantContext) => {
export default function initApis(tenant: TenantContext): Koa {
const apisApp = new Koa();
const { adminUrlSet, cloudUrlSet } = EnvSet.values;
apisApp.use(koaCors([adminUrlSet, cloudUrlSet], ['/profile', '/verifications']));
apisApp.use(koaCors([adminUrlSet, cloudUrlSet], [accountApiPrefix, verificationApiPrefix]));
apisApp.use(koaBodyEtag());
for (const router of createRouters(tenant)) {

View file

@ -124,7 +124,7 @@ const exceptionPrefixes = Object.freeze([
'/interaction',
'/experience',
'/sign-in-exp/default/check-password',
'/profile',
'/account',
'/verifications',
]);

View file

@ -7,7 +7,7 @@ import type { WithI18nContext } from '#src/middleware/koa-i18next.js';
import { type WithHookContext } from '#src/middleware/koa-management-api-hooks.js';
import type TenantContext from '#src/tenants/TenantContext.js';
import { type WithAccountCenterContext } from './profile/middlewares/koa-account-center.js';
import { type WithAccountCenterContext } from './account/middlewares/koa-account-center.js';
export type AnonymousRouter = Router<unknown, WithLogContext & WithI18nContext>;

View file

@ -24,6 +24,8 @@ import { PasswordVerification } from '../experience/classes/verifications/passwo
import { SocialVerification } from '../experience/classes/verifications/social-verification.js';
import type { UserRouter, RouterInitArgs } from '../types.js';
export const verificationApiPrefix = '/verifications';
export default function verificationRoutes<T extends UserRouter>(
...[router, tenantContext]: RouterInitArgs<T>
) {
@ -34,7 +36,7 @@ export default function verificationRoutes<T extends UserRouter>(
}
router.post(
'/verifications/password',
`${verificationApiPrefix}/password`,
koaGuard({
body: z.object({ password: z.string().min(1) }),
response: z.object({ verificationRecordId: z.string(), expiresAt: z.string() }),
@ -76,7 +78,7 @@ export default function verificationRoutes<T extends UserRouter>(
);
router.post(
'/verifications/verification-code',
`${verificationApiPrefix}/verification-code`,
koaGuard({
body: z.object({
identifier: verificationCodeIdentifierGuard,
@ -119,7 +121,7 @@ export default function verificationRoutes<T extends UserRouter>(
);
router.post(
'/verifications/verification-code/verify',
`${verificationApiPrefix}/verification-code/verify`,
koaGuard({
body: z.object({
identifier: verificationCodeIdentifierGuard,
@ -164,7 +166,7 @@ export default function verificationRoutes<T extends UserRouter>(
);
router.post(
'/verifications/social',
`${verificationApiPrefix}/social`,
koaGuard({
body: socialAuthorizationUrlPayloadGuard.extend({
connectorId: z.string(),
@ -202,7 +204,7 @@ export default function verificationRoutes<T extends UserRouter>(
);
router.post(
'/verifications/social/verify',
`${verificationApiPrefix}/social/verify`,
koaGuard({
body: socialVerificationCallbackPayloadGuard
.pick({

View file

@ -31,7 +31,7 @@ export default function openapiRoutes<T extends AnonymousRouter, R extends Unkno
// Find supplemental documents
const supplementDocuments = await getSupplementDocuments('routes', {
excludeDirectories: ['experience', 'interaction', 'profile', 'verification'],
excludeDirectories: ['experience', 'interaction', 'account', 'verification'],
});
const baseDocument = buildManagementApiBaseDocument(pathMap, tags, ctx.request.origin);
@ -71,7 +71,7 @@ export default function openapiRoutes<T extends AnonymousRouter, R extends Unkno
// Find supplemental documents
const supplementDocuments = await getSupplementDocuments('routes', {
includeDirectories: ['profile', 'verification'],
includeDirectories: ['account', 'verification'],
});
const baseDocument = buildUserApiBaseDocument(pathMap, tags, ctx.request.origin);

View file

@ -8,7 +8,7 @@ export const updatePassword = async (
verificationRecordId: string,
password: string
) =>
api.post('api/profile/password', {
api.post('api/account/password', {
json: { password },
headers: { [verificationRecordIdHeader]: verificationRecordId },
});
@ -19,13 +19,13 @@ export const updatePrimaryEmail = async (
verificationRecordId: string,
newIdentifierVerificationRecordId: string
) =>
api.post('api/profile/primary-email', {
api.post('api/account/primary-email', {
json: { email, newIdentifierVerificationRecordId },
headers: { [verificationRecordIdHeader]: verificationRecordId },
});
export const deletePrimaryEmail = async (api: KyInstance, verificationRecordId: string) =>
api.delete('api/profile/primary-email', {
api.delete('api/account/primary-email', {
headers: { [verificationRecordIdHeader]: verificationRecordId },
});
@ -35,13 +35,13 @@ export const updatePrimaryPhone = async (
verificationRecordId: string,
newIdentifierVerificationRecordId: string
) =>
api.post('api/profile/primary-phone', {
api.post('api/account/primary-phone', {
json: { phone, newIdentifierVerificationRecordId },
headers: { [verificationRecordIdHeader]: verificationRecordId },
});
export const deletePrimaryPhone = async (api: KyInstance, verificationRecordId: string) =>
api.delete('api/profile/primary-phone', {
api.delete('api/account/primary-phone', {
headers: { [verificationRecordIdHeader]: verificationRecordId },
});
@ -50,7 +50,7 @@ export const updateIdentities = async (
verificationRecordId: string,
newIdentifierVerificationRecordId: string
) =>
api.post('api/profile/identities', {
api.post('api/account/identities', {
json: { newIdentifierVerificationRecordId },
headers: { [verificationRecordIdHeader]: verificationRecordId },
});
@ -60,15 +60,15 @@ export const deleteIdentity = async (
target: string,
verificationRecordId: string
) =>
api.delete(`api/profile/identities/${target}`, {
api.delete(`api/account/identities/${target}`, {
headers: { [verificationRecordIdHeader]: verificationRecordId },
});
export const updateUser = async (api: KyInstance, body: Record<string, unknown>) =>
api.patch('api/profile', { json: body }).json<Partial<UserProfileResponse>>();
api.patch('api/account', { json: body }).json<Partial<UserProfileResponse>>();
export const updateOtherProfile = async (api: KyInstance, body: Record<string, unknown>) =>
api.patch('api/profile/profile', { json: body }).json<Partial<UserProfileResponse['profile']>>();
api.patch('api/account/profile', { json: body }).json<Partial<UserProfileResponse['profile']>>();
export const getUserInfo = async (api: KyInstance) =>
api.get('api/profile').json<Partial<UserProfileResponse>>();
api.get('api/account').json<Partial<UserProfileResponse>>();

View file

@ -30,7 +30,7 @@ const expectedError = {
status: 400,
};
describe('profile, account center fields disabled', () => {
describe('account center fields disabled', () => {
beforeAll(async () => {
await enableAllPasswordSignInMethods();
await updateAccountCenter({
@ -42,7 +42,7 @@ describe('profile, account center fields disabled', () => {
});
});
it('should return only name in GET /profile', async () => {
it('should return only name in GET /account', async () => {
const { user, username, password } = await createDefaultTenantUserWithPassword();
const api = await signInAndGetUserApi(username, password, {
scopes: [UserScope.Email],

View file

@ -1,3 +1,4 @@
/* eslint-disable max-lines */
import { UserScope } from '@logto/core-kit';
import { SignInIdentifier } from '@logto/schemas';
@ -26,7 +27,7 @@ import { devFeatureTest, generateEmail, generatePhone } from '#src/utils.js';
const { describe, it } = devFeatureTest;
describe('profile (email and phone)', () => {
describe('account (email and phone)', () => {
beforeAll(async () => {
await enableAllPasswordSignInMethods();
await setEmailConnector(authedAdminApi);
@ -34,7 +35,7 @@ describe('profile (email and phone)', () => {
await enableAllAccountCenterFields(authedAdminApi);
});
describe('POST /profile/primary-email', () => {
describe('POST /account/primary-email', () => {
it('should fail if scope is missing', async () => {
const { user, username, password } = await createDefaultTenantUserWithPassword();
const api = await signInAndGetUserApi(username, password);
@ -141,7 +142,7 @@ describe('profile (email and phone)', () => {
});
});
describe('DELETE /profile/primary-email', () => {
describe('DELETE /account/primary-email', () => {
it('should fail if scope is missing', async () => {
const { user, username, password } = await createDefaultTenantUserWithPassword();
const api = await signInAndGetUserApi(username, password);
@ -237,20 +238,112 @@ describe('profile (email and phone)', () => {
});
});
describe('POST /profile/primary-phone', () => {
describe('POST /account/primary-phone', () => {
it('should fail if scope is missing', async () => {
const { user, username, password } = await createDefaultTenantUserWithPassword();
const api = await signInAndGetUserApi(username, password);
const newPhone = generatePhone();
const verificationRecordId = await createVerificationRecordByPassword(api, password);
await expectRejects(
updatePrimaryPhone(api, newPhone, verificationRecordId, 'new-verification-record-id'),
{
code: 'auth.unauthorized',
status: 400,
}
);
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('DELETE /account/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);
});
@ -260,50 +353,171 @@ describe('profile (email and phone)', () => {
const api = await signInAndGetUserApi(username, password, {
scopes: [UserScope.Profile, UserScope.Phone],
});
const newPhone = generatePhone();
await expectRejects(
updatePrimaryPhone(
api,
newPhone,
'invalid-verification-record-id',
'new-verification-record-id'
),
{
code: 'verification_record.permission_denied',
status: 401,
}
);
await expectRejects(deletePrimaryPhone(api, 'invalid-verification-record-id'), {
code: 'verification_record.permission_denied',
status: 401,
});
await deleteDefaultTenantUser(user.id);
});
it('should fail if new identifier verification record is invalid', async () => {
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 newPhone = generatePhone();
const verificationRecordId = await createVerificationRecordByPassword(api, password);
await enableAllPasswordSignInMethods({
identifiers: [SignInIdentifier.Phone],
password: true,
verify: true,
});
await expectRejects(
updatePrimaryPhone(api, newPhone, verificationRecordId, 'new-verification-record-id'),
{
code: 'verification_record.not_found',
status: 400,
}
);
await expectRejects(deletePrimaryPhone(api, verificationRecordId), {
code: 'user.phone_required',
status: 400,
});
await enableAllPasswordSignInMethods();
await deleteDefaultTenantUser(user.id);
});
it('should be able to update primary phone by verifying password', async () => {
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 newPhone = generatePhone();
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);
});
describe('POST /account/primary-phone', () => {
it('should fail if scope is missing', async () => {
const { user, username, password } = await createDefaultTenantUserWithPassword();
const api = await signInAndGetUserApi(username, password);
const newPhone = generatePhone();
const verificationRecordId = await createVerificationRecordByPassword(api, password);
await expectRejects(
updatePrimaryPhone(api, newPhone, verificationRecordId, 'new-verification-record-id'),
{
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],
});
const newPhone = generatePhone();
await expectRejects(
updatePrimaryPhone(
api,
newPhone,
'invalid-verification-record-id',
'new-verification-record-id'
),
{
code: 'verification_record.permission_denied',
status: 401,
}
);
await deleteDefaultTenantUser(user.id);
});
it('should fail if new identifier verification record is invalid', async () => {
const { user, username, password } = await createDefaultTenantUserWithPassword();
const api = await signInAndGetUserApi(username, password, {
scopes: [UserScope.Profile, UserScope.Phone],
});
const newPhone = generatePhone();
const verificationRecordId = await createVerificationRecordByPassword(api, password);
await expectRejects(
updatePrimaryPhone(api, newPhone, verificationRecordId, 'new-verification-record-id'),
{
code: 'verification_record.not_found',
status: 400,
}
);
await deleteDefaultTenantUser(user.id);
});
it('should be able to update primary phone by verifying password', async () => {
const { user, username, password } = await createDefaultTenantUserWithPassword();
const api = await signInAndGetUserApi(username, password, {
scopes: [UserScope.Profile, UserScope.Phone],
});
const newPhone = generatePhone();
const verificationRecordId = await createVerificationRecordByPassword(api, password);
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 deleteDefaultTenantUser(user.id);
});
it('should be able to update primary phone by verifying existing phone', async () => {
const primaryPhone = generatePhone();
const { user, username, password } = await createDefaultTenantUserWithPassword({
primaryPhone,
});
const api = await signInAndGetUserApi(username, password, {
scopes: [UserScope.Profile, UserScope.Phone],
});
const newPhone = generatePhone();
const verificationRecordId = await createAndVerifyVerificationCode(api, {
type: SignInIdentifier.Phone,
value: primaryPhone,
});
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 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,
@ -314,37 +528,16 @@ describe('profile (email and phone)', () => {
const userInfo = await getUserInfo(api);
expect(userInfo).toHaveProperty('primaryPhone', newPhone);
await deleteDefaultTenantUser(user.id);
});
await deletePrimaryPhone(api, verificationRecordId);
it('should be able to update primary phone by verifying existing phone', async () => {
const primaryPhone = generatePhone();
const { user, username, password } = await createDefaultTenantUserWithPassword({
primaryPhone,
});
const api = await signInAndGetUserApi(username, password, {
scopes: [UserScope.Profile, UserScope.Phone],
});
const newPhone = generatePhone();
const verificationRecordId = await createAndVerifyVerificationCode(api, {
type: SignInIdentifier.Phone,
value: primaryPhone,
});
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);
const userInfoAfterDelete = await getUserInfo(api);
expect(userInfoAfterDelete).toHaveProperty('primaryPhone', null);
await deleteDefaultTenantUser(user.id);
});
});
describe('DELETE /profile/primary-phone', () => {
describe('DELETE /account/primary-phone', () => {
it('should fail if scope is missing', async () => {
const { user, username, password } = await createDefaultTenantUserWithPassword();
const api = await signInAndGetUserApi(username, password);
@ -440,3 +633,4 @@ describe('profile (email and phone)', () => {
});
});
});
/* eslint-enable max-lines */

View file

@ -21,10 +21,10 @@ import { assertHookLogResult } from '../hook/utils.js';
const { describe, it } = devFeatureTest;
describe('profile', () => {
describe('account', () => {
const webHookMockServer = new WebhookMockServer(9999);
const webHookApi = new WebHookApiTest();
const hookName = 'profileApiHookEventListener';
const hookName = 'accountApiHookEventListener';
beforeAll(async () => {
await webHookMockServer.listen();
@ -48,11 +48,11 @@ describe('profile', () => {
await webHookApi.cleanUp();
});
describe('GET /profile', () => {
describe('GET /account', () => {
it('should allow all origins', async () => {
const { user, username, password } = await createDefaultTenantUserWithPassword();
const api = await signInAndGetUserApi(username, password);
const response = await api.get('api/profile');
const response = await api.get('api/account');
expect(response.status).toBe(200);
expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*');
@ -121,7 +121,7 @@ describe('profile', () => {
});
});
describe('PATCH /profile', () => {
describe('PATCH /account', () => {
it('should be able to update name', async () => {
const { user, username, password } = await createDefaultTenantUserWithPassword();
const api = await signInAndGetUserApi(username, password);
@ -211,7 +211,7 @@ describe('profile', () => {
});
});
describe('PATCH /profile/profile', () => {
describe('PATCH /account/profile', () => {
it('should be able to update other profile', async () => {
const { user, username, password } = await createDefaultTenantUserWithPassword();
const api = await signInAndGetUserApi(username, password);
@ -258,7 +258,7 @@ describe('profile', () => {
});
});
describe('POST /profile/password', () => {
describe('POST /account/password', () => {
it('should fail if verification record is invalid', async () => {
const { user, username, password } = await createDefaultTenantUserWithPassword();
const api = await signInAndGetUserApi(username, password);

View file

@ -29,7 +29,7 @@ import { devFeatureTest } from '#src/utils.js';
const { describe, it } = devFeatureTest;
describe('profile (social)', () => {
describe('account (social)', () => {
const state = 'fake_state';
const redirectUri = 'http://localhost:3000/redirect';
const authorizationCode = 'fake_code';
@ -50,7 +50,7 @@ describe('profile (social)', () => {
await clearConnectorsByTypes([ConnectorType.Social, ConnectorType.Email]);
});
describe('POST /profile/identities', () => {
describe('POST /account/identities', () => {
it('should fail if scope is missing', async () => {
const { user, username, password } = await createDefaultTenantUserWithPassword();
const api = await signInAndGetUserApi(username, password);
@ -169,7 +169,7 @@ describe('profile (social)', () => {
});
});
describe('DELETE /profile/identities/:target', () => {
describe('DELETE /account/identities/:target', () => {
it('should fail if scope is missing', async () => {
const { user, username, password } = await createDefaultTenantUserWithPassword();
const api = await signInAndGetUserApi(username, password);

View file

@ -1,26 +0,0 @@
import { z } from 'zod';
export enum AccountCenterControlValue {
Off = 'Off',
ReadOnly = 'ReadOnly',
Edit = 'Edit',
}
// Control list of each field in the account center (profile API)
// all fields are optional, if not set, the default value is `Off`
// this can make the alteration of the field control easier
export const accountCenterFieldControlGuard = z
.object({
name: z.nativeEnum(AccountCenterControlValue),
avatar: z.nativeEnum(AccountCenterControlValue),
profile: z.nativeEnum(AccountCenterControlValue),
email: z.nativeEnum(AccountCenterControlValue),
phone: z.nativeEnum(AccountCenterControlValue),
password: z.nativeEnum(AccountCenterControlValue),
username: z.nativeEnum(AccountCenterControlValue),
social: z.nativeEnum(AccountCenterControlValue),
customData: z.nativeEnum(AccountCenterControlValue),
})
.partial();
export type AccountCenterFieldControl = z.infer<typeof accountCenterFieldControlGuard>;