feat: oauth相关aspect实现 && 给其他aspect定义加上了注释

This commit is contained in:
Pan Qiancheng 2025-10-23 15:31:19 +08:00
parent a3a4837974
commit 35c6b01f65
3 changed files with 997 additions and 0 deletions

View File

@ -8,6 +8,12 @@ import { BackendRuntimeContext } from '../context/BackendRuntimeContext';
import { WechatPublicEventData, WechatMpEventData } from 'oak-external-sdk';
export type AspectDict<ED extends EntityDict> = {
/**
* 使 token Web
* @param mpToken token
* @param env Web
* @returns Web token
*/
loginWebByMpToken: (
params: {
mpToken: string;
@ -15,18 +21,42 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
*
* @param from ID
* @param to ID
*/
mergeUser: (
params: { from: string; to: string },
context: BackendRuntimeContext<ED>
) => Promise<void>;
/**
*
*/
refreshWechatPublicUserInfo: (
params: {},
context: BackendRuntimeContext<ED>
) => Promise<void>;
/**
*
* @param code code
* @param env
* @returns
*/
getWechatMpUserPhoneNumber: (
params: { code: string; env: WechatMpEnv },
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
*
* @param mobile
* @param captcha
* @param env
*/
bindByMobile: (
params: {
mobile: string;
@ -35,6 +65,13 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<void>;
/**
*
* @param email
* @param captcha
* @param env
*/
bindByEmail: (
params: {
email: string;
@ -43,6 +80,15 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<void>;
/**
*
* @param mobile
* @param captcha
* @param disableRegister true
* @param env
* @returns token
*/
loginByMobile: (
params: {
mobile: string;
@ -52,6 +98,12 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
*
* @param password SHA1
* @param env
*/
verifyPassword: (
params: {
password: string;
@ -59,6 +111,14 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<void>;
/**
* //
* @param account
* @param password SHA1
* @param env
* @returns token
*/
loginByAccount: (
params: {
account: string;
@ -67,6 +127,15 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
*
* @param email
* @param captcha
* @param disableRegister true
* @param env
* @returns token
*/
loginByEmail: (
params: {
email: string;
@ -76,6 +145,14 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
*
* @param code code
* @param env Web
* @param wechatLoginId ID
* @returns token
*/
loginWechat: (
{
code,
@ -88,10 +165,22 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
* 使 token
* @param tokenValue token
*/
logout: (
params: { tokenValue: string },
context: BackendRuntimeContext<ED>
) => Promise<void>;
/**
*
* @param code code
* @param env
* @returns token
*/
loginWechatMp: (
{
code,
@ -102,6 +191,13 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
* APP
* @param code code
* @param env APP
* @returns token
*/
loginWechatNative: (
{
code,
@ -112,6 +208,15 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
*
* @param nickname
* @param avatarUrl URL
* @param encryptedData
* @param iv
* @param signature
*/
syncUserInfoWechatMp: (
{
nickname,
@ -128,6 +233,13 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<void>;
/**
* shadow
* @param id ID
* @param env
* @returns token
*/
wakeupParasite: (
params: {
id: string;
@ -135,6 +247,14 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
* token
* @param tokenValue token
* @param env
* @param applicationId ID
* @returns token
*/
refreshToken: (
params: {
tokenValue: string;
@ -143,6 +263,14 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
*
* @param mobile
* @param env
* @param type login-changePassword-confirm-
* @returns ID
*/
sendCaptchaByMobile: (
params: {
mobile: string;
@ -151,6 +279,14 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
*
* @param email
* @param env
* @param type login-changePassword-confirm-
* @returns ID
*/
sendCaptchaByEmail: (
params: {
email: string;
@ -159,6 +295,16 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
*
* @param version
* @param type web/wechatMp/wechatPublic/native
* @param domain
* @param data
* @param appId ID
* @returns ID
*/
getApplication: (
params: {
version: string;
@ -169,6 +315,13 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
* JS-SDK JS
* @param url URL
* @param env Web
* @returns signaturenoncestrtimestampappId
*/
signatureJsSDK: (
params: {
url: string;
@ -181,6 +334,13 @@ export type AspectDict<ED extends EntityDict> = {
timestamp: number;
appId: string;
}>;
/**
*
* @param entity platform system
* @param entityId ID
* @param config
*/
updateConfig: (
params: {
entity: 'platform' | 'system';
@ -189,6 +349,13 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<void>;
/**
*
* @param entity platform/system/application
* @param entityId ID
* @param style
*/
updateStyle: (
params: {
entity: 'platform' | 'system' | 'application';
@ -197,6 +364,13 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<void>;
/**
*
* @param entity application
* @param entityId ID
* @param config
*/
updateApplicationConfig: (
params: {
entity: 'application';
@ -205,16 +379,34 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<void>;
/**
*
* @param userId ID
*/
switchTo: (
params: {
userId: string;
},
context: BackendRuntimeContext<ED>
) => Promise<void>;
/**
*
* @param wechatQrCodeId ID
* @returns Base64
*/
getMpUnlimitWxaCode: (
wechatQrCodeId: string,
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
*
* @param type login-bind-
* @param interval
* @returns ID
*/
createWechatLogin: (
params: {
type: EntityDict['wechatLogin']['Schema']['type'];
@ -222,6 +414,13 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
*
* @param wechatUserId ID
* @param captcha
* @param mobile
*/
unbindingWechat: (
params: {
wechatUserId: string;
@ -230,6 +429,13 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<void>;
/**
* ID Web
* @param wechatLoginId ID
* @param env Web
* @returns token
*/
loginByWechat: (
params: {
wechatLoginId: string;
@ -237,15 +443,37 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
* URL
* @param url URL
* @returns
*/
getInfoByUrl: (params: { url: string }) => Promise<{
title: string;
publishDate: number | undefined;
imageList: string[];
}>;
/**
*
* @param userId ID
* @returns mobile-password-
*/
getChangePasswordChannels: (
params: { userId: string },
context: BackendRuntimeContext<ED>
) => Promise<string[]>;
/**
*
* @param userId ID
* @param prevPassword 使
* @param mobile 使
* @param captcha 使
* @param newPassword
* @returns
*/
updateUserPassword: (
params: {
userId: string;
@ -256,6 +484,15 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<{ result: string; times?: number }>;
/**
*
* @param data
* @param type
* @param entity
* @param entityId ID
* @returns ID
*/
createSession: (
params: {
data?: WechatPublicEventData | WechatMpEventData;
@ -265,22 +502,47 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
*
* @param params ID
* @returns mediaId
*/
uploadWechatMedia: (
params: any,
context: BackendRuntimeContext<ED>
) => Promise<{ mediaId: string }>;
/**
* 使
* @param applicationId ID
* @returns
*/
getCurrentMenu: (
params: {
applicationId: string;
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
* @returns
*/
getMenu: (
params: {
applicationId: string;
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
* @param menuConfig
* @param id ID
*/
createMenu: (
params: {
applicationId: string;
@ -289,6 +551,13 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
* @param menuConfig
* @param id ID
*/
createConditionalMenu: (
params: {
applicationId: string;
@ -297,6 +566,12 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
* @param menuId ID
*/
deleteConditionalMenu: (
params: {
applicationId: string;
@ -304,12 +579,26 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
*/
deleteMenu: (
params: {
applicationId: string;
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
* @param offset
* @param count
* @param noContent 0-1-
* @returns
*/
batchGetArticle: (
params: {
applicationId: string;
@ -319,6 +608,13 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
* @param articleId ID
* @returns
*/
getArticle: (
params: {
applicationId: string;
@ -326,6 +622,15 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
* @param type image-voice-video-news-
* @param offset
* @param count
* @returns
*/
batchGetMaterialList: (
params: {
applicationId: string;
@ -335,6 +640,14 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
* @param mediaId ID
* @param isPermanent
* @returns
*/
getMaterial: (
params: {
applicationId: string;
@ -343,6 +656,12 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
* @param mediaId ID
*/
deleteMaterial: (
params: {
applicationId: string;
@ -350,6 +669,13 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
* @param name
* @returns
*/
createTag: (
params: {
applicationId: string;
@ -357,12 +683,25 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
* @returns
*/
getTags: (
params: {
applicationId: string;
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
* @param id ID
* @param name
*/
editTag: (
params: {
applicationId: string;
@ -371,6 +710,13 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
* @param id ID
* @param wechatId ID
*/
deleteTag: (
params: {
applicationId: string;
@ -379,16 +725,33 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
* @returns
*/
syncMessageTemplate: (
params: {
applicationId: string;
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @returns
*/
getMessageType: (
params: {},
content: BackendRuntimeContext<ED>
) => Promise<string[]>;
/**
*
* @param applicationId ID
* @param id ID
*/
syncTag: (
params: {
applicationId: string;
@ -396,12 +759,24 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
*/
oneKeySync: (
params: {
applicationId: string;
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
* @param tagId ID
* @returns
*/
getTagUsers: (
params: {
applicationId: string;
@ -409,6 +784,13 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
* @param openIdList openId
* @param tagId ID
*/
batchtagging: (
params: {
applicationId: string;
@ -417,6 +799,13 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
* @param openIdList openId
* @param tagId ID
*/
batchuntagging: (
params: {
applicationId: string;
@ -425,6 +814,13 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
* @param openId openId
* @returns ID
*/
getUserTags: (
params: {
applicationId: string;
@ -432,6 +828,13 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
* @param nextOpenId openId
* @returns
*/
getUsers: (
params: {
applicationId: string;
@ -439,6 +842,13 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
* @param openId openId
* @param tagIdList ID
*/
tagging: (
params: {
applicationId: string;
@ -447,6 +857,12 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
* @param openId openId
*/
syncToLocale: (
params: {
applicationId: string;
@ -454,6 +870,13 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param applicationId ID
* @param id ID
* @param openId openId
*/
syncToWechat: (
params: {
applicationId: string;
@ -462,6 +885,12 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<any>;
/**
*
* @param systemId ID
* @param origin
*/
syncSmsTemplate: (
params: {
systemId: string;
@ -469,18 +898,97 @@ export type AspectDict<ED extends EntityDict> = {
},
context: BackendRuntimeContext<ED>
) => Promise<void>;
/**
*
* @param applicationId ID
* @returns
*/
getApplicationPassports: (
params: {
applicationId: string;
},
context: BackendRuntimeContext<ED>
) => Promise<EntityDict['applicationPassport']['Schema'][]>;
/**
* ID
* @param passportIds ID
*/
removeApplicationPassportsByPIds: (
params: {
passportIds: string[];
},
content: BackendRuntimeContext<ED>
) => Promise<void>;
/**
* OAuth 2.0
* @param code OAuth
* @param state
* @param env
* @returns token
*/
loginByOauth: (
params: {
code: string;
state: string;
env: WebEnv | WechatMpEnv | NativeEnv;
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
* OAuth /
* @param providerId OAuth ID
* @param userId ID
* @param type bind-login-
* @returns
*/
createOAuthState: (
params: {
providerId: string;
userId?: string;
type: "bind" | "login";
},
context: BackendRuntimeContext<ED>
) => Promise<string>;
/**
* OAuth 2.0
* @param response_type "code"
* @param client_id ID
* @param redirect_uri
* @param scope
* @param state
* @param action grant-deny-
* @returns URL
*/
authorize: (
params: {
response_type: string;
client_id: string;
redirect_uri: string;
scope: string;
state: string;
action: "grant" | "deny";
},
context: BackendRuntimeContext<ED>
) => Promise<{
redirectUri: string;
}>
/**
* OAuth
* @param client_id ID
* @returns null
*/
getOAuthClientInfo: (
params: {
client_id: string;
},
context: BackendRuntimeContext<ED>
) => Promise<EntityDict['oauthApplication']['Schema'] | null>;
};
export default AspectDict;

View File

@ -69,6 +69,7 @@ import {
wechatMpJump,
} from './wechatMpJump';
import { getApplicationPassports, removeApplicationPassportsByPIds } from './applicationPassport';
import { authorize, createOAuthState, getOAuthClientInfo, loginByOauth } from './oauth';
const aspectDict = {
bindByEmail,
@ -136,6 +137,11 @@ const aspectDict = {
removeApplicationPassportsByPIds,
verifyPassword,
loginWebByMpToken,
// oauth
loginByOauth,
getOAuthClientInfo,
createOAuthState,
authorize,
};
export default aspectDict;

483
src/aspects/oauth.ts Normal file
View File

@ -0,0 +1,483 @@
import assert from "assert";
import { BRC } from "../types/RuntimeCxt";
import { EntityDict } from "../oak-app-domain";
import { NativeEnv, OakUserException, WebEnv, WechatMpEnv } from "oak-domain/lib/types";
import { generateNewIdAsync } from "oak-domain/lib/utils/uuid";
import { loadTokenInfo, setUpTokenAndUser } from "./token";
import { randomUUID } from "crypto";
import { processUserInfo } from "../utils/oauth";
export async function loginByOauth<ED extends EntityDict>(params: {
code: string;
state: string;
env: WebEnv | WechatMpEnv | NativeEnv;
}, context: BRC<ED>) {
const { code, state: stateCode, env } = params;
const closeRootMode = context.openRootMode();
const currentUserId = context.getCurrentUserId(true);
const applicationId = context.getApplicationId();
const islogginedIn = !!currentUserId;
assert(applicationId, '无法获取当前应用ID');
assert(code, 'code 参数缺失');
assert(stateCode, 'state 参数缺失');
// 验证 state 并获取 OAuth 配置
const [state] = await context.select("oauthState", {
data: {
provider: {
type: 1,
clientId: 1,
redirectUri: 1,
clientSecret: 1,
tokenEndpoint: 1,
userInfoEndpoint: 1,
ableState: 1,
autoRegister: 1,
},
usedAt: 1,
},
filter: {
state: stateCode,
},
}, {})
assert(state, '无效的 state 参数');
assert(state.provider?.ableState && state.provider?.ableState === 'enabled', '该 OAuth 提供商已被禁用');
// 如果已经使用
if (state.usedAt) {
throw new OakUserException('该授权请求已被使用,请重新发起授权请求');
}
// 更新为使用过
await context.operate("oauthState", {
id: await generateNewIdAsync(),
action: 'update',
data: {
usedAt: Date.now(),
},
filter: {
id: state.id,
}
}, {});
// 使用 code 换取 access_token 并获取用户信息
const { oauthUserInfo, accessToken, refreshToken, accessTokenExp, refreshTokenExp } = await fetchOAuthUserInfo(
code,
state.provider,
);
const [existingOAuthUser] = await context.select("oauthUser", {
data: {
id: 1,
userId: 1,
providerUserId: 1,
user: {
id: 1,
userState: 1,
refId: 1,
ref: {
id: 1,
userState: 1,
}
}
},
filter: {
providerUserId: oauthUserInfo.providerUserId,
providerConfigId: state.providerId!,
}
}, {})
// 已登录的情况
if (islogginedIn) {
// 检查当前用户是否已绑定此提供商
const [currentUserBinding] = await context.select("oauthUser", {
data: {
id: 1,
},
filter: {
userId: currentUserId,
providerConfigId: state.providerId!,
}
}, {});
if (currentUserBinding) {
throw new OakUserException('当前用户已绑定该 OAuth 平台账号');
}
if (existingOAuthUser) {
throw new OakUserException('该 OAuth 账号已被其他用户绑定');
}
console.log("绑定 OAuth 账号到当前用户:", currentUserId, oauthUserInfo.providerUserId);
// 创建绑定关系
await context.operate("oauthUser", {
id: await generateNewIdAsync(),
action: 'create',
data: {
id: await generateNewIdAsync(),
userId: currentUserId,
providerConfigId: state.providerId,
providerUserId: oauthUserInfo.providerUserId,
rawUserInfo: oauthUserInfo.rawData,
accessToken,
accessExpiresAt: accessTokenExp,
refreshToken,
refreshExpiresAt: refreshTokenExp,
applicationId,
stateId: state.id,
}
}, {});
// 返回当前 token
const tokenValue = context.getTokenValue()!;
await loadTokenInfo(tokenValue, context);
closeRootMode();
return tokenValue;
// 未登录OAuth账号已存在直接登录
} else if (existingOAuthUser) {
console.log("使用已绑定的 OAuth 账号登录:", existingOAuthUser.id);
const { user } = existingOAuthUser;
const targetUser = user?.userState === 'merged' ? user.ref : user;
const tokenValue = await setUpTokenAndUser(
env,
context,
'oauthUser',
existingOAuthUser.id, // 使用已存在的 oauthUser ID
undefined,
targetUser // 关联的用户
);
// 更新登录信息
await context.operate("oauthUser", {
id: await generateNewIdAsync(),
action: 'update',
data: {
rawUserInfo: oauthUserInfo.rawData,
accessToken,
accessExpiresAt: accessTokenExp,
refreshToken,
refreshExpiresAt: refreshTokenExp,
applicationId,
stateId: state.id,
},
filter: {
id: existingOAuthUser.id,
}
}, {});
await loadTokenInfo(tokenValue, context);
closeRootMode();
return tokenValue;
}
// 未登录OAuth账号不存在创建新用户
else {
if (!state.provider.autoRegister) {
throw new OakUserException('您还没有账号,请先注册一个账号');
}
console.log("使用未绑定的 OAuth 账号登录:", oauthUserInfo.providerUserId);
const newUserId = await generateNewIdAsync();
const oauthUserCreateData: EntityDict['oauthUser']['CreateOperationData'] = {
id: newUserId,
providerConfigId: state.providerId,
providerUserId: oauthUserInfo.providerUserId,
rawUserInfo: oauthUserInfo.rawData,
accessToken,
accessExpiresAt: accessTokenExp,
refreshToken,
refreshExpiresAt: refreshTokenExp,
applicationId,
stateId: state.id,
loadState: 'unload'
};
// 不传 user 参数,会自动创建新用户
const tokenValue = await setUpTokenAndUser(
env,
context,
'oauthUser',
undefined,
oauthUserCreateData, // 创建新的 oauthUser
undefined // 不传 user自动创建新用户
);
await context.operate("oauthUser", {
id: await generateNewIdAsync(),
action: 'loadUserInfo',
data: {},
filter: {
id: newUserId,
}
}, {});
await loadTokenInfo(tokenValue, context);
closeRootMode();
return tokenValue;
}
}
export async function getOAuthClientInfo<ED extends EntityDict>(params: {
client_id: string;
}, context: BRC<ED>) {
const { client_id } = params;
const closeRootMode = context.openRootMode();
const systemId = context.getSystemId();
const [oauthApp] = await context.select("oauthApplication", {
data: {
id: 1,
name: 1,
redirectUris: 1,
description: 1,
logo: 1,
isConfidential: 1,
},
filter: {
id: client_id,
systemId: systemId,
ableState: "enabled",
}
}, {})
if (!oauthApp) {
throw new OakUserException('未经授权的客户端应用');
}
closeRootMode();
return oauthApp;
}
export async function createOAuthState<ED extends EntityDict>(params: {
providerId: string;
userId?: string;
type: 'login' | 'bind';
}, context: BRC<ED>) {
const { providerId, userId, type } = params;
const closeRootMode = context.openRootMode();
const generateCode = () => {
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
}
const state = generateCode();
await context.operate("oauthState", {
id: await generateNewIdAsync(),
action: 'create',
data: {
id: await generateNewIdAsync(),
providerId,
userId,
type,
state
}
}, {})
closeRootMode();
return state;
}
export async function authorize<ED extends EntityDict>(params: {
response_type: string;
client_id: string;
redirect_uri: string;
scope?: string;
state?: string;
action: 'grant' | 'deny';
}, context: BRC<ED>) {
const { response_type, client_id, redirect_uri, scope, state, action } = params;
if (response_type !== 'code') {
throw new OakUserException('不支持的 response_type 类型');
}
const closeRootMode = context.openRootMode();
const systemId = context.getSystemId();
const [oauthApp] = await context.select("oauthApplication", {
data: {
id: 1,
redirectUris: 1,
isConfidential: 1,
},
filter: {
id: client_id,
systemId: systemId,
}
}, {})
if (!oauthApp) {
throw new OakUserException('未经授权的客户端应用');
}
// 创建授权记录
const recordId = await generateNewIdAsync();
await context.operate("oauthUserAuthorization", {
id: await generateNewIdAsync(),
action: 'create',
data: {
id: recordId,
userId: context.getCurrentUserId()!,
applicationId: oauthApp.id,
usageState: action === 'grant' ? 'granted' : 'denied',
authorizedAt: Date.now(),
}
}, {})
if (action === 'deny') {
const params = new URLSearchParams();
params.set('error', 'access_denied');
params.set('error_description', '用户拒绝了授权请求');
if (state) {
params.set('state', state);
}
closeRootMode();
return {
redirectUri: `${redirect_uri}?${params.toString()}`,
}
}
if (action === 'grant') {
// 检查redirectUri 是否在注册的列表中
if (!oauthApp.redirectUris?.includes(redirect_uri)) {
console.log('不合法的重定向 URI:', redirect_uri, oauthApp.redirectUris);
throw new OakUserException('重定向 URI 不合法');
}
const code = randomUUID();
const codeId = await generateNewIdAsync();
// 存储授权码
await context.operate("oauthAuthorizationCode", {
id: await generateNewIdAsync(),
action: 'create',
data: {
id: codeId,
code,
redirectUri: redirect_uri,
oauthAppId: oauthApp.id,
applicationId: context.getApplicationId()!,
userId: context.getCurrentUserId()!,
scope: [scope || ""],
expiresAt: Date.now() + 10 * 60 * 1000, // 10分钟后过期
}
}, {})
// 更新记录
await context.operate("oauthUserAuthorization", {
id: await generateNewIdAsync(),
action: 'update',
data: {
codeId: codeId,
},
filter: {
id: recordId,
}
}, {})
const params = new URLSearchParams();
params.set('code', code);
if (state) {
params.set('state', state);
}
closeRootMode();
return {
redirectUri: `${redirect_uri}?${params.toString()}`,
}
}
closeRootMode();
throw new Error('unknown action');
}
const fetchOAuthUserInfo = async (
code: string,
providerConfig: EntityDict['oauthProvider']['Schema'],
): Promise<{
oauthUserInfo: {
providerUserId: string;
rawData: any;
};
accessToken: string;
refreshToken?: string;
accessTokenExp?: number;
refreshTokenExp?: number;
}> => {
// 1. 使用 code 换取 access_token
const tokenResponse = await fetch(providerConfig.tokenEndpoint!, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
client_id: providerConfig.clientId!,
client_secret: providerConfig.clientSecret!,
redirect_uri: providerConfig.redirectUri!,
}),
});
if (!tokenResponse.ok) {
const errorjson = await tokenResponse.json();
if (errorjson.error == "unauthorized_client") {
throw new OakUserException(`授权校验已过期,请重新发起授权请求`);
} else if (errorjson.error == "invalid_grant") {
throw new OakUserException(`授权码无效或已过期,请重新发起授权请求`);
} else if (errorjson.error) {
throw new OakUserException(`获取访问令牌失败: ${errorjson.error_description || errorjson.error}`);
}
throw new OakUserException(`获取访问令牌失败: ${tokenResponse.statusText}`);
}
const tokenData = await tokenResponse.json();
const accessToken = tokenData.access_token;
const refreshToken = tokenData.refresh_token;
const accessTokenExp = tokenData.expires_in ? Date.now() + tokenData.expires_in * 1000 : undefined;
const refreshTokenExp = tokenData.refresh_expires_in ? Date.now() + tokenData.refresh_expires_in * 1000 : undefined;
const tokenType = tokenData.token_type;
assert(tokenType && tokenType.toLowerCase() === 'bearer', '不支持的令牌类型');
// 2. 使用 access_token 获取用户信息
const userInfoResponse = await fetch(providerConfig.userInfoEndpoint!, {
method: 'GET',
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
if (!userInfoResponse.ok) {
throw new OakUserException(`获取用户信息失败: ${userInfoResponse.statusText}`);
}
const userInfoData = await userInfoResponse.json();
// TODO: 用户信息中获取唯一标识,通过注入解决: utils/oauth/index.ts
const { id: providerUserId } = await processUserInfo(providerConfig.type!, userInfoData)
if (!providerUserId) {
throw new OakUserException('用户信息中缺少唯一标识符');
}
return {
oauthUserInfo: {
providerUserId,
rawData: userInfoData,
},
accessToken,
refreshToken,
accessTokenExp,
refreshTokenExp,
};
}