diff --git a/apps/oauth-demo/app.html b/apps/oauth-demo/app.html new file mode 100644 index 00000000..1e9490c7 --- /dev/null +++ b/apps/oauth-demo/app.html @@ -0,0 +1,11 @@ + + + + + + OAuth Demo + + + 登录 + + diff --git a/apps/oauth-demo/index.ts b/apps/oauth-demo/index.ts new file mode 100644 index 00000000..5dda6714 --- /dev/null +++ b/apps/oauth-demo/index.ts @@ -0,0 +1,63 @@ +import express from 'express'; +import path from 'path'; +import fs from 'fs-extra'; +import { OAuthClient } from 'tailchat-server-sdk'; +const app = express(); +const port = 8080; + +const API = process.env.API || 'http://localhost:11001'; +const clientUrl = `http://localhost:${port}`; +const clientId = process.env.ID; +const clientSecret = process.env.SECRET; + +if (!clientId || !clientSecret) { + throw new Error('环境变量缺失, 请设置环境变量 ID 和 SECRET'); +} + +console.log('config:', { + API, + clientUrl, + clientId, +}); + +const tailchatClient = new OAuthClient(API, clientId, clientSecret); + +app.get('/', async (req, res) => { + let html = ( + await fs.readFile(path.resolve(__dirname, './app.html')) + ).toString(); + html = html + .replace('', API) + .replace('', clientId) + .replace('', clientUrl); + + res.send(html); +}); + +app.get('/cb', async (req, res, next) => { + try { + const { code, state } = req.query; + + console.log('code', code); + + // 根据获取到的code获取授权码 + const { access_token } = await tailchatClient.getToken( + String(code), + `${clientUrl}/cb` + ); + + console.log('access_token', access_token); + + const { data: userInfo } = await tailchatClient.getUserInfo(access_token); + + res.json({ userInfo }); + } catch (err: any) { + console.error(err.response.data); + next(err); + } +}); + +app.listen(port, () => { + console.log(`请确保回调已经被注册在OIDC服务端的白名单中: ${clientUrl}/cb`); + console.log(`测试服务地址: http://127.0.0.1:${port}`); +}); diff --git a/apps/oauth-demo/package.json b/apps/oauth-demo/package.json new file mode 100644 index 00000000..1901831b --- /dev/null +++ b/apps/oauth-demo/package.json @@ -0,0 +1,24 @@ +{ + "name": "oauth-demo", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "ts-node ./index.ts", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "moonrailgun ", + "license": "MIT", + "dependencies": { + "express": "^4.18.2", + "fs-extra": "^11.1.0", + "tailchat-server-sdk": "workspace:^0.0.14" + }, + "devDependencies": { + "@types/express": "^4.17.15", + "@types/fs-extra": "^9.0.13", + "@types/node": "^18.13.0", + "ts-node": "^10.9.1", + "typescript": "^4.9.5" + } +} diff --git a/apps/oauth-demo/tsconfig.json b/apps/oauth-demo/tsconfig.json new file mode 100644 index 00000000..40134578 --- /dev/null +++ b/apps/oauth-demo/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "isolatedModules": true, + "strict": true, + "importsNotUsedAsValues": "error", + "experimentalDecorators": true, + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fce8d5f1..87c16d43 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -186,6 +186,27 @@ importers: ts-node: 10.9.1_4hee3ckhxcse3era5mxqjwg7u4 typescript: 4.9.5 + apps/oauth-demo: + specifiers: + '@types/express': ^4.17.15 + '@types/fs-extra': ^9.0.13 + '@types/node': ^18.13.0 + express: ^4.18.2 + fs-extra: ^11.1.0 + tailchat-server-sdk: workspace:^0.0.14 + ts-node: ^10.9.1 + typescript: ^4.9.5 + dependencies: + express: 4.18.2 + fs-extra: 11.1.0 + tailchat-server-sdk: link:../../server/packages/sdk + devDependencies: + '@types/express': 4.17.15 + '@types/fs-extra': 9.0.13 + '@types/node': 18.13.0 + ts-node: 10.9.1_4bewfcp2iebiwuold25d6rgcsy + typescript: 4.9.5 + apps/widget: specifiers: typescript: ^4.8.2 @@ -1093,6 +1114,7 @@ importers: '@fastify/busboy': ^1.1.0 '@typegoose/typegoose': 9.3.1 accept-language: ^3.0.18 + axios: ^1.3.3 body-parser: ^1.20.1 crc: ^3.8.0 dotenv: ^10.0.0 @@ -1117,6 +1139,7 @@ importers: '@fastify/busboy': 1.1.0 '@typegoose/typegoose': 9.3.1_mongoose@6.1.1 accept-language: 3.0.18 + axios: 1.3.3 body-parser: 1.20.1 crc: 3.8.0 dotenv: 10.0.0 @@ -12736,7 +12759,7 @@ packages: resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} dependencies: '@types/connect': 3.4.35 - '@types/node': 18.11.18 + '@types/node': 18.13.0 /@types/bonjour/3.5.10: resolution: {integrity: sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==} @@ -12784,7 +12807,7 @@ packages: /@types/connect/3.4.35: resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} dependencies: - '@types/node': 18.11.18 + '@types/node': 18.13.0 /@types/content-disposition/0.5.5: resolution: {integrity: sha512-v6LCdKfK6BwcqMo+wYW05rLS12S0ZO0Fl4w1h4aaZMD7bqT3gVUns6FvLJKGZHQmYn3SX55JWGpziwJRwVgutA==} @@ -12932,7 +12955,7 @@ packages: /@types/express-serve-static-core/4.17.31: resolution: {integrity: sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==} dependencies: - '@types/node': 18.11.18 + '@types/node': 18.13.0 '@types/qs': 6.9.7 '@types/range-parser': 1.2.4 @@ -12961,7 +12984,7 @@ packages: /@types/fs-extra/9.0.13: resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} dependencies: - '@types/node': 18.11.16 + '@types/node': 18.13.0 /@types/generate-json-webpack-plugin/0.3.4: resolution: {integrity: sha512-MubuE9xfB/NYIDGsmDs6O5nw6SDRmiUMAIWEfDHng1gQoI6w0/Zaa0CPTWIQ3OoiE9Z988q4R4Vsog4r6gXFvg==} @@ -13306,6 +13329,9 @@ packages: /@types/node/18.11.18: resolution: {integrity: sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==} + /@types/node/18.13.0: + resolution: {integrity: sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg==} + /@types/node/18.7.11: resolution: {integrity: sha512-KZhFpSLlmK/sdocfSAjqPETTMd0ug6HIMIAwkwUpU79olnZdQtMxpQP+G1wDzCH7na+FltSIhbaZuKdwZ8RDrw==} dev: true @@ -13610,7 +13636,7 @@ packages: resolution: {integrity: sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==} dependencies: '@types/mime': 3.0.1 - '@types/node': 18.11.18 + '@types/node': 18.13.0 /@types/sockjs/0.3.33: resolution: {integrity: sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==} @@ -15411,6 +15437,16 @@ packages: - debug dev: false + /axios/1.3.3: + resolution: {integrity: sha512-eYq77dYIFS77AQlhzEL937yUBSepBfPIe8FcgEDN35vMNZKMrs81pgnyrQpwfy4NF4b4XWX1Zgx7yX+25w8QJA==} + dependencies: + follow-redirects: 1.15.2 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + /b-tween/0.3.3: resolution: {integrity: sha512-oEHegcRpA7fAuc9KC4nktucuZn2aS8htymCPcP3qkEGPqiBH+GfqtqoG2l7LxHngg6O0HFM7hOeOYExl1Oz4ZA==} dev: false @@ -21638,7 +21674,6 @@ packages: graceful-fs: 4.2.10 jsonfile: 6.1.0 universalify: 2.0.0 - dev: true /fs-extra/8.1.0: resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} @@ -36905,6 +36940,37 @@ packages: tweetnacl: 1.0.3 dev: true + /ts-node/10.9.1_4bewfcp2iebiwuold25d6rgcsy: + resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.9 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.3 + '@types/node': 18.13.0 + acorn: 8.8.1 + acorn-walk: 8.2.0 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 4.9.5 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + dev: true + /ts-node/10.9.1_4hee3ckhxcse3era5mxqjwg7u4: resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true diff --git a/server/packages/sdk/package.json b/server/packages/sdk/package.json index 3f56751f..d4dc64bf 100644 --- a/server/packages/sdk/package.json +++ b/server/packages/sdk/package.json @@ -35,6 +35,7 @@ "@fastify/busboy": "^1.1.0", "@typegoose/typegoose": "9.3.1", "accept-language": "^3.0.18", + "axios": "^1.3.3", "body-parser": "^1.20.1", "crc": "^3.8.0", "dotenv": "^10.0.0", diff --git a/server/packages/sdk/src/index.ts b/server/packages/sdk/src/index.ts index e390f6aa..d212ed4d 100644 --- a/server/packages/sdk/src/index.ts +++ b/server/packages/sdk/src/index.ts @@ -46,6 +46,9 @@ export type { UserStruct, UserType } from './structs/user'; // db export * as db from './db'; +// openapi +export * from './openapi'; + export * from './const'; // other diff --git a/server/packages/sdk/src/openapi/index.ts b/server/packages/sdk/src/openapi/index.ts new file mode 100644 index 00000000..c22f08c8 --- /dev/null +++ b/server/packages/sdk/src/openapi/index.ts @@ -0,0 +1 @@ +export { OAuthClient } from './oauth'; diff --git a/server/packages/sdk/src/openapi/oauth.ts b/server/packages/sdk/src/openapi/oauth.ts new file mode 100644 index 00000000..d6c42106 --- /dev/null +++ b/server/packages/sdk/src/openapi/oauth.ts @@ -0,0 +1,66 @@ +import axios, { AxiosInstance } from 'axios'; + +/** + * 用于 Tailchat OAuth 信息集成的实例 + */ +export class OAuthClient { + request: AxiosInstance; + + constructor( + apiUrl: string, + private appId: string, + private appSecret: string + ) { + this.request = axios.create({ + baseURL: apiUrl, + transformRequest: [ + function (data) { + let ret = ''; + for (const it in data) { + ret += + encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&'; + } + ret = ret.substring(0, ret.lastIndexOf('&')); + return ret; + }, + ], + headers: { + 'content-type': 'application/x-www-form-urlencoded', + }, + }); + } + + /** + * 根据获取到的code获取授权码 + * @param code 从重定向获取到的临时code + * @param redirectUrl 重定向的地址 + */ + async getToken( + code: string, + redirectUrl: string + ): Promise<{ + access_token: string; + expires_in: string; + id_token: string; + scope: string; + token_type: string; + }> { + const { data: tokenInfo } = await this.request.post('/open/token', { + client_id: this.appId, + client_secret: this.appSecret, + redirect_uri: redirectUrl, + code, + grant_type: 'authorization_code', + }); + + return tokenInfo; + } + + async getUserInfo(accessToken: string): Promise { + const { data: userInfo } = await this.request.post('/open/me', { + access_token: accessToken, + }); + + return userInfo; + } +} diff --git a/server/packages/sdk/tsconfig.json b/server/packages/sdk/tsconfig.json index 4a85622d..eaa09601 100644 --- a/server/packages/sdk/tsconfig.json +++ b/server/packages/sdk/tsconfig.json @@ -2,6 +2,7 @@ "extends": "../../tsconfig.json", "compilerOptions": { "declaration": true, + "declarationMap": true, "rootDir": "./src", "outDir": "dist" },