diff --git a/package.json b/package.json index 869de8cc..614f9c97 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "react-router-dom@^6.0.0": "6.11.0", "@typegoose/typegoose": "9.3.1", "typescript": "4.9.4", + "got": "11.8.3", "mongodb": "4.2.1", "mongoose@~6.0.14": "6.1.1", "mongoose@^6.0.0": "6.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 312db686..c3fc97aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1877,6 +1877,34 @@ importers: specifier: ^4.3.6 version: 4.3.6(immer@9.0.21)(react@18.2.0) + server/plugins/com.msgbyte.fim: + dependencies: + got: + specifier: ^11.8.3 + version: 11.8.3 + tailchat-server-sdk: + specifier: '*' + version: link:../../packages/sdk + devDependencies: + '@types/react': + specifier: 18.0.20 + version: 18.0.20 + mini-star: + specifier: '*' + version: 1.3.1 + + server/plugins/com.msgbyte.fim/web/plugins/com.msgbyte.fim: + devDependencies: + '@types/styled-components': + specifier: ^5.1.26 + version: 5.1.26 + react: + specifier: 18.2.0 + version: 18.2.0 + styled-components: + specifier: ^5.3.6 + version: 5.3.6(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0) + server/plugins/com.msgbyte.getui: dependencies: got: diff --git a/server/plugins/com.msgbyte.fim/.ministarrc.js b/server/plugins/com.msgbyte.fim/.ministarrc.js new file mode 100644 index 00000000..3c4db179 --- /dev/null +++ b/server/plugins/com.msgbyte.fim/.ministarrc.js @@ -0,0 +1,14 @@ +const path = require('path'); + +module.exports = { + externalDeps: [ + 'react', + 'react-router', + 'axios', + 'styled-components', + 'zustand', + 'zustand/middleware/immer', + ], + pluginRoot: path.resolve(__dirname, './web'), + outDir: path.resolve(__dirname, '../../public'), +}; diff --git a/server/plugins/com.msgbyte.fim/models/fim.ts b/server/plugins/com.msgbyte.fim/models/fim.ts new file mode 100644 index 00000000..72de2d18 --- /dev/null +++ b/server/plugins/com.msgbyte.fim/models/fim.ts @@ -0,0 +1,20 @@ +import { db } from 'tailchat-server-sdk'; +const { getModelForClass, prop, modelOptions, TimeStamps } = db; + +@modelOptions({ + options: { + customName: 'p_fim', + }, +}) +export class Fim extends TimeStamps implements db.Base { + _id: db.Types.ObjectId; + id: string; +} + +export type FimDocument = db.DocumentType; + +const model = getModelForClass(Fim); + +export type FimModel = typeof model; + +export default model; diff --git a/server/plugins/com.msgbyte.fim/package.json b/server/plugins/com.msgbyte.fim/package.json new file mode 100644 index 00000000..d78da91b --- /dev/null +++ b/server/plugins/com.msgbyte.fim/package.json @@ -0,0 +1,21 @@ +{ + "name": "tailchat-plugin-fim", + "version": "1.0.0", + "main": "index.js", + "author": "moonrailgun", + "description": "Unified identity authentication", + "license": "MIT", + "private": true, + "scripts": { + "build:web": "ministar buildPlugin all", + "build:web:watch": "ministar watchPlugin all" + }, + "devDependencies": { + "@types/react": "18.0.20", + "mini-star": "*" + }, + "dependencies": { + "got": "^11.8.3", + "tailchat-server-sdk": "*" + } +} diff --git a/server/plugins/com.msgbyte.fim/services/fim.service.dev.ts b/server/plugins/com.msgbyte.fim/services/fim.service.dev.ts new file mode 100644 index 00000000..d4b38f76 --- /dev/null +++ b/server/plugins/com.msgbyte.fim/services/fim.service.dev.ts @@ -0,0 +1,50 @@ +import { TcService, TcDbService, TcPureContext } from 'tailchat-server-sdk'; +import type { FimDocument, FimModel } from '../models/fim'; +import { strategies } from '../strategies'; +import type { StrategyType } from '../strategies/types'; + +/** + * Federated Identity Management + * + * Unified identity authentication + */ +interface FimService extends TcService, TcDbService {} +class FimService extends TcService { + get serviceName() { + return 'plugin:com.msgbyte.fim'; + } + + onInit() { + // this.registerLocalDb(require('../models/fim').default); + + strategies + .filter((strategy) => strategy.checkAvailable()) + .map((strategy) => { + const action = this.buildStrategyAction(strategy); + const name = strategy.name; + this.registerAction(`${name}.url`, action.url); + this.registerAction(`${name}.redirect`, action.redirect); + + this.registerAuthWhitelist([`/${name}/url`, `/${name}/redirect`]); + }); + } + + buildStrategyAction(strategy: StrategyType) { + return { + url: async (ctx: TcPureContext) => { + return strategy.getUrl(); + }, + redirect: async (ctx: TcPureContext<{ code: string }>) => { + const code = ctx.params.code; + + if (!code) { + throw new Error(JSON.stringify(ctx.params)); + } + + return strategy.getUserInfo(code); + }, + }; + } +} + +export default FimService; diff --git a/server/plugins/com.msgbyte.fim/strategies/github.ts b/server/plugins/com.msgbyte.fim/strategies/github.ts new file mode 100644 index 00000000..f356b7e4 --- /dev/null +++ b/server/plugins/com.msgbyte.fim/strategies/github.ts @@ -0,0 +1,54 @@ +import { config } from 'tailchat-server-sdk'; +import type { StrategyType } from './types'; +import got from 'got'; + +const clientInfo = { + id: process.env.FIM_GITHUB_ID, + secret: process.env.FIM_GITHUB_SECRET, +}; + +const authorize_uri = 'https://github.com/login/oauth/authorize'; +const access_token_uri = 'https://github.com/login/oauth/access_token'; +const userinfo_uri = 'https://api.github.com/user'; +const redirect_uri = `${config.apiUrl}/api/plugin:com.msgbyte.fim/github/redirect`; + +export const GithubStrategy: StrategyType = { + name: 'github', + checkAvailable: () => !!clientInfo.id && !!clientInfo.secret, + getUrl: () => { + return `${authorize_uri}?client_id=${clientInfo.id}&redirect_uri=${redirect_uri}`; + }, + getUserInfo: async (code) => { + console.log('authorization code:', code); + + const tokenResponse = await got(access_token_uri, { + method: 'POST', + searchParams: { + client_id: clientInfo.id, + client_secret: clientInfo.secret, + code: code, + }, + headers: { + accept: 'application/json', + }, + }).json<{ access_token: string }>(); + + const accessToken = tokenResponse.access_token; + console.log(`access token: ${accessToken}`); + + const result = await got(userinfo_uri, { + method: 'GET', + headers: { + accept: 'application/json', + Authorization: `token ${accessToken}`, + }, + }).json<{ id: number; name: string; email: string; avatar_url: string }>(); + + return { + id: String(result.id), + nickname: result.name, + email: result.email, + avatar: result.avatar_url, + }; + }, +}; diff --git a/server/plugins/com.msgbyte.fim/strategies/index.ts b/server/plugins/com.msgbyte.fim/strategies/index.ts new file mode 100644 index 00000000..fd07cade --- /dev/null +++ b/server/plugins/com.msgbyte.fim/strategies/index.ts @@ -0,0 +1,3 @@ +import { GithubStrategy } from './github'; + +export const strategies = [GithubStrategy]; diff --git a/server/plugins/com.msgbyte.fim/strategies/types.ts b/server/plugins/com.msgbyte.fim/strategies/types.ts new file mode 100644 index 00000000..5078a19a --- /dev/null +++ b/server/plugins/com.msgbyte.fim/strategies/types.ts @@ -0,0 +1,11 @@ +export interface StrategyType { + name: string; + checkAvailable: () => boolean; + getUrl: () => string; + getUserInfo: (code: string) => Promise<{ + id: string; + nickname: string; + email: string; + avatar: string; + }>; +} diff --git a/server/plugins/com.msgbyte.fim/web/plugins/com.msgbyte.fim/manifest.json b/server/plugins/com.msgbyte.fim/web/plugins/com.msgbyte.fim/manifest.json new file mode 100644 index 00000000..e53f89b3 --- /dev/null +++ b/server/plugins/com.msgbyte.fim/web/plugins/com.msgbyte.fim/manifest.json @@ -0,0 +1,9 @@ +{ + "label": "Federated Identity Management", + "name": "com.msgbyte.fim", + "url": "{BACKEND}/plugins/com.msgbyte.fim/index.js", + "version": "0.0.0", + "author": "moonrailgun", + "description": "Unified identity authentication", + "requireRestart": true +} diff --git a/server/plugins/com.msgbyte.fim/web/plugins/com.msgbyte.fim/package.json b/server/plugins/com.msgbyte.fim/web/plugins/com.msgbyte.fim/package.json new file mode 100644 index 00000000..ab6c849b --- /dev/null +++ b/server/plugins/com.msgbyte.fim/web/plugins/com.msgbyte.fim/package.json @@ -0,0 +1,16 @@ +{ + "name": "@plugins/com.msgbyte.fim", + "main": "src/index.tsx", + "version": "0.0.0", + "description": "Unified identity authentication", + "private": true, + "scripts": { + "sync:declaration": "tailchat declaration github" + }, + "dependencies": {}, + "devDependencies": { + "@types/styled-components": "^5.1.26", + "react": "18.2.0", + "styled-components": "^5.3.6" + } +} diff --git a/server/plugins/com.msgbyte.fim/web/plugins/com.msgbyte.fim/src/index.tsx b/server/plugins/com.msgbyte.fim/web/plugins/com.msgbyte.fim/src/index.tsx new file mode 100644 index 00000000..a0b7c39b --- /dev/null +++ b/server/plugins/com.msgbyte.fim/web/plugins/com.msgbyte.fim/src/index.tsx @@ -0,0 +1 @@ +console.log('Plugin Federated Identity Management is loaded'); diff --git a/server/plugins/com.msgbyte.fim/web/plugins/com.msgbyte.fim/tsconfig.json b/server/plugins/com.msgbyte.fim/web/plugins/com.msgbyte.fim/tsconfig.json new file mode 100644 index 00000000..d9b47ed0 --- /dev/null +++ b/server/plugins/com.msgbyte.fim/web/plugins/com.msgbyte.fim/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "jsx": "react", + "importsNotUsedAsValues": "error" + } +} diff --git a/server/plugins/com.msgbyte.fim/web/plugins/com.msgbyte.fim/types/tailchat.d.ts b/server/plugins/com.msgbyte.fim/web/plugins/com.msgbyte.fim/types/tailchat.d.ts new file mode 100644 index 00000000..49f524ae --- /dev/null +++ b/server/plugins/com.msgbyte.fim/web/plugins/com.msgbyte.fim/types/tailchat.d.ts @@ -0,0 +1,2 @@ +declare module '@capital/common'; +declare module '@capital/component'; diff --git a/server/services/core/gateway.service.ts b/server/services/core/gateway.service.ts index 32c92991..de300d16 100644 --- a/server/services/core/gateway.service.ts +++ b/server/services/core/gateway.service.ts @@ -79,7 +79,7 @@ export default class ApiService extends TcService { // window: 60 * 1000, // // Max number of requests during window. Defaults to 30 - // limit: 30, + // limit: 60, // // Set rate limit headers to response. Defaults to false // headers: true,