diff --git a/es/endpoints/oauth.js b/es/endpoints/oauth.js index 8f77363d6..d1e93bed0 100644 --- a/es/endpoints/oauth.js +++ b/es/endpoints/oauth.js @@ -1,7 +1,8 @@ import { generateNewIdAsync } from "oak-domain/lib/utils/uuid"; import { randomUUID } from "crypto"; -import { applicationProjection, extraFileProjection } from "../types/Projection"; import { composeFileUrl } from "../utils/cos/index.backend"; +import assert from "assert"; +import { checkOauthTokenAvaliable } from "../utils/oauth"; const oauthTokenEndpoint = { name: "获取OAuth Token", params: [], @@ -93,11 +94,12 @@ const oauthTokenEndpoint = { const refreshToken = randomUUID(); const refreshTokenExpiresIn = 86400 * 30; // 30 days // create + const tokenId = await generateNewIdAsync(); await context.operate("oauthToken", { id: await generateNewIdAsync(), action: "create", data: { - id: await generateNewIdAsync(), + id: tokenId, accessToken: genaccessToken, refreshToken: refreshToken, userId: authCodeRecord.userId, @@ -106,6 +108,17 @@ const oauthTokenEndpoint = { codeId: authCodeRecord.id, } }, {}); + // 创建记录 + await context.operate("oauthUserAuthorization", { + id: await generateNewIdAsync(), + action: "update", + data: { + tokenId: tokenId, + }, + filter: { + codeId: authCodeRecord.id, + } + }, {}); // 标记code为已使用 await context.operate("oauthAuthorizationCode", { id: await generateNewIdAsync(), @@ -139,75 +152,23 @@ const oauthUserInfoEndpoint = { fn: async (contextBuilder, params, header, req, body) => { const context = await contextBuilder(); const token = header.authorization; // Bearer token - // Validate and decode the token - const decoded = validateToken(token); - if (decoded.error) { + const checkResult = await checkOauthTokenAvaliable(context, token); + if (checkResult.error) { + await context.commit(); return { - statusCode: 401, - data: { error: decoded.error, success: false } + statusCode: checkResult.statusCode || 401, + data: { error: checkResult.error, success: false } }; } - // 获取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, - 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 tokenRecord = checkResult.tokenRecord; + assert(tokenRecord?.user, "User must be present in token record"); + assert(tokenRecord?.code?.application, "Application must be present in token record"); const extrafile = tokenRecord.user.extraFile$entity?.[0]; const application = tokenRecord.code.application; let avatarUrl = ''; if (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(); return { 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 = { name: "刷新OAuth令牌", params: [], diff --git a/es/entities/OauthUserAuthorization.js b/es/entities/OauthUserAuthorization.js index d2b7827be..7d7737942 100644 --- a/es/entities/OauthUserAuthorization.js +++ b/es/entities/OauthUserAuthorization.js @@ -40,5 +40,20 @@ export const entityDesc = { revoked: '#6c757d', } } - } + }, + indexes: [ + // 根据授权码查询唯一记录 + { + name: 'idx_code_id', + attributes: [ + { + name: 'code', + direction: 'ASC', + } + ], + config: { + unique: true, + } + } + ] }; diff --git a/es/oak-app-domain/OauthUserAuthorization/Storage.js b/es/oak-app-domain/OauthUserAuthorization/Storage.js index 22711cf8e..88df04791 100644 --- a/es/oak-app-domain/OauthUserAuthorization/Storage.js +++ b/es/oak-app-domain/OauthUserAuthorization/Storage.js @@ -29,5 +29,20 @@ export const desc = { } }, actionType: "crud", - actions + actions, + indexes: [ + // 根据授权码查询唯一记录 + { + name: 'idx_code_id', + attributes: [ + { + name: "codeId", + direction: 'ASC', + } + ], + config: { + unique: true, + } + } + ] }; diff --git a/es/triggers/index.d.ts b/es/triggers/index.d.ts index dc75c09cd..d778a52a7 100644 --- a/es/triggers/index.d.ts +++ b/es/triggers/index.d.ts @@ -1,2 +1,2 @@ -declare const _default: (import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger>)[]; +declare const _default: (import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger>)[]; export default _default; diff --git a/es/triggers/index.js b/es/triggers/index.js index 34af12751..633fbc91c 100644 --- a/es/triggers/index.js +++ b/es/triggers/index.js @@ -19,6 +19,7 @@ import passportTriggers from './passport'; import oauthAppsTriggers from './oauthApps'; import oauthProviderTriggers from './oauthProvider'; import oauthUserTriggers from './oauthUser'; +import oauthUserAuthTriggers from './oauthUserAuth'; // import accountTriggers from './account'; export default [ // ...accountTriggers, @@ -43,4 +44,5 @@ export default [ ...oauthAppsTriggers, ...oauthProviderTriggers, ...oauthUserTriggers, + ...oauthUserAuthTriggers, ]; diff --git a/es/triggers/oauthUserAuth.d.ts b/es/triggers/oauthUserAuth.d.ts new file mode 100644 index 000000000..1002f1462 --- /dev/null +++ b/es/triggers/oauthUserAuth.d.ts @@ -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>[]; +export default triggers; diff --git a/es/triggers/oauthUserAuth.js b/es/triggers/oauthUserAuth.js new file mode 100644 index 000000000..f398b45b9 --- /dev/null +++ b/es/triggers/oauthUserAuth.js @@ -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; diff --git a/es/utils/oauth/index.d.ts b/es/utils/oauth/index.d.ts index 66df3177f..06495e00f 100644 --- a/es/utils/oauth/index.d.ts +++ b/es/utils/oauth/index.d.ts @@ -1,3 +1,4 @@ +import BackendRuntimeContext from "../../context/BackendRuntimeContext"; import { EntityDict } from "../../oak-app-domain"; type UserInfo = EntityDict['oauthUser']['Schema']['rawUserInfo']; type UserID = { @@ -11,4 +12,9 @@ type UserID = { export type UserInfoHandler = (data: UserInfo) => UserID | Promise; 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; +export declare function checkOauthTokenAvaliable(context: BackendRuntimeContext, token: string | undefined): Promise<{ + error: string | null; + statusCode?: number; + tokenRecord?: Partial; +}>; export {}; diff --git a/es/utils/oauth/index.js b/es/utils/oauth/index.js index 8d188eae8..d4506c9a5 100644 --- a/es/utils/oauth/index.js +++ b/es/utils/oauth/index.js @@ -1,3 +1,5 @@ +import { generateNewIdAsync } from "oak-domain/lib/utils/uuid"; +import { applicationProjection, extraFileProjection } from "../../types/Projection"; import { getDefaultHandlers } from "./handler"; const handlerMap = new Map(); export const registerUserinfoHandler = (type, handler) => { @@ -17,3 +19,80 @@ const defaulthandlers = getDefaultHandlers(); Object.entries(defaulthandlers).forEach(([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 }; +} diff --git a/lib/endpoints/oauth.js b/lib/endpoints/oauth.js index 0863d7c4c..43d024df7 100644 --- a/lib/endpoints/oauth.js +++ b/lib/endpoints/oauth.js @@ -1,9 +1,11 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +const tslib_1 = require("tslib"); const uuid_1 = require("oak-domain/lib/utils/uuid"); const crypto_1 = require("crypto"); -const Projection_1 = require("../types/Projection"); 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 = { name: "获取OAuth Token", params: [], @@ -95,11 +97,12 @@ const oauthTokenEndpoint = { const refreshToken = (0, crypto_1.randomUUID)(); const refreshTokenExpiresIn = 86400 * 30; // 30 days // create + const tokenId = await (0, uuid_1.generateNewIdAsync)(); await context.operate("oauthToken", { id: await (0, uuid_1.generateNewIdAsync)(), action: "create", data: { - id: await (0, uuid_1.generateNewIdAsync)(), + id: tokenId, accessToken: genaccessToken, refreshToken: refreshToken, userId: authCodeRecord.userId, @@ -108,6 +111,17 @@ const oauthTokenEndpoint = { codeId: authCodeRecord.id, } }, {}); + // 创建记录 + await context.operate("oauthUserAuthorization", { + id: await (0, uuid_1.generateNewIdAsync)(), + action: "update", + data: { + tokenId: tokenId, + }, + filter: { + codeId: authCodeRecord.id, + } + }, {}); // 标记code为已使用 await context.operate("oauthAuthorizationCode", { id: await (0, uuid_1.generateNewIdAsync)(), @@ -141,75 +155,23 @@ const oauthUserInfoEndpoint = { fn: async (contextBuilder, params, header, req, body) => { const context = await contextBuilder(); const token = header.authorization; // Bearer token - // Validate and decode the token - const decoded = validateToken(token); - if (decoded.error) { + const checkResult = await (0, oauth_1.checkOauthTokenAvaliable)(context, token); + if (checkResult.error) { + await context.commit(); return { - statusCode: 401, - data: { error: decoded.error, success: false } + statusCode: checkResult.statusCode || 401, + data: { error: checkResult.error, success: false } }; } - // 获取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, - 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 tokenRecord = checkResult.tokenRecord; + (0, assert_1.default)(tokenRecord?.user, "User must be present in token record"); + (0, assert_1.default)(tokenRecord?.code?.application, "Application must be present in token record"); const extrafile = tokenRecord.user.extraFile$entity?.[0]; const application = tokenRecord.code.application; let avatarUrl = ''; if (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(); return { 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 = { name: "刷新OAuth令牌", params: [], diff --git a/lib/entities/OauthUserAuthorization.js b/lib/entities/OauthUserAuthorization.js index 84304d959..0628a3ee3 100644 --- a/lib/entities/OauthUserAuthorization.js +++ b/lib/entities/OauthUserAuthorization.js @@ -43,5 +43,20 @@ exports.entityDesc = { revoked: '#6c757d', } } - } + }, + indexes: [ + // 根据授权码查询唯一记录 + { + name: 'idx_code_id', + attributes: [ + { + name: 'code', + direction: 'ASC', + } + ], + config: { + unique: true, + } + } + ] }; diff --git a/lib/oak-app-domain/OauthUserAuthorization/Storage.js b/lib/oak-app-domain/OauthUserAuthorization/Storage.js index bad3322ef..1761387c3 100644 --- a/lib/oak-app-domain/OauthUserAuthorization/Storage.js +++ b/lib/oak-app-domain/OauthUserAuthorization/Storage.js @@ -32,5 +32,20 @@ exports.desc = { } }, actionType: "crud", - actions: Action_1.actions + actions: Action_1.actions, + indexes: [ + // 根据授权码查询唯一记录 + { + name: 'idx_code_id', + attributes: [ + { + name: "codeId", + direction: 'ASC', + } + ], + config: { + unique: true, + } + } + ] }; diff --git a/lib/triggers/index.d.ts b/lib/triggers/index.d.ts index dc75c09cd..d778a52a7 100644 --- a/lib/triggers/index.d.ts +++ b/lib/triggers/index.d.ts @@ -1,2 +1,2 @@ -declare const _default: (import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger>)[]; +declare const _default: (import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger> | import("oak-domain/lib/types").Trigger>)[]; export default _default; diff --git a/lib/triggers/index.js b/lib/triggers/index.js index 0c5f5ea33..579f71c39 100644 --- a/lib/triggers/index.js +++ b/lib/triggers/index.js @@ -22,6 +22,7 @@ const passport_1 = tslib_1.__importDefault(require("./passport")); const oauthApps_1 = tslib_1.__importDefault(require("./oauthApps")); const oauthProvider_1 = tslib_1.__importDefault(require("./oauthProvider")); const oauthUser_1 = tslib_1.__importDefault(require("./oauthUser")); +const oauthUserAuth_1 = tslib_1.__importDefault(require("./oauthUserAuth")); // import accountTriggers from './account'; exports.default = [ // ...accountTriggers, @@ -46,4 +47,5 @@ exports.default = [ ...oauthApps_1.default, ...oauthProvider_1.default, ...oauthUser_1.default, + ...oauthUserAuth_1.default, ]; diff --git a/lib/triggers/oauthUserAuth.d.ts b/lib/triggers/oauthUserAuth.d.ts new file mode 100644 index 000000000..1002f1462 --- /dev/null +++ b/lib/triggers/oauthUserAuth.d.ts @@ -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>[]; +export default triggers; diff --git a/lib/triggers/oauthUserAuth.js b/lib/triggers/oauthUserAuth.js new file mode 100644 index 000000000..3fa720e59 --- /dev/null +++ b/lib/triggers/oauthUserAuth.js @@ -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; diff --git a/lib/utils/oauth/index.d.ts b/lib/utils/oauth/index.d.ts index 66df3177f..06495e00f 100644 --- a/lib/utils/oauth/index.d.ts +++ b/lib/utils/oauth/index.d.ts @@ -1,3 +1,4 @@ +import BackendRuntimeContext from "../../context/BackendRuntimeContext"; import { EntityDict } from "../../oak-app-domain"; type UserInfo = EntityDict['oauthUser']['Schema']['rawUserInfo']; type UserID = { @@ -11,4 +12,9 @@ type UserID = { export type UserInfoHandler = (data: UserInfo) => UserID | Promise; 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; +export declare function checkOauthTokenAvaliable(context: BackendRuntimeContext, token: string | undefined): Promise<{ + error: string | null; + statusCode?: number; + tokenRecord?: Partial; +}>; export {}; diff --git a/lib/utils/oauth/index.js b/lib/utils/oauth/index.js index c99fc07ef..dba5b369b 100644 --- a/lib/utils/oauth/index.js +++ b/lib/utils/oauth/index.js @@ -1,6 +1,9 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); 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 handlerMap = new Map(); const registerUserinfoHandler = (type, handler) => { @@ -22,3 +25,80 @@ const defaulthandlers = (0, handler_1.getDefaultHandlers)(); Object.entries(defaulthandlers).forEach(([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 }; +} diff --git a/src/endpoints/oauth.ts b/src/endpoints/oauth.ts index cf299b18a..f0c06e95d 100644 --- a/src/endpoints/oauth.ts +++ b/src/endpoints/oauth.ts @@ -6,6 +6,8 @@ import BackendRuntimeContext from "../context/BackendRuntimeContext"; import { EntityDict } from "../oak-app-domain"; import { applicationProjection, extraFileProjection } from "../types/Projection"; import { composeFileUrl } from "../utils/cos/index.backend"; +import assert from "assert"; +import { checkOauthTokenAvaliable } from "../utils/oauth"; const oauthTokenEndpoint: Endpoint> = { name: "获取OAuth Token", @@ -170,64 +172,19 @@ const oauthUserInfoEndpoint: Endpoint { const context = await contextBuilder() const token = header.authorization; // Bearer token - // Validate and decode the token - const decoded = validateToken(token); - if (decoded.error) { + + const checkResult = await checkOauthTokenAvaliable(context, token); + if (checkResult.error) { + await context.commit(); return { - statusCode: 401, - data: { error: decoded.error, success: false } + statusCode: checkResult.statusCode || 401, + data: { error: checkResult.error, success: false } } } - // 获取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, - 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 tokenRecord = checkResult.tokenRecord; + assert(tokenRecord?.user, "User must be present in token record"); + assert(tokenRecord?.code?.application, "Application must be present in token record"); const extrafile = tokenRecord.user.extraFile$entity?.[0]; const application = tokenRecord.code.application; let avatarUrl = ''; @@ -235,18 +192,6 @@ const oauthUserInfoEndpoint: Endpoint> = { name: "刷新OAuth令牌", params: [], diff --git a/src/triggers/index.ts b/src/triggers/index.ts index 60ec77139..ba0fd5721 100644 --- a/src/triggers/index.ts +++ b/src/triggers/index.ts @@ -19,6 +19,7 @@ import passportTriggers from './passport'; import oauthAppsTriggers from './oauthApps'; import oauthProviderTriggers from './oauthProvider'; import oauthUserTriggers from './oauthUser'; +import oauthUserAuthTriggers from './oauthUserAuth'; // import accountTriggers from './account'; @@ -46,4 +47,5 @@ export default [ ...oauthAppsTriggers, ...oauthProviderTriggers, ...oauthUserTriggers, + ...oauthUserAuthTriggers, ]; diff --git a/src/triggers/oauthUserAuth.ts b/src/triggers/oauthUserAuth.ts new file mode 100644 index 000000000..b263f4f96 --- /dev/null +++ b/src/triggers/oauthUserAuth.ts @@ -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>[]; + +export default triggers; diff --git a/src/utils/oauth/index.ts b/src/utils/oauth/index.ts index 8e30f6114..4d3eabd8c 100644 --- a/src/utils/oauth/index.ts +++ b/src/utils/oauth/index.ts @@ -1,4 +1,7 @@ +import { generateNewIdAsync } from "oak-domain/lib/utils/uuid"; +import BackendRuntimeContext from "../../context/BackendRuntimeContext"; import { EntityDict } from "../../oak-app-domain" +import { applicationProjection, extraFileProjection } from "../../types/Projection"; import { getDefaultHandlers } from "./handler"; type UserInfo = EntityDict['oauthUser']['Schema']['rawUserInfo'] @@ -33,4 +36,103 @@ const defaulthandlers = getDefaultHandlers(); Object.entries(defaulthandlers).forEach(([type, handler]) => { registerUserinfoHandler(type as EntityDict['oauthProvider']['Schema']['type'], handler); -}); \ No newline at end of file +}); + + +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, + token: string | undefined +): Promise<{ + error: string | null; + statusCode?: number; + tokenRecord?:Partial; +}> { + // 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 }; +} \ No newline at end of file