mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat: social register (#222)
This commit is contained in:
parent
8249493c40
commit
78cc86ec77
6 changed files with 79 additions and 23 deletions
|
@ -4,11 +4,18 @@ import { Provider } from 'oidc-provider';
|
|||
|
||||
import RequestError from '@/errors/RequestError';
|
||||
import { WithUserLogContext } from '@/middleware/koa-user-log';
|
||||
import { hasUser, hasUserWithEmail, hasUserWithPhone, insertUser } from '@/queries/user';
|
||||
import {
|
||||
hasUser,
|
||||
hasUserWithEmail,
|
||||
hasUserWithPhone,
|
||||
hasUserWithIdentity,
|
||||
insertUser,
|
||||
} from '@/queries/user';
|
||||
import assertThat from '@/utils/assert-that';
|
||||
import { emailRegEx, phoneRegEx } from '@/utils/regex';
|
||||
|
||||
import { createPasscode, sendPasscode, verifyPasscode } from './passcode';
|
||||
import { getUserInfoByConnectorCode } from './social';
|
||||
import { encryptUserPassword, generateUserId } from './user';
|
||||
|
||||
const assignRegistrationResult = async (ctx: Context, provider: Provider, userId: string) => {
|
||||
|
@ -144,3 +151,32 @@ export const registerWithPhoneAndPasscode = async (
|
|||
ctx.userLog.phone = phone;
|
||||
ctx.userLog.type = UserLogType.RegisterPhone;
|
||||
};
|
||||
|
||||
export const registerWithSocial = async (
|
||||
ctx: WithUserLogContext<Context>,
|
||||
provider: Provider,
|
||||
{ connectorId, code }: { connectorId: string; code: string }
|
||||
) => {
|
||||
const userInfo = await getUserInfoByConnectorCode(connectorId, code);
|
||||
|
||||
assertThat(
|
||||
!(await hasUserWithIdentity(connectorId, userInfo.id)),
|
||||
new RequestError({
|
||||
code: 'user.identity_exists',
|
||||
status: 422,
|
||||
})
|
||||
);
|
||||
|
||||
const id = await generateUserId();
|
||||
await insertUser({
|
||||
id,
|
||||
identities: {
|
||||
[connectorId]: {
|
||||
userId: userInfo.id,
|
||||
details: userInfo,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await assignRegistrationResult(ctx, provider, id);
|
||||
};
|
||||
|
|
|
@ -17,6 +17,7 @@ import assertThat from '@/utils/assert-that';
|
|||
import { emailRegEx, phoneRegEx } from '@/utils/regex';
|
||||
|
||||
import { createPasscode, sendPasscode, verifyPasscode } from './passcode';
|
||||
import { getUserInfoByConnectorCode } from './social';
|
||||
import { findUserByUsernameAndPassword } from './user';
|
||||
|
||||
const assignSignInResult = async (ctx: Context, provider: Provider, userId: string) => {
|
||||
|
@ -117,22 +118,6 @@ export const assignRedirectUrlForSocial = async (
|
|||
ctx.body = { redirectTo };
|
||||
};
|
||||
|
||||
const getConnector = async (connectorId: string) => {
|
||||
try {
|
||||
return await getSocialConnectorInstanceById(connectorId);
|
||||
} catch (error: unknown) {
|
||||
// Throw a new error with status 422 when connector not found.
|
||||
if (error instanceof RequestError && error.code === 'entity.not_found') {
|
||||
throw new RequestError({
|
||||
code: 'session.invalid_connector_id',
|
||||
status: 422,
|
||||
data: { connectorId },
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const signInWithSocial = async (
|
||||
ctx: WithUserLogContext<Context>,
|
||||
provider: Provider,
|
||||
|
@ -141,10 +126,7 @@ export const signInWithSocial = async (
|
|||
ctx.userLog.connectorId = connectorId;
|
||||
ctx.userLog.type = UserLogType.SignInSocial;
|
||||
|
||||
const connector = await getConnector(connectorId);
|
||||
const accessToken = await connector.getAccessToken(code);
|
||||
|
||||
const userInfo = await connector.getUserInfo(accessToken);
|
||||
const userInfo = await getUserInfoByConnectorCode(connectorId, code);
|
||||
|
||||
assertThat(
|
||||
await hasUserWithIdentity(connectorId, userInfo.id),
|
||||
|
|
29
packages/core/src/lib/social.ts
Normal file
29
packages/core/src/lib/social.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { getSocialConnectorInstanceById } from '@/connectors';
|
||||
import { SocialUserInfo } from '@/connectors/types';
|
||||
import RequestError from '@/errors/RequestError';
|
||||
|
||||
const getConnector = async (connectorId: string) => {
|
||||
try {
|
||||
return await getSocialConnectorInstanceById(connectorId);
|
||||
} catch (error: unknown) {
|
||||
// Throw a new error with status 422 when connector not found.
|
||||
if (error instanceof RequestError && error.code === 'entity.not_found') {
|
||||
throw new RequestError({
|
||||
code: 'session.invalid_connector_id',
|
||||
status: 422,
|
||||
data: { connectorId },
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getUserInfoByConnectorCode = async (
|
||||
connectorId: string,
|
||||
code: string
|
||||
): Promise<SocialUserInfo> => {
|
||||
const connector = await getConnector(connectorId);
|
||||
const accessToken = await connector.getAccessToken(code);
|
||||
|
||||
return connector.getUserInfo(accessToken);
|
||||
};
|
|
@ -5,6 +5,7 @@ import { object, string } from 'zod';
|
|||
|
||||
import RequestError from '@/errors/RequestError';
|
||||
import {
|
||||
registerWithSocial,
|
||||
registerWithEmailAndPasscode,
|
||||
registerWithPhoneAndPasscode,
|
||||
registerWithUsernameAndPassword,
|
||||
|
@ -133,14 +134,20 @@ export default function sessionRoutes<T extends AnonymousRouter>(router: T, prov
|
|||
email: string().optional(),
|
||||
phone: string().optional(),
|
||||
code: string().optional(),
|
||||
connectorId: string().optional(),
|
||||
state: string().optional(),
|
||||
}),
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const interaction = await provider.interactionDetails(ctx.req, ctx.res);
|
||||
const { jti } = interaction;
|
||||
const { username, password, email, phone, code } = ctx.guard.body;
|
||||
const { username, password, email, phone, code, connectorId, state } = ctx.guard.body;
|
||||
|
||||
if (email && !code) {
|
||||
if (connectorId && state && !code) {
|
||||
await assignRedirectUrlForSocial(ctx, connectorId, state);
|
||||
} else if (connectorId && state && code) {
|
||||
await registerWithSocial(ctx, provider, { connectorId, code });
|
||||
} else if (email && !code) {
|
||||
await sendPasscodeToEmail(ctx, jti, email);
|
||||
} else if (email && code) {
|
||||
await registerWithEmailAndPasscode(ctx, provider, { jti, email, code });
|
||||
|
|
|
@ -52,6 +52,7 @@ const errors = {
|
|||
email_not_exists: 'The email address has not been registered yet.',
|
||||
phone_not_exists: 'The phone number has not been registered yet.',
|
||||
identity_not_exists: 'The social account has not been registered yet.',
|
||||
identity_exists: 'The social account has been registered.',
|
||||
},
|
||||
password: {
|
||||
unsupported_encryption_method: 'The encryption method {{name}} is not supported.',
|
||||
|
|
|
@ -53,6 +53,7 @@ const errors = {
|
|||
email_not_exists: '邮箱地址尚未注册。',
|
||||
phone_not_exists: '手机号码尚未注册。',
|
||||
identity_not_exists: '该社交账号尚未注册。',
|
||||
identity_exists: '该社交账号已被注册。',
|
||||
},
|
||||
password: {
|
||||
unsupported_encryption_method: '不支持的加密方法 {{name}}。',
|
||||
|
|
Loading…
Reference in a new issue