mirror of
https://github.com/logto-io/logto.git
synced 2025-01-27 21:39:16 -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 koaGuard from '#src/middleware/koa-guard.js';
|
||||||
|
|
||||||
import { EnvSet } from '../../env-set/index.js';
|
import { EnvSet } from '../../env-set/index.js';
|
||||||
|
import RequestError from '../../errors/RequestError/index.js';
|
||||||
import { encryptUserPassword } from '../../libraries/user.utils.js';
|
import { encryptUserPassword } from '../../libraries/user.utils.js';
|
||||||
import {
|
import {
|
||||||
buildVerificationRecordByIdAndType,
|
buildVerificationRecordByIdAndType,
|
||||||
|
@ -20,7 +21,7 @@ export default function profileRoutes<T extends UserRouter>(
|
||||||
...[router, { queries, libraries }]: RouterInitArgs<T>
|
...[router, { queries, libraries }]: RouterInitArgs<T>
|
||||||
) {
|
) {
|
||||||
const {
|
const {
|
||||||
users: { updateUserById, findUserById },
|
users: { updateUserById, findUserById, deleteUserIdentity },
|
||||||
signInExperiences: { findDefaultSignInExperience },
|
signInExperiences: { findDefaultSignInExperience },
|
||||||
} = queries;
|
} = queries;
|
||||||
|
|
||||||
|
@ -295,4 +296,47 @@ export default function profileRoutes<T extends UserRouter>(
|
||||||
return next();
|
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 },
|
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>) =>
|
export const updateUser = async (api: KyInstance, body: Record<string, unknown>) =>
|
||||||
api.patch('api/profile', { json: body }).json<Partial<UserProfileResponse>>();
|
api.patch('api/profile', { json: body }).json<Partial<UserProfileResponse>>();
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {
|
||||||
mockSocialConnectorId,
|
mockSocialConnectorId,
|
||||||
mockSocialConnectorTarget,
|
mockSocialConnectorTarget,
|
||||||
} from '#src/__mocks__/connectors-mock.js';
|
} from '#src/__mocks__/connectors-mock.js';
|
||||||
import { getUserInfo, updateIdentities } from '#src/api/profile.js';
|
import { deleteIdentity, getUserInfo, updateIdentities } from '#src/api/profile.js';
|
||||||
import {
|
import {
|
||||||
createSocialVerificationRecord,
|
createSocialVerificationRecord,
|
||||||
createVerificationRecordByPassword,
|
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…
Add table
Reference in a new issue