mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
feat(core): remove social identity (#6709)
* feat(core): add social identity * feat(core): remove social identity
This commit is contained in:
parent
73627a76a3
commit
fa791d3a63
4 changed files with 159 additions and 2 deletions
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>>();
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue