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 systemTriggers from './system';
|
||||
import passportTriggers from './passport';
|
||||
import oauthAppsTriggers from './oauthApps';
|
||||
import oauthProviderTriggers from './oauthProvider';
|
||||
import oauthUserTriggers from './oauthUser';
|
||||
|
||||
// import accountTriggers from './account';
|
||||
|
||||
|
|
@ -40,4 +43,7 @@ export default [
|
|||
...wechatMpJump,
|
||||
...systemTriggers,
|
||||
...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