Skip to content

Commit

Permalink
feat(core): remove social identity
Browse files Browse the repository at this point in the history
  • Loading branch information
wangsijie committed Oct 28, 2024
1 parent 1d485a0 commit e6b3f6d
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 2 deletions.
25 changes: 25 additions & 0 deletions packages/core/src/routes/profile/index.openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,31 @@
}
}
}
},
"/api/profile/identities/{target}": {
"delete": {
"operationId": "DeleteIdentity",
"summary": "Delete a user identity",
"description": "Delete an identity (social identity) from the user, a verification record is required for checking sensitive permissions.",
"parameters": [
{
"name": "verificationRecordId",
"in": "query",
"description": "The verification record ID for checking sensitive permissions."
}
],
"responses": {
"204": {
"description": "The identity was deleted successfully."
},
"400": {
"description": "The verification record is invalid."
},
"404": {
"description": "The identity does not exist."
}
}
}
}
}
}
46 changes: 45 additions & 1 deletion packages/core/src/routes/profile/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { z } from 'zod';
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,
Expand All @@ -20,7 +21,7 @@ export default function profileRoutes<T extends UserRouter>(
...[router, { queries, libraries }]: RouterInitArgs<T>
) {
const {
users: { updateUserById, findUserById },
users: { updateUserById, findUserById, deleteUserIdentity },
signInExperiences: { findDefaultSignInExperience },
} = queries;

Expand Down Expand Up @@ -295,4 +296,47 @@ export default function profileRoutes<T extends UserRouter>(
return next();
}
);

router.delete(
'/profile/identities/:target',
koaGuard({
params: z.object({ target: z.string() }),
query: z.object({
// TODO: Move all sensitive permission checks to the header
verificationRecordId: z.string(),
}),
status: [204, 400, 401, 404],
}),
async (ctx, next) => {
const { id: userId, scopes } = ctx.auth;
const { verificationRecordId } = ctx.guard.query;
const { target } = ctx.guard.params;
assertThat(scopes.has(UserScope.Identities), 'auth.unauthorized');

await verifyUserSensitivePermission({
userId,
id: verificationRecordId,
queries,
libraries,
});

const user = await findUserById(userId);

assertThat(
user.identities[target],
new RequestError({
code: 'user.identity_not_exist',
status: 404,
})
);

const updatedUser = await deleteUserIdentity(userId, target);

ctx.appendDataHookContext('User.Data.Updated', { user: updatedUser });

ctx.status = 204;

return next();
}

Check warning on line 340 in packages/core/src/routes/profile/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/core/src/routes/profile/index.ts#L311-L340

Added lines #L311 - L340 were not covered by tests
);
}
9 changes: 9 additions & 0 deletions packages/integration-tests/src/api/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ export const updateIdentities = async (
json: { verificationRecordId, newIdentifierVerificationRecordId },
});

export const deleteIdentity = async (
api: KyInstance,
target: string,
verificationRecordId: string
) =>
api.delete(`api/profile/identities/${target}`, {
searchParams: { verificationRecordId },
});

export const updateUser = async (api: KyInstance, body: Record<string, unknown>) =>
api.patch('api/profile', { json: body }).json<Partial<UserProfileResponse>>();

Expand Down
81 changes: 80 additions & 1 deletion packages/integration-tests/src/tests/api/profile/social.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
mockSocialConnectorId,
mockSocialConnectorTarget,
} from '#src/__mocks__/connectors-mock.js';
import { getUserInfo, updateIdentities } from '#src/api/profile.js';
import { deleteIdentity, getUserInfo, updateIdentities } from '#src/api/profile.js';
import {
createSocialVerificationRecord,
createVerificationRecordByPassword,
Expand Down Expand Up @@ -166,4 +166,83 @@ describe('profile (social)', () => {
});
});
});

describe('DELETE /profile/identities/:target', () => {
it('should fail if scope is missing', async () => {
const { user, username, password } = await createDefaultTenantUserWithPassword();
const api = await signInAndGetUserApi(username, password);

await expectRejects(
deleteIdentity(api, mockSocialConnectorTarget, 'invalid-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.Identities],
});

await expectRejects(
deleteIdentity(api, mockSocialConnectorTarget, 'invalid-verification-record-id'),
{
code: 'verification_record.permission_denied',
status: 401,
}
);

await deleteDefaultTenantUser(user.id);
});

it('should fail if identity does not exist', async () => {
const { user, username, password } = await createDefaultTenantUserWithPassword();
const api = await signInAndGetUserApi(username, password, {
scopes: [UserScope.Profile, UserScope.Identities],
});
const verificationRecordId = await createVerificationRecordByPassword(api, password);

await expectRejects(deleteIdentity(api, mockSocialConnectorTarget, verificationRecordId), {
code: 'user.identity_not_exist',
status: 404,
});

await deleteDefaultTenantUser(user.id);
});

it('should be able to delete social identity', async () => {
const { user, username, password } = await createDefaultTenantUserWithPassword();
const api = await signInAndGetUserApi(username, password, {
scopes: [UserScope.Profile, UserScope.Identities],
});
const verificationRecordId = await createVerificationRecordByPassword(api, password);

// Link social identity to the user
const { verificationRecordId: newVerificationRecordId } =
await createSocialVerificationRecord(
api,
connectorIdMap.get(mockSocialConnectorId)!,
state,
redirectUri
);
await verifySocialAuthorization(api, newVerificationRecordId, {
code: authorizationCode,
});
await updateIdentities(api, verificationRecordId, newVerificationRecordId);
const userInfo = await getUserInfo(api);
expect(userInfo.identities).toHaveProperty(mockSocialConnectorTarget);

await deleteIdentity(api, mockSocialConnectorTarget, verificationRecordId);

const updatedUserInfo = await getUserInfo(api);
expect(updatedUserInfo.identities).not.toHaveProperty(mockSocialConnectorTarget);

await deleteDefaultTenantUser(user.id);
});
});
});

0 comments on commit e6b3f6d

Please sign in to comment.