feat: 实现派发方的revokr逻辑
This commit is contained in:
parent
6f5dead6cc
commit
9a05b713ab
|
|
@ -1,7 +1,8 @@
|
||||||
import { generateNewIdAsync } from "oak-domain/lib/utils/uuid";
|
import { generateNewIdAsync } from "oak-domain/lib/utils/uuid";
|
||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from "crypto";
|
||||||
import { applicationProjection, extraFileProjection } from "../types/Projection";
|
|
||||||
import { composeFileUrl } from "../utils/cos/index.backend";
|
import { composeFileUrl } from "../utils/cos/index.backend";
|
||||||
|
import assert from "assert";
|
||||||
|
import { checkOauthTokenAvaliable } from "../utils/oauth";
|
||||||
const oauthTokenEndpoint = {
|
const oauthTokenEndpoint = {
|
||||||
name: "获取OAuth Token",
|
name: "获取OAuth Token",
|
||||||
params: [],
|
params: [],
|
||||||
|
|
@ -93,11 +94,12 @@ const oauthTokenEndpoint = {
|
||||||
const refreshToken = randomUUID();
|
const refreshToken = randomUUID();
|
||||||
const refreshTokenExpiresIn = 86400 * 30; // 30 days
|
const refreshTokenExpiresIn = 86400 * 30; // 30 days
|
||||||
// create
|
// create
|
||||||
|
const tokenId = await generateNewIdAsync();
|
||||||
await context.operate("oauthToken", {
|
await context.operate("oauthToken", {
|
||||||
id: await generateNewIdAsync(),
|
id: await generateNewIdAsync(),
|
||||||
action: "create",
|
action: "create",
|
||||||
data: {
|
data: {
|
||||||
id: await generateNewIdAsync(),
|
id: tokenId,
|
||||||
accessToken: genaccessToken,
|
accessToken: genaccessToken,
|
||||||
refreshToken: refreshToken,
|
refreshToken: refreshToken,
|
||||||
userId: authCodeRecord.userId,
|
userId: authCodeRecord.userId,
|
||||||
|
|
@ -106,6 +108,17 @@ const oauthTokenEndpoint = {
|
||||||
codeId: authCodeRecord.id,
|
codeId: authCodeRecord.id,
|
||||||
}
|
}
|
||||||
}, {});
|
}, {});
|
||||||
|
// 创建记录
|
||||||
|
await context.operate("oauthUserAuthorization", {
|
||||||
|
id: await generateNewIdAsync(),
|
||||||
|
action: "update",
|
||||||
|
data: {
|
||||||
|
tokenId: tokenId,
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
codeId: authCodeRecord.id,
|
||||||
|
}
|
||||||
|
}, {});
|
||||||
// 标记code为已使用
|
// 标记code为已使用
|
||||||
await context.operate("oauthAuthorizationCode", {
|
await context.operate("oauthAuthorizationCode", {
|
||||||
id: await generateNewIdAsync(),
|
id: await generateNewIdAsync(),
|
||||||
|
|
@ -139,75 +152,23 @@ const oauthUserInfoEndpoint = {
|
||||||
fn: async (contextBuilder, params, header, req, body) => {
|
fn: async (contextBuilder, params, header, req, body) => {
|
||||||
const context = await contextBuilder();
|
const context = await contextBuilder();
|
||||||
const token = header.authorization; // Bearer token
|
const token = header.authorization; // Bearer token
|
||||||
// Validate and decode the token
|
const checkResult = await checkOauthTokenAvaliable(context, token);
|
||||||
const decoded = validateToken(token);
|
if (checkResult.error) {
|
||||||
if (decoded.error) {
|
await context.commit();
|
||||||
return {
|
return {
|
||||||
statusCode: 401,
|
statusCode: checkResult.statusCode || 401,
|
||||||
data: { error: decoded.error, success: false }
|
data: { error: checkResult.error, success: false }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// 获取token记录
|
const tokenRecord = checkResult.tokenRecord;
|
||||||
const [tokenRecord] = await context.select("oauthToken", {
|
assert(tokenRecord?.user, "User must be present in token record");
|
||||||
data: {
|
assert(tokenRecord?.code?.application, "Application must be present in token record");
|
||||||
id: 1,
|
|
||||||
user: {
|
|
||||||
id: 1,
|
|
||||||
name: 1,
|
|
||||||
nickname: 1,
|
|
||||||
birth: 1,
|
|
||||||
gender: 1,
|
|
||||||
extraFile$entity: {
|
|
||||||
$entity: 'extraFile',
|
|
||||||
data: extraFileProjection,
|
|
||||||
filter: {
|
|
||||||
tag1: 'avatar',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
accessExpiresAt: 1,
|
|
||||||
code: {
|
|
||||||
id: 1,
|
|
||||||
application: applicationProjection,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
filter: {
|
|
||||||
accessToken: decoded.token,
|
|
||||||
}
|
|
||||||
}, {});
|
|
||||||
if (!tokenRecord) {
|
|
||||||
await context.commit();
|
|
||||||
return { statusCode: 401, data: { error: "Invalid token", success: false } };
|
|
||||||
}
|
|
||||||
if (tokenRecord.accessExpiresAt < Date.now()) {
|
|
||||||
await context.commit();
|
|
||||||
return { statusCode: 401, data: { error: "Token expired", success: false } };
|
|
||||||
}
|
|
||||||
if (!tokenRecord.user) {
|
|
||||||
await context.commit();
|
|
||||||
return { statusCode: 401, data: { error: "User not found", success: false } };
|
|
||||||
}
|
|
||||||
if (!tokenRecord.code || !tokenRecord.code.application) {
|
|
||||||
await context.commit();
|
|
||||||
return { statusCode: 401, data: { error: "Application not found", success: false } };
|
|
||||||
}
|
|
||||||
const extrafile = tokenRecord.user.extraFile$entity?.[0];
|
const extrafile = tokenRecord.user.extraFile$entity?.[0];
|
||||||
const application = tokenRecord.code.application;
|
const application = tokenRecord.code.application;
|
||||||
let avatarUrl = '';
|
let avatarUrl = '';
|
||||||
if (extrafile) {
|
if (extrafile) {
|
||||||
avatarUrl = composeFileUrl(application, extrafile);
|
avatarUrl = composeFileUrl(application, extrafile);
|
||||||
}
|
}
|
||||||
// 更新最后使用日期
|
|
||||||
await context.operate("oauthToken", {
|
|
||||||
id: await generateNewIdAsync(),
|
|
||||||
action: "update",
|
|
||||||
data: {
|
|
||||||
lastUsedAt: Date.now(),
|
|
||||||
},
|
|
||||||
filter: {
|
|
||||||
id: tokenRecord.id,
|
|
||||||
}
|
|
||||||
}, {});
|
|
||||||
await context.commit();
|
await context.commit();
|
||||||
return {
|
return {
|
||||||
statusCode: 200, data: {
|
statusCode: 200, data: {
|
||||||
|
|
@ -224,16 +185,6 @@ const oauthUserInfoEndpoint = {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
function validateToken(token) {
|
|
||||||
if (!token) {
|
|
||||||
return { token: "", error: "Missing authorization token" };
|
|
||||||
}
|
|
||||||
// Token validation logic here
|
|
||||||
if (!token.startsWith("Bearer ")) {
|
|
||||||
return { token: "", error: "Invalid token format" };
|
|
||||||
}
|
|
||||||
return { token: token.slice(7), error: null };
|
|
||||||
}
|
|
||||||
const refreshTokenEndpoint = {
|
const refreshTokenEndpoint = {
|
||||||
name: "刷新OAuth令牌",
|
name: "刷新OAuth令牌",
|
||||||
params: [],
|
params: [],
|
||||||
|
|
|
||||||
|
|
@ -40,5 +40,20 @@ export const entityDesc = {
|
||||||
revoked: '#6c757d',
|
revoked: '#6c757d',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
indexes: [
|
||||||
|
// 根据授权码查询唯一记录
|
||||||
|
{
|
||||||
|
name: 'idx_code_id',
|
||||||
|
attributes: [
|
||||||
|
{
|
||||||
|
name: 'code',
|
||||||
|
direction: 'ASC',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
config: {
|
||||||
|
unique: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -29,5 +29,20 @@ export const desc = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actionType: "crud",
|
actionType: "crud",
|
||||||
actions
|
actions,
|
||||||
|
indexes: [
|
||||||
|
// 根据授权码查询唯一记录
|
||||||
|
{
|
||||||
|
name: 'idx_code_id',
|
||||||
|
attributes: [
|
||||||
|
{
|
||||||
|
name: "codeId",
|
||||||
|
direction: 'ASC',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
config: {
|
||||||
|
unique: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
declare const _default: (import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "message", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "address", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "application", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "article", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "articleMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "extraFile", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "user", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "userEntityGrant", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatQrCode", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "notification", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatLogin", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "parasite", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "sessionMessage", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatPublicTag", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMpJump", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "system", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "passport", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthApplication", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthProvider", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthUser", import("..").BRC<import("../oak-app-domain").EntityDict>>)[];
|
declare const _default: (import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthUser", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthUserAuthorization", import("../context/BackendRuntimeContext").BackendRuntimeContext<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "application", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "address", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "user", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "userEntityGrant", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatQrCode", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "message", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "notification", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatLogin", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "articleMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "article", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "parasite", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "extraFile", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "sessionMessage", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatPublicTag", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMpJump", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "system", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "passport", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthApplication", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthProvider", import("..").BRC<import("../oak-app-domain").EntityDict>>)[];
|
||||||
export default _default;
|
export default _default;
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import passportTriggers from './passport';
|
||||||
import oauthAppsTriggers from './oauthApps';
|
import oauthAppsTriggers from './oauthApps';
|
||||||
import oauthProviderTriggers from './oauthProvider';
|
import oauthProviderTriggers from './oauthProvider';
|
||||||
import oauthUserTriggers from './oauthUser';
|
import oauthUserTriggers from './oauthUser';
|
||||||
|
import oauthUserAuthTriggers from './oauthUserAuth';
|
||||||
// import accountTriggers from './account';
|
// import accountTriggers from './account';
|
||||||
export default [
|
export default [
|
||||||
// ...accountTriggers,
|
// ...accountTriggers,
|
||||||
|
|
@ -43,4 +44,5 @@ export default [
|
||||||
...oauthAppsTriggers,
|
...oauthAppsTriggers,
|
||||||
...oauthProviderTriggers,
|
...oauthProviderTriggers,
|
||||||
...oauthUserTriggers,
|
...oauthUserTriggers,
|
||||||
|
...oauthUserAuthTriggers,
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { Trigger } from 'oak-domain/lib/types';
|
||||||
|
import { BackendRuntimeContext } from '../context/BackendRuntimeContext';
|
||||||
|
import { EntityDict } from '../oak-app-domain';
|
||||||
|
declare const triggers: Trigger<EntityDict, "oauthUserAuthorization", BackendRuntimeContext<EntityDict>>[];
|
||||||
|
export default triggers;
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
import assert from 'assert';
|
||||||
|
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
|
||||||
|
const triggers = [
|
||||||
|
{
|
||||||
|
name: "在撤销用户OAuth授权前,执行操作",
|
||||||
|
action: "revoke",
|
||||||
|
when: "after",
|
||||||
|
entity: "oauthUserAuthorization",
|
||||||
|
fn: async ({ operation }, context) => {
|
||||||
|
const { filter } = operation;
|
||||||
|
assert(filter, 'No filter found in revoke operation');
|
||||||
|
let res = 0;
|
||||||
|
// 如果没有token,可以直接删除oauthUserAuthorization
|
||||||
|
const opRes = await context.operate("oauthUserAuthorization", {
|
||||||
|
id: await generateNewIdAsync(),
|
||||||
|
action: "remove",
|
||||||
|
data: {},
|
||||||
|
filter: {
|
||||||
|
...filter,
|
||||||
|
tokenId: {
|
||||||
|
$exists: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {});
|
||||||
|
res += opRes.oauthApplication?.remove || 0;
|
||||||
|
// 如果有token,则将token的revokedAt设置为当前时间
|
||||||
|
const opRes2 = await context.operate("oauthToken", {
|
||||||
|
id: await generateNewIdAsync(),
|
||||||
|
action: "update",
|
||||||
|
data: {
|
||||||
|
revokedAt: new Date()
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
oauthUserAuthorization$token: {
|
||||||
|
...filter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {});
|
||||||
|
res += opRes2.oauthToken?.update || 0;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
export default triggers;
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import BackendRuntimeContext from "../../context/BackendRuntimeContext";
|
||||||
import { EntityDict } from "../../oak-app-domain";
|
import { EntityDict } from "../../oak-app-domain";
|
||||||
type UserInfo = EntityDict['oauthUser']['Schema']['rawUserInfo'];
|
type UserInfo = EntityDict['oauthUser']['Schema']['rawUserInfo'];
|
||||||
type UserID = {
|
type UserID = {
|
||||||
|
|
@ -11,4 +12,9 @@ type UserID = {
|
||||||
export type UserInfoHandler = (data: UserInfo) => UserID | Promise<UserID>;
|
export type UserInfoHandler = (data: UserInfo) => UserID | Promise<UserID>;
|
||||||
export declare const registerUserinfoHandler: (type: EntityDict["oauthProvider"]["Schema"]["type"], handler: UserInfoHandler) => void;
|
export declare const registerUserinfoHandler: (type: EntityDict["oauthProvider"]["Schema"]["type"], handler: UserInfoHandler) => void;
|
||||||
export declare const processUserInfo: (type: EntityDict["oauthProvider"]["Schema"]["type"], data: UserInfo) => UserID | Promise<UserID>;
|
export declare const processUserInfo: (type: EntityDict["oauthProvider"]["Schema"]["type"], data: UserInfo) => UserID | Promise<UserID>;
|
||||||
|
export declare function checkOauthTokenAvaliable(context: BackendRuntimeContext<EntityDict>, token: string | undefined): Promise<{
|
||||||
|
error: string | null;
|
||||||
|
statusCode?: number;
|
||||||
|
tokenRecord?: Partial<EntityDict['oauthToken']['Schema']>;
|
||||||
|
}>;
|
||||||
export {};
|
export {};
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { generateNewIdAsync } from "oak-domain/lib/utils/uuid";
|
||||||
|
import { applicationProjection, extraFileProjection } from "../../types/Projection";
|
||||||
import { getDefaultHandlers } from "./handler";
|
import { getDefaultHandlers } from "./handler";
|
||||||
const handlerMap = new Map();
|
const handlerMap = new Map();
|
||||||
export const registerUserinfoHandler = (type, handler) => {
|
export const registerUserinfoHandler = (type, handler) => {
|
||||||
|
|
@ -17,3 +19,80 @@ const defaulthandlers = getDefaultHandlers();
|
||||||
Object.entries(defaulthandlers).forEach(([type, handler]) => {
|
Object.entries(defaulthandlers).forEach(([type, handler]) => {
|
||||||
registerUserinfoHandler(type, handler);
|
registerUserinfoHandler(type, handler);
|
||||||
});
|
});
|
||||||
|
function validateToken(token) {
|
||||||
|
if (!token) {
|
||||||
|
return { token: "", error: "Missing authorization token" };
|
||||||
|
}
|
||||||
|
// Token validation logic here
|
||||||
|
if (!token.startsWith("Bearer ")) {
|
||||||
|
return { token: "", error: "Invalid token format" };
|
||||||
|
}
|
||||||
|
return { token: token.slice(7), error: null };
|
||||||
|
}
|
||||||
|
// 工具函数
|
||||||
|
export async function checkOauthTokenAvaliable(context, token) {
|
||||||
|
// Validate and decode the token
|
||||||
|
const decoded = validateToken(token);
|
||||||
|
if (decoded.error) {
|
||||||
|
return {
|
||||||
|
error: decoded.error,
|
||||||
|
statusCode: 401
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 获取token记录
|
||||||
|
const [tokenRecord] = await context.select("oauthToken", {
|
||||||
|
data: {
|
||||||
|
id: 1,
|
||||||
|
user: {
|
||||||
|
id: 1,
|
||||||
|
name: 1,
|
||||||
|
nickname: 1,
|
||||||
|
birth: 1,
|
||||||
|
gender: 1,
|
||||||
|
extraFile$entity: {
|
||||||
|
$entity: 'extraFile',
|
||||||
|
data: extraFileProjection,
|
||||||
|
filter: {
|
||||||
|
tag1: 'avatar',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
accessExpiresAt: 1,
|
||||||
|
revokedAt: 1,
|
||||||
|
code: {
|
||||||
|
id: 1,
|
||||||
|
application: applicationProjection,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
accessToken: decoded.token,
|
||||||
|
}
|
||||||
|
}, {});
|
||||||
|
if (!tokenRecord) {
|
||||||
|
return { error: "Invalid token", statusCode: 401 };
|
||||||
|
}
|
||||||
|
if (tokenRecord.accessExpiresAt < Date.now()) {
|
||||||
|
return { error: "Token expired", statusCode: 401 };
|
||||||
|
}
|
||||||
|
if (tokenRecord.revokedAt) {
|
||||||
|
return { error: "Token revoked", statusCode: 401 };
|
||||||
|
}
|
||||||
|
if (!tokenRecord.user) {
|
||||||
|
return { error: "User not found", statusCode: 401 };
|
||||||
|
}
|
||||||
|
if (!tokenRecord.code || !tokenRecord.code.application) {
|
||||||
|
return { error: "Application not found", statusCode: 401 };
|
||||||
|
}
|
||||||
|
// 更新最后使用日期
|
||||||
|
await context.operate("oauthToken", {
|
||||||
|
id: await generateNewIdAsync(),
|
||||||
|
action: "update",
|
||||||
|
data: {
|
||||||
|
lastUsedAt: Date.now(),
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
id: tokenRecord.id,
|
||||||
|
}
|
||||||
|
}, {});
|
||||||
|
return { error: null, tokenRecord };
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const tslib_1 = require("tslib");
|
||||||
const uuid_1 = require("oak-domain/lib/utils/uuid");
|
const uuid_1 = require("oak-domain/lib/utils/uuid");
|
||||||
const crypto_1 = require("crypto");
|
const crypto_1 = require("crypto");
|
||||||
const Projection_1 = require("../types/Projection");
|
|
||||||
const index_backend_1 = require("../utils/cos/index.backend");
|
const index_backend_1 = require("../utils/cos/index.backend");
|
||||||
|
const assert_1 = tslib_1.__importDefault(require("assert"));
|
||||||
|
const oauth_1 = require("../utils/oauth");
|
||||||
const oauthTokenEndpoint = {
|
const oauthTokenEndpoint = {
|
||||||
name: "获取OAuth Token",
|
name: "获取OAuth Token",
|
||||||
params: [],
|
params: [],
|
||||||
|
|
@ -95,11 +97,12 @@ const oauthTokenEndpoint = {
|
||||||
const refreshToken = (0, crypto_1.randomUUID)();
|
const refreshToken = (0, crypto_1.randomUUID)();
|
||||||
const refreshTokenExpiresIn = 86400 * 30; // 30 days
|
const refreshTokenExpiresIn = 86400 * 30; // 30 days
|
||||||
// create
|
// create
|
||||||
|
const tokenId = await (0, uuid_1.generateNewIdAsync)();
|
||||||
await context.operate("oauthToken", {
|
await context.operate("oauthToken", {
|
||||||
id: await (0, uuid_1.generateNewIdAsync)(),
|
id: await (0, uuid_1.generateNewIdAsync)(),
|
||||||
action: "create",
|
action: "create",
|
||||||
data: {
|
data: {
|
||||||
id: await (0, uuid_1.generateNewIdAsync)(),
|
id: tokenId,
|
||||||
accessToken: genaccessToken,
|
accessToken: genaccessToken,
|
||||||
refreshToken: refreshToken,
|
refreshToken: refreshToken,
|
||||||
userId: authCodeRecord.userId,
|
userId: authCodeRecord.userId,
|
||||||
|
|
@ -108,6 +111,17 @@ const oauthTokenEndpoint = {
|
||||||
codeId: authCodeRecord.id,
|
codeId: authCodeRecord.id,
|
||||||
}
|
}
|
||||||
}, {});
|
}, {});
|
||||||
|
// 创建记录
|
||||||
|
await context.operate("oauthUserAuthorization", {
|
||||||
|
id: await (0, uuid_1.generateNewIdAsync)(),
|
||||||
|
action: "update",
|
||||||
|
data: {
|
||||||
|
tokenId: tokenId,
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
codeId: authCodeRecord.id,
|
||||||
|
}
|
||||||
|
}, {});
|
||||||
// 标记code为已使用
|
// 标记code为已使用
|
||||||
await context.operate("oauthAuthorizationCode", {
|
await context.operate("oauthAuthorizationCode", {
|
||||||
id: await (0, uuid_1.generateNewIdAsync)(),
|
id: await (0, uuid_1.generateNewIdAsync)(),
|
||||||
|
|
@ -141,75 +155,23 @@ const oauthUserInfoEndpoint = {
|
||||||
fn: async (contextBuilder, params, header, req, body) => {
|
fn: async (contextBuilder, params, header, req, body) => {
|
||||||
const context = await contextBuilder();
|
const context = await contextBuilder();
|
||||||
const token = header.authorization; // Bearer token
|
const token = header.authorization; // Bearer token
|
||||||
// Validate and decode the token
|
const checkResult = await (0, oauth_1.checkOauthTokenAvaliable)(context, token);
|
||||||
const decoded = validateToken(token);
|
if (checkResult.error) {
|
||||||
if (decoded.error) {
|
await context.commit();
|
||||||
return {
|
return {
|
||||||
statusCode: 401,
|
statusCode: checkResult.statusCode || 401,
|
||||||
data: { error: decoded.error, success: false }
|
data: { error: checkResult.error, success: false }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// 获取token记录
|
const tokenRecord = checkResult.tokenRecord;
|
||||||
const [tokenRecord] = await context.select("oauthToken", {
|
(0, assert_1.default)(tokenRecord?.user, "User must be present in token record");
|
||||||
data: {
|
(0, assert_1.default)(tokenRecord?.code?.application, "Application must be present in token record");
|
||||||
id: 1,
|
|
||||||
user: {
|
|
||||||
id: 1,
|
|
||||||
name: 1,
|
|
||||||
nickname: 1,
|
|
||||||
birth: 1,
|
|
||||||
gender: 1,
|
|
||||||
extraFile$entity: {
|
|
||||||
$entity: 'extraFile',
|
|
||||||
data: Projection_1.extraFileProjection,
|
|
||||||
filter: {
|
|
||||||
tag1: 'avatar',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
accessExpiresAt: 1,
|
|
||||||
code: {
|
|
||||||
id: 1,
|
|
||||||
application: Projection_1.applicationProjection,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
filter: {
|
|
||||||
accessToken: decoded.token,
|
|
||||||
}
|
|
||||||
}, {});
|
|
||||||
if (!tokenRecord) {
|
|
||||||
await context.commit();
|
|
||||||
return { statusCode: 401, data: { error: "Invalid token", success: false } };
|
|
||||||
}
|
|
||||||
if (tokenRecord.accessExpiresAt < Date.now()) {
|
|
||||||
await context.commit();
|
|
||||||
return { statusCode: 401, data: { error: "Token expired", success: false } };
|
|
||||||
}
|
|
||||||
if (!tokenRecord.user) {
|
|
||||||
await context.commit();
|
|
||||||
return { statusCode: 401, data: { error: "User not found", success: false } };
|
|
||||||
}
|
|
||||||
if (!tokenRecord.code || !tokenRecord.code.application) {
|
|
||||||
await context.commit();
|
|
||||||
return { statusCode: 401, data: { error: "Application not found", success: false } };
|
|
||||||
}
|
|
||||||
const extrafile = tokenRecord.user.extraFile$entity?.[0];
|
const extrafile = tokenRecord.user.extraFile$entity?.[0];
|
||||||
const application = tokenRecord.code.application;
|
const application = tokenRecord.code.application;
|
||||||
let avatarUrl = '';
|
let avatarUrl = '';
|
||||||
if (extrafile) {
|
if (extrafile) {
|
||||||
avatarUrl = (0, index_backend_1.composeFileUrl)(application, extrafile);
|
avatarUrl = (0, index_backend_1.composeFileUrl)(application, extrafile);
|
||||||
}
|
}
|
||||||
// 更新最后使用日期
|
|
||||||
await context.operate("oauthToken", {
|
|
||||||
id: await (0, uuid_1.generateNewIdAsync)(),
|
|
||||||
action: "update",
|
|
||||||
data: {
|
|
||||||
lastUsedAt: Date.now(),
|
|
||||||
},
|
|
||||||
filter: {
|
|
||||||
id: tokenRecord.id,
|
|
||||||
}
|
|
||||||
}, {});
|
|
||||||
await context.commit();
|
await context.commit();
|
||||||
return {
|
return {
|
||||||
statusCode: 200, data: {
|
statusCode: 200, data: {
|
||||||
|
|
@ -226,16 +188,6 @@ const oauthUserInfoEndpoint = {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
function validateToken(token) {
|
|
||||||
if (!token) {
|
|
||||||
return { token: "", error: "Missing authorization token" };
|
|
||||||
}
|
|
||||||
// Token validation logic here
|
|
||||||
if (!token.startsWith("Bearer ")) {
|
|
||||||
return { token: "", error: "Invalid token format" };
|
|
||||||
}
|
|
||||||
return { token: token.slice(7), error: null };
|
|
||||||
}
|
|
||||||
const refreshTokenEndpoint = {
|
const refreshTokenEndpoint = {
|
||||||
name: "刷新OAuth令牌",
|
name: "刷新OAuth令牌",
|
||||||
params: [],
|
params: [],
|
||||||
|
|
|
||||||
|
|
@ -43,5 +43,20 @@ exports.entityDesc = {
|
||||||
revoked: '#6c757d',
|
revoked: '#6c757d',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
indexes: [
|
||||||
|
// 根据授权码查询唯一记录
|
||||||
|
{
|
||||||
|
name: 'idx_code_id',
|
||||||
|
attributes: [
|
||||||
|
{
|
||||||
|
name: 'code',
|
||||||
|
direction: 'ASC',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
config: {
|
||||||
|
unique: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -32,5 +32,20 @@ exports.desc = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actionType: "crud",
|
actionType: "crud",
|
||||||
actions: Action_1.actions
|
actions: Action_1.actions,
|
||||||
|
indexes: [
|
||||||
|
// 根据授权码查询唯一记录
|
||||||
|
{
|
||||||
|
name: 'idx_code_id',
|
||||||
|
attributes: [
|
||||||
|
{
|
||||||
|
name: "codeId",
|
||||||
|
direction: 'ASC',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
config: {
|
||||||
|
unique: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
declare const _default: (import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "message", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "address", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "application", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "article", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "articleMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "extraFile", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "user", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "userEntityGrant", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatQrCode", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "notification", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatLogin", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "parasite", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "sessionMessage", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatPublicTag", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMpJump", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "system", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "passport", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthApplication", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthProvider", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthUser", import("..").BRC<import("../oak-app-domain").EntityDict>>)[];
|
declare const _default: (import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthUser", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthUserAuthorization", import("../context/BackendRuntimeContext").BackendRuntimeContext<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "application", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "address", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "user", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "userEntityGrant", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatQrCode", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "message", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "notification", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatLogin", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "articleMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "article", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "parasite", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "extraFile", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "sessionMessage", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatPublicTag", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMpJump", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "system", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "passport", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthApplication", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthProvider", import("..").BRC<import("../oak-app-domain").EntityDict>>)[];
|
||||||
export default _default;
|
export default _default;
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ const passport_1 = tslib_1.__importDefault(require("./passport"));
|
||||||
const oauthApps_1 = tslib_1.__importDefault(require("./oauthApps"));
|
const oauthApps_1 = tslib_1.__importDefault(require("./oauthApps"));
|
||||||
const oauthProvider_1 = tslib_1.__importDefault(require("./oauthProvider"));
|
const oauthProvider_1 = tslib_1.__importDefault(require("./oauthProvider"));
|
||||||
const oauthUser_1 = tslib_1.__importDefault(require("./oauthUser"));
|
const oauthUser_1 = tslib_1.__importDefault(require("./oauthUser"));
|
||||||
|
const oauthUserAuth_1 = tslib_1.__importDefault(require("./oauthUserAuth"));
|
||||||
// import accountTriggers from './account';
|
// import accountTriggers from './account';
|
||||||
exports.default = [
|
exports.default = [
|
||||||
// ...accountTriggers,
|
// ...accountTriggers,
|
||||||
|
|
@ -46,4 +47,5 @@ exports.default = [
|
||||||
...oauthApps_1.default,
|
...oauthApps_1.default,
|
||||||
...oauthProvider_1.default,
|
...oauthProvider_1.default,
|
||||||
...oauthUser_1.default,
|
...oauthUser_1.default,
|
||||||
|
...oauthUserAuth_1.default,
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { Trigger } from 'oak-domain/lib/types';
|
||||||
|
import { BackendRuntimeContext } from '../context/BackendRuntimeContext';
|
||||||
|
import { EntityDict } from '../oak-app-domain';
|
||||||
|
declare const triggers: Trigger<EntityDict, "oauthUserAuthorization", BackendRuntimeContext<EntityDict>>[];
|
||||||
|
export default triggers;
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const tslib_1 = require("tslib");
|
||||||
|
const assert_1 = tslib_1.__importDefault(require("assert"));
|
||||||
|
const uuid_1 = require("oak-domain/lib/utils/uuid");
|
||||||
|
const triggers = [
|
||||||
|
{
|
||||||
|
name: "在撤销用户OAuth授权前,执行操作",
|
||||||
|
action: "revoke",
|
||||||
|
when: "after",
|
||||||
|
entity: "oauthUserAuthorization",
|
||||||
|
fn: async ({ operation }, context) => {
|
||||||
|
const { filter } = operation;
|
||||||
|
(0, assert_1.default)(filter, 'No filter found in revoke operation');
|
||||||
|
let res = 0;
|
||||||
|
// 如果没有token,可以直接删除oauthUserAuthorization
|
||||||
|
const opRes = await context.operate("oauthUserAuthorization", {
|
||||||
|
id: await (0, uuid_1.generateNewIdAsync)(),
|
||||||
|
action: "remove",
|
||||||
|
data: {},
|
||||||
|
filter: {
|
||||||
|
...filter,
|
||||||
|
tokenId: {
|
||||||
|
$exists: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {});
|
||||||
|
res += opRes.oauthApplication?.remove || 0;
|
||||||
|
// 如果有token,则将token的revokedAt设置为当前时间
|
||||||
|
const opRes2 = await context.operate("oauthToken", {
|
||||||
|
id: await (0, uuid_1.generateNewIdAsync)(),
|
||||||
|
action: "update",
|
||||||
|
data: {
|
||||||
|
revokedAt: new Date()
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
oauthUserAuthorization$token: {
|
||||||
|
...filter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {});
|
||||||
|
res += opRes2.oauthToken?.update || 0;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
exports.default = triggers;
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import BackendRuntimeContext from "../../context/BackendRuntimeContext";
|
||||||
import { EntityDict } from "../../oak-app-domain";
|
import { EntityDict } from "../../oak-app-domain";
|
||||||
type UserInfo = EntityDict['oauthUser']['Schema']['rawUserInfo'];
|
type UserInfo = EntityDict['oauthUser']['Schema']['rawUserInfo'];
|
||||||
type UserID = {
|
type UserID = {
|
||||||
|
|
@ -11,4 +12,9 @@ type UserID = {
|
||||||
export type UserInfoHandler = (data: UserInfo) => UserID | Promise<UserID>;
|
export type UserInfoHandler = (data: UserInfo) => UserID | Promise<UserID>;
|
||||||
export declare const registerUserinfoHandler: (type: EntityDict["oauthProvider"]["Schema"]["type"], handler: UserInfoHandler) => void;
|
export declare const registerUserinfoHandler: (type: EntityDict["oauthProvider"]["Schema"]["type"], handler: UserInfoHandler) => void;
|
||||||
export declare const processUserInfo: (type: EntityDict["oauthProvider"]["Schema"]["type"], data: UserInfo) => UserID | Promise<UserID>;
|
export declare const processUserInfo: (type: EntityDict["oauthProvider"]["Schema"]["type"], data: UserInfo) => UserID | Promise<UserID>;
|
||||||
|
export declare function checkOauthTokenAvaliable(context: BackendRuntimeContext<EntityDict>, token: string | undefined): Promise<{
|
||||||
|
error: string | null;
|
||||||
|
statusCode?: number;
|
||||||
|
tokenRecord?: Partial<EntityDict['oauthToken']['Schema']>;
|
||||||
|
}>;
|
||||||
export {};
|
export {};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.processUserInfo = exports.registerUserinfoHandler = void 0;
|
exports.processUserInfo = exports.registerUserinfoHandler = void 0;
|
||||||
|
exports.checkOauthTokenAvaliable = checkOauthTokenAvaliable;
|
||||||
|
const uuid_1 = require("oak-domain/lib/utils/uuid");
|
||||||
|
const Projection_1 = require("../../types/Projection");
|
||||||
const handler_1 = require("./handler");
|
const handler_1 = require("./handler");
|
||||||
const handlerMap = new Map();
|
const handlerMap = new Map();
|
||||||
const registerUserinfoHandler = (type, handler) => {
|
const registerUserinfoHandler = (type, handler) => {
|
||||||
|
|
@ -22,3 +25,80 @@ const defaulthandlers = (0, handler_1.getDefaultHandlers)();
|
||||||
Object.entries(defaulthandlers).forEach(([type, handler]) => {
|
Object.entries(defaulthandlers).forEach(([type, handler]) => {
|
||||||
(0, exports.registerUserinfoHandler)(type, handler);
|
(0, exports.registerUserinfoHandler)(type, handler);
|
||||||
});
|
});
|
||||||
|
function validateToken(token) {
|
||||||
|
if (!token) {
|
||||||
|
return { token: "", error: "Missing authorization token" };
|
||||||
|
}
|
||||||
|
// Token validation logic here
|
||||||
|
if (!token.startsWith("Bearer ")) {
|
||||||
|
return { token: "", error: "Invalid token format" };
|
||||||
|
}
|
||||||
|
return { token: token.slice(7), error: null };
|
||||||
|
}
|
||||||
|
// 工具函数
|
||||||
|
async function checkOauthTokenAvaliable(context, token) {
|
||||||
|
// Validate and decode the token
|
||||||
|
const decoded = validateToken(token);
|
||||||
|
if (decoded.error) {
|
||||||
|
return {
|
||||||
|
error: decoded.error,
|
||||||
|
statusCode: 401
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 获取token记录
|
||||||
|
const [tokenRecord] = await context.select("oauthToken", {
|
||||||
|
data: {
|
||||||
|
id: 1,
|
||||||
|
user: {
|
||||||
|
id: 1,
|
||||||
|
name: 1,
|
||||||
|
nickname: 1,
|
||||||
|
birth: 1,
|
||||||
|
gender: 1,
|
||||||
|
extraFile$entity: {
|
||||||
|
$entity: 'extraFile',
|
||||||
|
data: Projection_1.extraFileProjection,
|
||||||
|
filter: {
|
||||||
|
tag1: 'avatar',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
accessExpiresAt: 1,
|
||||||
|
revokedAt: 1,
|
||||||
|
code: {
|
||||||
|
id: 1,
|
||||||
|
application: Projection_1.applicationProjection,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
accessToken: decoded.token,
|
||||||
|
}
|
||||||
|
}, {});
|
||||||
|
if (!tokenRecord) {
|
||||||
|
return { error: "Invalid token", statusCode: 401 };
|
||||||
|
}
|
||||||
|
if (tokenRecord.accessExpiresAt < Date.now()) {
|
||||||
|
return { error: "Token expired", statusCode: 401 };
|
||||||
|
}
|
||||||
|
if (tokenRecord.revokedAt) {
|
||||||
|
return { error: "Token revoked", statusCode: 401 };
|
||||||
|
}
|
||||||
|
if (!tokenRecord.user) {
|
||||||
|
return { error: "User not found", statusCode: 401 };
|
||||||
|
}
|
||||||
|
if (!tokenRecord.code || !tokenRecord.code.application) {
|
||||||
|
return { error: "Application not found", statusCode: 401 };
|
||||||
|
}
|
||||||
|
// 更新最后使用日期
|
||||||
|
await context.operate("oauthToken", {
|
||||||
|
id: await (0, uuid_1.generateNewIdAsync)(),
|
||||||
|
action: "update",
|
||||||
|
data: {
|
||||||
|
lastUsedAt: Date.now(),
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
id: tokenRecord.id,
|
||||||
|
}
|
||||||
|
}, {});
|
||||||
|
return { error: null, tokenRecord };
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import BackendRuntimeContext from "../context/BackendRuntimeContext";
|
||||||
import { EntityDict } from "../oak-app-domain";
|
import { EntityDict } from "../oak-app-domain";
|
||||||
import { applicationProjection, extraFileProjection } from "../types/Projection";
|
import { applicationProjection, extraFileProjection } from "../types/Projection";
|
||||||
import { composeFileUrl } from "../utils/cos/index.backend";
|
import { composeFileUrl } from "../utils/cos/index.backend";
|
||||||
|
import assert from "assert";
|
||||||
|
import { checkOauthTokenAvaliable } from "../utils/oauth";
|
||||||
|
|
||||||
const oauthTokenEndpoint: Endpoint<EntityDict, BackendRuntimeContext<EntityDict>> = {
|
const oauthTokenEndpoint: Endpoint<EntityDict, BackendRuntimeContext<EntityDict>> = {
|
||||||
name: "获取OAuth Token",
|
name: "获取OAuth Token",
|
||||||
|
|
@ -170,64 +172,19 @@ const oauthUserInfoEndpoint: Endpoint<EntityDict, BackendRuntimeContext<EntityDi
|
||||||
fn: async (contextBuilder, params, header, req, body) => {
|
fn: async (contextBuilder, params, header, req, body) => {
|
||||||
const context = await contextBuilder()
|
const context = await contextBuilder()
|
||||||
const token = header.authorization; // Bearer token
|
const token = header.authorization; // Bearer token
|
||||||
// Validate and decode the token
|
|
||||||
const decoded = validateToken(token);
|
const checkResult = await checkOauthTokenAvaliable(context, token);
|
||||||
if (decoded.error) {
|
if (checkResult.error) {
|
||||||
|
await context.commit();
|
||||||
return {
|
return {
|
||||||
statusCode: 401,
|
statusCode: checkResult.statusCode || 401,
|
||||||
data: { error: decoded.error, success: false }
|
data: { error: checkResult.error, success: false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取token记录
|
const tokenRecord = checkResult.tokenRecord;
|
||||||
const [tokenRecord] = await context.select("oauthToken", {
|
assert(tokenRecord?.user, "User must be present in token record");
|
||||||
data: {
|
assert(tokenRecord?.code?.application, "Application must be present in token record");
|
||||||
id: 1,
|
|
||||||
user: {
|
|
||||||
id: 1,
|
|
||||||
name: 1,
|
|
||||||
nickname: 1,
|
|
||||||
birth: 1,
|
|
||||||
gender: 1,
|
|
||||||
extraFile$entity: {
|
|
||||||
$entity: 'extraFile',
|
|
||||||
data: extraFileProjection,
|
|
||||||
filter: {
|
|
||||||
tag1: 'avatar',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
accessExpiresAt: 1,
|
|
||||||
code: {
|
|
||||||
id: 1,
|
|
||||||
application: applicationProjection,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
filter: {
|
|
||||||
accessToken: decoded.token,
|
|
||||||
}
|
|
||||||
}, {})
|
|
||||||
|
|
||||||
if (!tokenRecord) {
|
|
||||||
await context.commit();
|
|
||||||
return { statusCode: 401, data: { error: "Invalid token", success: false } };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tokenRecord.accessExpiresAt as number < Date.now()) {
|
|
||||||
await context.commit();
|
|
||||||
return { statusCode: 401, data: { error: "Token expired", success: false } };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tokenRecord.user) {
|
|
||||||
await context.commit();
|
|
||||||
return { statusCode: 401, data: { error: "User not found", success: false } };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tokenRecord.code || !tokenRecord.code.application) {
|
|
||||||
await context.commit();
|
|
||||||
return { statusCode: 401, data: { error: "Application not found", success: false } };
|
|
||||||
}
|
|
||||||
|
|
||||||
const extrafile = tokenRecord.user.extraFile$entity?.[0];
|
const extrafile = tokenRecord.user.extraFile$entity?.[0];
|
||||||
const application = tokenRecord.code.application;
|
const application = tokenRecord.code.application;
|
||||||
let avatarUrl = '';
|
let avatarUrl = '';
|
||||||
|
|
@ -235,18 +192,6 @@ const oauthUserInfoEndpoint: Endpoint<EntityDict, BackendRuntimeContext<EntityDi
|
||||||
avatarUrl = composeFileUrl(application, extrafile as EntityDict['extraFile']['Schema']);
|
avatarUrl = composeFileUrl(application, extrafile as EntityDict['extraFile']['Schema']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新最后使用日期
|
|
||||||
await context.operate("oauthToken", {
|
|
||||||
id: await generateNewIdAsync(),
|
|
||||||
action: "update",
|
|
||||||
data: {
|
|
||||||
lastUsedAt: Date.now(),
|
|
||||||
},
|
|
||||||
filter: {
|
|
||||||
id: tokenRecord.id,
|
|
||||||
}
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
await context.commit();
|
await context.commit();
|
||||||
return {
|
return {
|
||||||
statusCode: 200, data: {
|
statusCode: 200, data: {
|
||||||
|
|
@ -264,21 +209,6 @@ const oauthUserInfoEndpoint: Endpoint<EntityDict, BackendRuntimeContext<EntityDi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateToken(token: string | undefined): {
|
|
||||||
token: string;
|
|
||||||
error: string | null;
|
|
||||||
} {
|
|
||||||
if (!token) {
|
|
||||||
return { token: "", error: "Missing authorization token" };
|
|
||||||
}
|
|
||||||
// Token validation logic here
|
|
||||||
if (!token.startsWith("Bearer ")) {
|
|
||||||
return { token: "", error: "Invalid token format" };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { token: token.slice(7), error: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
const refreshTokenEndpoint: Endpoint<EntityDict, BackendRuntimeContext<EntityDict>> = {
|
const refreshTokenEndpoint: Endpoint<EntityDict, BackendRuntimeContext<EntityDict>> = {
|
||||||
name: "刷新OAuth令牌",
|
name: "刷新OAuth令牌",
|
||||||
params: [],
|
params: [],
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import passportTriggers from './passport';
|
||||||
import oauthAppsTriggers from './oauthApps';
|
import oauthAppsTriggers from './oauthApps';
|
||||||
import oauthProviderTriggers from './oauthProvider';
|
import oauthProviderTriggers from './oauthProvider';
|
||||||
import oauthUserTriggers from './oauthUser';
|
import oauthUserTriggers from './oauthUser';
|
||||||
|
import oauthUserAuthTriggers from './oauthUserAuth';
|
||||||
|
|
||||||
// import accountTriggers from './account';
|
// import accountTriggers from './account';
|
||||||
|
|
||||||
|
|
@ -46,4 +47,5 @@ export default [
|
||||||
...oauthAppsTriggers,
|
...oauthAppsTriggers,
|
||||||
...oauthProviderTriggers,
|
...oauthProviderTriggers,
|
||||||
...oauthUserTriggers,
|
...oauthUserTriggers,
|
||||||
|
...oauthUserAuthTriggers,
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { Trigger } from 'oak-domain/lib/types';
|
||||||
|
import { BackendRuntimeContext } from '../context/BackendRuntimeContext';
|
||||||
|
import assert from 'assert';
|
||||||
|
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
|
||||||
|
import { EntityDict } from '../oak-app-domain';
|
||||||
|
|
||||||
|
const triggers = [
|
||||||
|
{
|
||||||
|
name: "在撤销用户OAuth授权前,执行操作",
|
||||||
|
action: "revoke",
|
||||||
|
when: "after",
|
||||||
|
entity: "oauthUserAuthorization",
|
||||||
|
fn: async ({ operation }, context) => {
|
||||||
|
const { filter } = operation;
|
||||||
|
assert(filter, 'No filter found in revoke operation');
|
||||||
|
|
||||||
|
let res = 0;
|
||||||
|
// 如果没有token,可以直接删除oauthUserAuthorization
|
||||||
|
const opRes = await context.operate("oauthUserAuthorization", {
|
||||||
|
id: await generateNewIdAsync(),
|
||||||
|
action: "remove",
|
||||||
|
data: {},
|
||||||
|
filter: {
|
||||||
|
...filter,
|
||||||
|
tokenId: {
|
||||||
|
$exists: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
res += opRes.oauthApplication?.remove || 0;
|
||||||
|
|
||||||
|
// 如果有token,则将token的revokedAt设置为当前时间
|
||||||
|
const opRes2 = await context.operate("oauthToken", {
|
||||||
|
id: await generateNewIdAsync(),
|
||||||
|
action: "update",
|
||||||
|
data: {
|
||||||
|
revokedAt: new Date()
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
oauthUserAuthorization$token: {
|
||||||
|
...filter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
res += opRes2.oauthToken?.update || 0;
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
] as Trigger<EntityDict, "oauthUserAuthorization", BackendRuntimeContext<EntityDict>>[];
|
||||||
|
|
||||||
|
export default triggers;
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
|
import { generateNewIdAsync } from "oak-domain/lib/utils/uuid";
|
||||||
|
import BackendRuntimeContext from "../../context/BackendRuntimeContext";
|
||||||
import { EntityDict } from "../../oak-app-domain"
|
import { EntityDict } from "../../oak-app-domain"
|
||||||
|
import { applicationProjection, extraFileProjection } from "../../types/Projection";
|
||||||
import { getDefaultHandlers } from "./handler";
|
import { getDefaultHandlers } from "./handler";
|
||||||
|
|
||||||
type UserInfo = EntityDict['oauthUser']['Schema']['rawUserInfo']
|
type UserInfo = EntityDict['oauthUser']['Schema']['rawUserInfo']
|
||||||
|
|
@ -33,4 +36,103 @@ const defaulthandlers = getDefaultHandlers();
|
||||||
|
|
||||||
Object.entries(defaulthandlers).forEach(([type, handler]) => {
|
Object.entries(defaulthandlers).forEach(([type, handler]) => {
|
||||||
registerUserinfoHandler(type as EntityDict['oauthProvider']['Schema']['type'], handler);
|
registerUserinfoHandler(type as EntityDict['oauthProvider']['Schema']['type'], handler);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function validateToken(token: string | undefined): {
|
||||||
|
token: string;
|
||||||
|
error: string | null;
|
||||||
|
} {
|
||||||
|
if (!token) {
|
||||||
|
return { token: "", error: "Missing authorization token" };
|
||||||
|
}
|
||||||
|
// Token validation logic here
|
||||||
|
if (!token.startsWith("Bearer ")) {
|
||||||
|
return { token: "", error: "Invalid token format" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { token: token.slice(7), error: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具函数
|
||||||
|
export async function checkOauthTokenAvaliable(
|
||||||
|
context: BackendRuntimeContext<EntityDict>,
|
||||||
|
token: string | undefined
|
||||||
|
): Promise<{
|
||||||
|
error: string | null;
|
||||||
|
statusCode?: number;
|
||||||
|
tokenRecord?:Partial<EntityDict['oauthToken']['Schema']>;
|
||||||
|
}> {
|
||||||
|
// Validate and decode the token
|
||||||
|
const decoded = validateToken(token);
|
||||||
|
if (decoded.error) {
|
||||||
|
return {
|
||||||
|
error: decoded.error,
|
||||||
|
statusCode: 401
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取token记录
|
||||||
|
const [tokenRecord] = await context.select("oauthToken", {
|
||||||
|
data: {
|
||||||
|
id: 1,
|
||||||
|
user: {
|
||||||
|
id: 1,
|
||||||
|
name: 1,
|
||||||
|
nickname: 1,
|
||||||
|
birth: 1,
|
||||||
|
gender: 1,
|
||||||
|
extraFile$entity: {
|
||||||
|
$entity: 'extraFile',
|
||||||
|
data: extraFileProjection,
|
||||||
|
filter: {
|
||||||
|
tag1: 'avatar',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
accessExpiresAt: 1,
|
||||||
|
revokedAt: 1,
|
||||||
|
code: {
|
||||||
|
id: 1,
|
||||||
|
application: applicationProjection,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
accessToken: decoded.token,
|
||||||
|
}
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
if (!tokenRecord) {
|
||||||
|
return { error: "Invalid token", statusCode: 401 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokenRecord.accessExpiresAt as number < Date.now()) {
|
||||||
|
return { error: "Token expired", statusCode: 401 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokenRecord.revokedAt) {
|
||||||
|
return { error: "Token revoked", statusCode: 401 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tokenRecord.user) {
|
||||||
|
return { error: "User not found", statusCode: 401 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tokenRecord.code || !tokenRecord.code.application) {
|
||||||
|
return { error: "Application not found", statusCode: 401 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新最后使用日期
|
||||||
|
await context.operate("oauthToken", {
|
||||||
|
id: await generateNewIdAsync(),
|
||||||
|
action: "update",
|
||||||
|
data: {
|
||||||
|
lastUsedAt: Date.now(),
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
id: tokenRecord.id,
|
||||||
|
}
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return { error: null, tokenRecord };
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue