mirror of
https://github.com/logto-io/logto.git
synced 2025-01-27 21:39:16 -05:00
feat: continue social sign in (#223)
* feat: social sign in * feat: social register * feat: continue social sign in
This commit is contained in:
parent
e8cbe00d3a
commit
34e540d3ed
7 changed files with 70 additions and 20 deletions
|
@ -1,5 +1,6 @@
|
|||
import { Languages } from '@logto/phrases';
|
||||
import { ConnectorConfig, Connector, PasscodeType } from '@logto/schemas';
|
||||
import { z } from 'zod';
|
||||
|
||||
export enum ConnectorType {
|
||||
SMS = 'SMS',
|
||||
|
@ -80,10 +81,12 @@ export type GetAccessToken = (code: string) => Promise<string>;
|
|||
|
||||
export type GetUserInfo = (accessToken: string) => Promise<SocialUserInfo>;
|
||||
|
||||
export interface SocialUserInfo {
|
||||
id: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
name?: string;
|
||||
avatar?: string;
|
||||
}
|
||||
export const socialUserInfoGuard = z.object({
|
||||
id: z.string(),
|
||||
email: z.string().optional(),
|
||||
phone: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
avatar: z.string().optional(),
|
||||
});
|
||||
|
||||
export type SocialUserInfo = z.infer<typeof socialUserInfoGuard>;
|
||||
|
|
|
@ -15,15 +15,29 @@ import assertThat from '@/utils/assert-that';
|
|||
import { emailRegEx, phoneRegEx } from '@/utils/regex';
|
||||
|
||||
import { createPasscode, sendPasscode, verifyPasscode } from './passcode';
|
||||
import { getUserInfoByConnectorCode } from './social';
|
||||
import { getUserInfoByConnectorCode, SocialUserInfoSession } from './social';
|
||||
import { encryptUserPassword, generateUserId } from './user';
|
||||
|
||||
const assignRegistrationResult = async (ctx: Context, provider: Provider, userId: string) => {
|
||||
const redirectTo = await provider.interactionResult(
|
||||
ctx.req,
|
||||
ctx.res,
|
||||
{ login: { accountId: userId } },
|
||||
{ mergeWithLastSubmission: false }
|
||||
);
|
||||
ctx.body = { redirectTo };
|
||||
};
|
||||
|
||||
const saveUserInfoToSession = async (
|
||||
ctx: Context,
|
||||
provider: Provider,
|
||||
socialUserInfo: SocialUserInfoSession
|
||||
) => {
|
||||
const redirectTo = await provider.interactionResult(
|
||||
ctx.req,
|
||||
ctx.res,
|
||||
{
|
||||
login: { accountId: userId },
|
||||
socialUserInfo,
|
||||
},
|
||||
{ mergeWithLastSubmission: false }
|
||||
);
|
||||
|
@ -159,13 +173,13 @@ export const registerWithSocial = async (
|
|||
) => {
|
||||
const userInfo = await getUserInfoByConnectorCode(connectorId, code);
|
||||
|
||||
assertThat(
|
||||
!(await hasUserWithIdentity(connectorId, userInfo.id)),
|
||||
new RequestError({
|
||||
if (await hasUserWithIdentity(connectorId, userInfo.id)) {
|
||||
await saveUserInfoToSession(ctx, provider, { connectorId, userInfo });
|
||||
throw new RequestError({
|
||||
code: 'user.identity_exists',
|
||||
status: 422,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const id = await generateUserId();
|
||||
await insertUser({
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { PasscodeType, UserLogType } from '@logto/schemas';
|
||||
import { Context } from 'koa';
|
||||
import { Provider } from 'oidc-provider';
|
||||
import { InteractionResults, Provider } from 'oidc-provider';
|
||||
|
||||
import { getSocialConnectorInstanceById } from '@/connectors';
|
||||
import RequestError from '@/errors/RequestError';
|
||||
|
@ -18,7 +18,7 @@ import assertThat from '@/utils/assert-that';
|
|||
import { emailRegEx, phoneRegEx } from '@/utils/regex';
|
||||
|
||||
import { createPasscode, sendPasscode, verifyPasscode } from './passcode';
|
||||
import { getUserInfoByConnectorCode } from './social';
|
||||
import { getUserInfoByConnectorCode, getUserInfoFromInteractionResult } from './social';
|
||||
import { findUserByUsernameAndPassword } from './user';
|
||||
|
||||
const assignSignInResult = async (ctx: Context, provider: Provider, userId: string) => {
|
||||
|
@ -122,12 +122,15 @@ export const assignRedirectUrlForSocial = async (
|
|||
export const signInWithSocial = async (
|
||||
ctx: WithUserLogContext<Context>,
|
||||
provider: Provider,
|
||||
{ connectorId, code }: { connectorId: string; code: string }
|
||||
{ connectorId, code, result }: { connectorId: string; code: string; result?: InteractionResults }
|
||||
) => {
|
||||
ctx.userLog.connectorId = connectorId;
|
||||
ctx.userLog.type = UserLogType.SignInSocial;
|
||||
|
||||
const userInfo = await getUserInfoByConnectorCode(connectorId, code);
|
||||
const userInfo =
|
||||
code === 'session'
|
||||
? await getUserInfoFromInteractionResult(connectorId, result)
|
||||
: await getUserInfoByConnectorCode(connectorId, code);
|
||||
|
||||
assertThat(
|
||||
await hasUserWithIdentity(connectorId, userInfo.id),
|
||||
|
|
|
@ -1,6 +1,15 @@
|
|||
import { InteractionResults } from 'oidc-provider';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getSocialConnectorInstanceById } from '@/connectors';
|
||||
import { SocialUserInfo } from '@/connectors/types';
|
||||
import { SocialUserInfo, socialUserInfoGuard } from '@/connectors/types';
|
||||
import RequestError from '@/errors/RequestError';
|
||||
import assertThat from '@/utils/assert-that';
|
||||
|
||||
export interface SocialUserInfoSession {
|
||||
connectorId: string;
|
||||
userInfo: SocialUserInfo;
|
||||
}
|
||||
|
||||
const getConnector = async (connectorId: string) => {
|
||||
try {
|
||||
|
@ -27,3 +36,21 @@ export const getUserInfoByConnectorCode = async (
|
|||
|
||||
return connector.getUserInfo(accessToken);
|
||||
};
|
||||
|
||||
export const getUserInfoFromInteractionResult = async (
|
||||
connectorId: string,
|
||||
interactionResult?: InteractionResults
|
||||
): Promise<SocialUserInfo> => {
|
||||
const result = z
|
||||
.object({
|
||||
socialUserInfo: z.object({
|
||||
connectorId: z.string(),
|
||||
userInfo: socialUserInfoGuard,
|
||||
}),
|
||||
})
|
||||
.parse(interactionResult);
|
||||
|
||||
assertThat(result.socialUserInfo.connectorId === connectorId, 'session.insufficient_info');
|
||||
|
||||
return result.socialUserInfo.userInfo;
|
||||
};
|
||||
|
|
|
@ -48,6 +48,7 @@ export default function sessionRoutes<T extends AnonymousRouter>(router: T, prov
|
|||
// https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7
|
||||
jti,
|
||||
prompt: { name },
|
||||
result,
|
||||
} = interaction;
|
||||
|
||||
if (name === 'consent') {
|
||||
|
@ -62,7 +63,7 @@ export default function sessionRoutes<T extends AnonymousRouter>(router: T, prov
|
|||
if (connectorId && state && !code) {
|
||||
await assignRedirectUrlForSocial(ctx, connectorId, state);
|
||||
} else if (connectorId && code) {
|
||||
await signInWithSocial(ctx, provider, { connectorId, code });
|
||||
await signInWithSocial(ctx, provider, { connectorId, code, result });
|
||||
} else if (email && !code) {
|
||||
await sendSignInWithEmailPasscode(ctx, jti, email);
|
||||
} else if (email && code) {
|
||||
|
|
|
@ -64,6 +64,7 @@ const errors = {
|
|||
invalid_sign_in_method: 'Current sign-in method is not available.',
|
||||
invalid_connector_id: 'Unable to find available connector with id {{connectorId}}.',
|
||||
insufficient_info: 'Insufficent sign-in info.',
|
||||
connector_id_mismatch: 'The connectorId is mismatched with session record.',
|
||||
},
|
||||
connector: {
|
||||
not_found: 'Cannot find any available connector for type: {{type}}.',
|
||||
|
|
|
@ -65,6 +65,7 @@ const errors = {
|
|||
invalid_sign_in_method: '当前登录方式不可用。',
|
||||
insufficient_info: '登录信息缺失,请检查您的输入。',
|
||||
invalid_connector_id: '无法找到 ID 为 {{connectorId}} 的可用连接器。',
|
||||
connector_id_mismatch: '传入的 connectorId 与 session 中保存的记录不一致。',
|
||||
},
|
||||
connector: {
|
||||
not_found: '找不到可用的 {{type}} 类型的连接器。',
|
||||
|
|
Loading…
Add table
Reference in a new issue