feat: oauth相关trigger实现
This commit is contained in:
parent
dc3dadc824
commit
82073c86e0
|
|
@ -16,6 +16,9 @@ import wechatPublicTag from './wechatPublicTag';
|
||||||
import wechatMpJump from './wechatMpJump';
|
import wechatMpJump from './wechatMpJump';
|
||||||
import systemTriggers from './system';
|
import systemTriggers from './system';
|
||||||
import passportTriggers from './passport';
|
import passportTriggers from './passport';
|
||||||
|
import oauthAppsTriggers from './oauthApps';
|
||||||
|
import oauthProviderTriggers from './oauthProvider';
|
||||||
|
import oauthUserTriggers from './oauthUser';
|
||||||
|
|
||||||
// import accountTriggers from './account';
|
// import accountTriggers from './account';
|
||||||
|
|
||||||
|
|
@ -40,4 +43,7 @@ export default [
|
||||||
...wechatMpJump,
|
...wechatMpJump,
|
||||||
...systemTriggers,
|
...systemTriggers,
|
||||||
...passportTriggers,
|
...passportTriggers,
|
||||||
|
...oauthAppsTriggers,
|
||||||
|
...oauthProviderTriggers,
|
||||||
|
...oauthUserTriggers,
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { CreateTrigger, Trigger, UpdateTrigger } from 'oak-domain/lib/types';
|
||||||
|
import { BackendRuntimeContext } from '../context/BackendRuntimeContext';
|
||||||
|
import assert from 'assert';
|
||||||
|
import { formUuid, generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
|
||||||
|
import { randomUUID } from 'crypto';
|
||||||
|
import { EntityDict } from '../oak-app-domain/EntityDict';
|
||||||
|
import { BRC } from '../types/RuntimeCxt';
|
||||||
|
|
||||||
|
const triggers: Trigger<EntityDict, "oauthApplication", BRC<EntityDict>>[] = [
|
||||||
|
{
|
||||||
|
name: "创建oauth app时,填充数据",
|
||||||
|
action: "create",
|
||||||
|
when: "before",
|
||||||
|
entity: "oauthApplication",
|
||||||
|
fn: async ({ operation }, context) => {
|
||||||
|
assert(operation.data && !Array.isArray(operation.data), "oauthApplication create data 必须存在且为单条记录")
|
||||||
|
const { data } = operation;
|
||||||
|
const systemId = context.getSystemId();
|
||||||
|
|
||||||
|
data.systemId = systemId;
|
||||||
|
data.clientSecret = randomUUID();
|
||||||
|
|
||||||
|
return 0; // 没有引起数据库行修改
|
||||||
|
}
|
||||||
|
} as CreateTrigger<EntityDict, "oauthApplication", BRC<EntityDict>>,
|
||||||
|
{
|
||||||
|
name: "更新apps的secret",
|
||||||
|
action: "resetSecret",
|
||||||
|
when: "before",
|
||||||
|
entity: "oauthApplication",
|
||||||
|
fn: async ({ operation }, context) => {
|
||||||
|
const { filter } = operation;
|
||||||
|
assert(filter && filter.id, "resetSecret 操作必须指定 filter.id")
|
||||||
|
|
||||||
|
const opRes = await context.operate("oauthApplication", {
|
||||||
|
id: await generateNewIdAsync(),
|
||||||
|
action: "update",
|
||||||
|
data: {
|
||||||
|
clientSecret: randomUUID(),
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
id: filter.id,
|
||||||
|
}
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
return opRes;
|
||||||
|
}
|
||||||
|
} as Trigger<EntityDict, "oauthApplication", BRC<EntityDict>>,
|
||||||
|
|
||||||
|
];
|
||||||
|
|
||||||
|
export default triggers;
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { CreateTrigger, Trigger } from 'oak-domain/lib/types';
|
||||||
|
import { BackendRuntimeContext } from '../context/BackendRuntimeContext';
|
||||||
|
import assert from 'assert';
|
||||||
|
import { BRC } from '../types/RuntimeCxt';
|
||||||
|
import { EntityDict } from '../oak-app-domain/EntityDict';
|
||||||
|
|
||||||
|
const triggers: Trigger<EntityDict, "oauthProvider", BRC<EntityDict>>[] = [
|
||||||
|
{
|
||||||
|
name: "创建provider时,填充数据",
|
||||||
|
action: "create",
|
||||||
|
when: "before",
|
||||||
|
entity: "oauthProvider",
|
||||||
|
fn: async ({ operation }, context) => {
|
||||||
|
assert(operation.data && !Array.isArray(operation.data), "oauthProvider create data 必须存在且为单条记录")
|
||||||
|
const { data } = operation;
|
||||||
|
const systemId = context.getSystemId();
|
||||||
|
|
||||||
|
data.systemId = systemId;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} as CreateTrigger<EntityDict, "oauthProvider", BRC<EntityDict>>,
|
||||||
|
];
|
||||||
|
|
||||||
|
export default triggers;
|
||||||
|
|
@ -0,0 +1,222 @@
|
||||||
|
import { CreateTrigger, Datetime, Trigger } from 'oak-domain/lib/types';
|
||||||
|
import { BackendRuntimeContext } from '../context/BackendRuntimeContext';
|
||||||
|
import assert from 'assert';
|
||||||
|
import { BRC } from '../types/RuntimeCxt';
|
||||||
|
import { EntityDict } from '../oak-app-domain/EntityDict';
|
||||||
|
import data from '../data';
|
||||||
|
import { extraFileProjection } from '../types/Projection';
|
||||||
|
import { OpenIDUserInfo } from '../types/OpenID';
|
||||||
|
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
|
||||||
|
import { Operation as ExtraFileOperation } from '../oak-app-domain/ExtraFile/Schema';
|
||||||
|
import { UpdateOperationData as UpdateUserData } from '../oak-app-domain/User/Schema';
|
||||||
|
import { composeFileUrl } from '../utils/cos/index.backend';
|
||||||
|
import { processUserInfo } from '../utils/oauth';
|
||||||
|
|
||||||
|
const triggers: Trigger<EntityDict, "oauthUser", BRC<EntityDict>>[] = [
|
||||||
|
{
|
||||||
|
name: "加载用户信息",
|
||||||
|
action: "loadUserInfo",
|
||||||
|
when: "before",
|
||||||
|
entity: "oauthUser",
|
||||||
|
fn: async ({ operation }, context) => {
|
||||||
|
const { filter } = operation;
|
||||||
|
assert(filter && filter.id, "loadUserInfo 操作必须指定 filter.id ")
|
||||||
|
|
||||||
|
const [findOauthUser] = await context.select("oauthUser", {
|
||||||
|
data: {
|
||||||
|
id: 1,
|
||||||
|
userId: 1,
|
||||||
|
providerUserId: 1,
|
||||||
|
rawUserInfo: 1,
|
||||||
|
user: {
|
||||||
|
id: 1,
|
||||||
|
name: 1,
|
||||||
|
nickname: 1,
|
||||||
|
extraFile$entity: {
|
||||||
|
$entity: 'extraFile',
|
||||||
|
data: extraFileProjection,
|
||||||
|
filter: {
|
||||||
|
tag1: 'avatar',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
providerConfig: {
|
||||||
|
type: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
id: filter.id,
|
||||||
|
},
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
assert(findOauthUser, `oauthUser ${filter.id} 不存在`)
|
||||||
|
assert(findOauthUser.user, `oauthUser ${filter.id} 关联的 user 不存在`)
|
||||||
|
assert(findOauthUser.providerConfig, `oauthUser ${filter.id} 关联的 providerConfig 不存在`)
|
||||||
|
|
||||||
|
const providerType = findOauthUser.providerConfig.type;
|
||||||
|
let updateUserInfo: UpdateUserData = {};
|
||||||
|
let newAvatarUrl: string | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
const { id, name, nickname, birth, gender, avatarUrl } = await processUserInfo(providerType!, findOauthUser.rawUserInfo || {})
|
||||||
|
updateUserInfo = {
|
||||||
|
name: name || findOauthUser.user.name,
|
||||||
|
nickname: nickname || findOauthUser.user.nickname,
|
||||||
|
birth: birth as Datetime | undefined || findOauthUser.user.birth,
|
||||||
|
gender: gender || findOauthUser.user.gender,
|
||||||
|
};
|
||||||
|
|
||||||
|
newAvatarUrl = avatarUrl;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`处理 oauthUser ${filter.id} 的 rawUserInfo 失败: 将使用默认生成数据`, err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { extraFile$entity, id } = findOauthUser.user;
|
||||||
|
const application = context.getApplication();
|
||||||
|
|
||||||
|
if (
|
||||||
|
newAvatarUrl &&
|
||||||
|
(extraFile$entity?.length === 0 ||
|
||||||
|
composeFileUrl<EntityDict>(application as EntityDict['application']['Schema'], extraFile$entity![0] as EntityDict['extraFile']['Schema']) !== newAvatarUrl)
|
||||||
|
) {
|
||||||
|
console.log("需要更新用户头像为:", newAvatarUrl);
|
||||||
|
|
||||||
|
// 需要更新新的avatar extra file
|
||||||
|
const extraFileOperations: ExtraFileOperation['data'][] = [
|
||||||
|
{
|
||||||
|
id: await generateNewIdAsync(),
|
||||||
|
action: 'create',
|
||||||
|
data: Object.assign({
|
||||||
|
id: await generateNewIdAsync(),
|
||||||
|
tag1: 'avatar',
|
||||||
|
entity: 'user',
|
||||||
|
entityId: findOauthUser.user.id,
|
||||||
|
objectId: await generateNewIdAsync(),
|
||||||
|
origin: 'unknown',
|
||||||
|
extra1: newAvatarUrl,
|
||||||
|
type: 'image',
|
||||||
|
filename: '',
|
||||||
|
bucket: '',
|
||||||
|
applicationId: context.getApplicationId()!,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
if (extraFile$entity!.length > 0) {
|
||||||
|
extraFileOperations.push({
|
||||||
|
id: await generateNewIdAsync(),
|
||||||
|
action: 'remove',
|
||||||
|
data: {},
|
||||||
|
filter: {
|
||||||
|
id: extraFile$entity![0].id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(updateUserInfo, {
|
||||||
|
extraFile$entity: extraFileOperations,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(updateUserInfo).length > 0) {
|
||||||
|
// 更新用户基本信息
|
||||||
|
await context.operate('user', {
|
||||||
|
id: await generateNewIdAsync(),
|
||||||
|
action: 'update',
|
||||||
|
data: updateUserInfo,
|
||||||
|
filter: {
|
||||||
|
id: id,
|
||||||
|
}
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} as Trigger<EntityDict, "oauthUser", BRC<EntityDict>>,
|
||||||
|
{
|
||||||
|
name: "刷新令牌",
|
||||||
|
action: "refreshTokens",
|
||||||
|
when: "before",
|
||||||
|
entity: "oauthUser",
|
||||||
|
fn: async ({ operation }, context) => {
|
||||||
|
const { filter } = operation;
|
||||||
|
const oauthUsers = await context.select("oauthUser", {
|
||||||
|
data: {
|
||||||
|
id: 1,
|
||||||
|
accessToken: 1,
|
||||||
|
accessExpiresAt: 1,
|
||||||
|
refreshToken: 1,
|
||||||
|
refreshExpiresAt: 1,
|
||||||
|
state: {
|
||||||
|
provider: {
|
||||||
|
refreshEndpoint: 1,
|
||||||
|
clientId: 1,
|
||||||
|
clientSecret: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
filter: filter,
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
if (oauthUsers.length === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const oauthUser of oauthUsers) {
|
||||||
|
assert(oauthUser.state, `oauthUser ${oauthUser.id} 关联的 state 不存在`);
|
||||||
|
assert(oauthUser.state.provider, `oauthUser ${oauthUser.id} 关联的 state 的 provider 不存在`);
|
||||||
|
const refreshEndpoint = oauthUser.state.provider.refreshEndpoint;
|
||||||
|
assert(refreshEndpoint, `oauthUser ${oauthUser.id} 关联的 provider 不支持刷新令牌`);
|
||||||
|
|
||||||
|
// 根据 RFC 6749 规范 使用 refresh token 刷新 access token
|
||||||
|
const authHeaderRaw = Buffer.from(`${oauthUser.state.provider.clientId}:${oauthUser.state.provider.clientSecret}`).toString('base64');
|
||||||
|
const resp = await fetch(refreshEndpoint!, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'Authorization': `Basic ${authHeaderRaw}`,
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
grant_type: 'refresh_token',
|
||||||
|
refresh_token: oauthUser.refreshToken!,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resp.ok) {
|
||||||
|
console.error(`刷新 oauthUser ${oauthUser.id} 令牌失败: `, await resp.text());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenData = await resp.json();
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
const newAccessToken = tokenData.access_token;
|
||||||
|
const newRefreshToken = tokenData.refresh_token || oauthUser.refreshToken;
|
||||||
|
const accessExpiresAt = tokenData.expires_in ? new Date(now.getTime() + tokenData.expires_in * 1000) : oauthUser.accessExpiresAt;
|
||||||
|
const refreshExpiresAt = tokenData.refresh_expires_in ? new Date(now.getTime() + tokenData.refresh_expires_in * 1000) : oauthUser.refreshExpiresAt;
|
||||||
|
|
||||||
|
await context.operate('oauthUser', {
|
||||||
|
id: await generateNewIdAsync(),
|
||||||
|
action: 'update',
|
||||||
|
data: {
|
||||||
|
accessToken: newAccessToken,
|
||||||
|
refreshToken: newRefreshToken,
|
||||||
|
accessExpiresAt: accessExpiresAt,
|
||||||
|
refreshExpiresAt: refreshExpiresAt,
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
id: oauthUser.id,
|
||||||
|
}
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} as Trigger<EntityDict, "oauthUser", BRC<EntityDict>>,
|
||||||
|
];
|
||||||
|
|
||||||
|
export default triggers;
|
||||||
Loading…
Reference in New Issue