diff --git a/.env.example b/.env.example index fccbcfb..792a37d 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,9 @@ # postgresql url DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public" -# Whether allow register user +# Whether allow feature ALLOW_REGISTER= +ALLOW_OPENAPI= # For analyze tianji self WEBSITE_ID= diff --git a/docker-compose.yml b/docker-compose.yml index 12b5291..d58c3e8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,7 @@ services: DATABASE_URL: postgresql://tianji:tianji@db:5432/tianji JWT_SECRET: replace-me-with-a-random-string ALLOW_REGISTER: "false" + ALLOW_OPENAPI: "true" depends_on: - postgres restart: always diff --git a/package.json b/package.json index 35e3444..a1a705a 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "build:server": "tsc -p tsconfig.server.json", "build:tracker": "ts-node scripts/build-tracker.ts", "build:geo": "ts-node scripts/build-geo.ts", + "postinstall": "pnpm db:generate", "check:type": "tsc --noEmit --skipLibCheck --module esnext", "db:push": "prisma db push", "db:generate": "prisma generate", @@ -69,6 +70,8 @@ "socket.io": "^4.7.2", "socket.io-client": "^4.7.2", "str2int": "^1.1.0", + "swagger-ui-express": "^5.0.0", + "trpc-openapi": "^1.2.0", "ts-node": "^10.9.1", "uuid": "^9.0.0", "vite-express": "^0.10.0", @@ -92,6 +95,7 @@ "@types/react": "^18.2.21", "@types/react-dom": "^18.2.7", "@types/request-ip": "^0.0.38", + "@types/swagger-ui-express": "^4.1.5", "@types/tar": "^6.1.5", "@vitejs/plugin-react": "^4.0.4", "autoprefixer": "^10.4.15", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c9c3a04..a43475c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -151,6 +151,12 @@ dependencies: str2int: specifier: ^1.1.0 version: 1.1.0 + swagger-ui-express: + specifier: ^5.0.0 + version: 5.0.0(express@4.18.2) + trpc-openapi: + specifier: ^1.2.0 + version: 1.2.0(@trpc/server@10.38.4)(zod@3.22.2) ts-node: specifier: ^10.9.1 version: 10.9.1(@types/node@18.17.12)(typescript@5.2.2) @@ -216,6 +222,9 @@ devDependencies: '@types/request-ip': specifier: ^0.0.38 version: 0.0.38 + '@types/swagger-ui-express': + specifier: ^4.1.5 + version: 4.1.5 '@types/tar': specifier: ^6.1.5 version: 6.1.5 @@ -2181,6 +2190,13 @@ packages: '@types/node': 18.17.12 dev: true + /@types/swagger-ui-express@4.1.5: + resolution: {integrity: sha512-MRvm1OCzIR321glc/4tP34wRVmsupgLzs6XIq50CFp0CJUzxbpDsrhJxEBMQfoO46ixrlCiw3QXxEs5HHxYI8Q==} + dependencies: + '@types/express': 4.17.17 + '@types/serve-static': 1.15.2 + dev: true + /@types/tar@6.1.5: resolution: {integrity: sha512-qm2I/RlZij5RofuY7vohTpYNaYcrSQlN2MyjucQc7ZweDwaEWkdN/EeNh6e9zjK6uEm6PwjdMXkcj05BxZdX1Q==} dependencies: @@ -2739,6 +2755,15 @@ packages: engines: {node: '>=6'} dev: false + /co-body@6.1.0: + resolution: {integrity: sha512-m7pOT6CdLN7FuXUcpuz/8lfQ/L77x8SchHCF4G0RBTJO20Wzmhn5Sp4/5WsKy8OSpifBSUrmg83qEqaDHdyFuQ==} + dependencies: + inflation: 2.1.0 + qs: 6.11.0 + raw-body: 2.5.1 + type-is: 1.6.18 + dev: false + /color-convert@0.5.3: resolution: {integrity: sha512-RwBeO/B/vZR3dfKL1ye/vx8MHZ40ugzpyfeVG5GsiuGnrlMWe2o8wxBbLCpw9CsxV+wHuzYlCiWnybrIA0ling==} dev: false @@ -2835,6 +2860,11 @@ packages: /concat-map@0.0.1: resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} + /consola@3.2.3: + resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==} + engines: {node: ^14.18.0 || >=16.10.0} + dev: false + /content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} @@ -2855,6 +2885,10 @@ packages: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} dev: true + /cookie-es@1.0.0: + resolution: {integrity: sha512-mWYvfOLrfEc996hlKcdABeIiPHUPC6DM2QYZdGGOvhOTbA3tjm2eBwqlJpoFdjC89NI4Qt6h0Pu06Mp+1Pj5OQ==} + dev: false + /cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} dev: false @@ -3162,6 +3196,10 @@ packages: resolution: {integrity: sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==} dev: false + /defu@6.1.2: + resolution: {integrity: sha512-+uO4+qr7msjNNWKYPHqN/3+Dx3NFkmIzayk2L1MyZQlvgZb/J1A0fo410dpKrN2SnqFjt8n4JL8fDJE0wIgjFQ==} + dev: false + /degenerator@5.0.1: resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} engines: {node: '>= 14'} @@ -3176,11 +3214,20 @@ packages: engines: {node: '>=0.4.0'} dev: false + /depd@1.1.2: + resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} + engines: {node: '>= 0.6'} + dev: false + /depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} dev: false + /destr@2.0.1: + resolution: {integrity: sha512-M1Ob1zPSIvlARiJUkKqvAZ3VAqQY6Jcuth/pBKQ2b1dX/Qx0OnJ8Vux6J2H5PTMQeRzWrrbTu70VxBfv/OPDJA==} + dev: false + /destroy@1.2.0: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -3843,6 +3890,19 @@ packages: resolution: {integrity: sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==} dev: false + /h3@1.8.2: + resolution: {integrity: sha512-1Ca0orJJlCaiFY68BvzQtP2lKLk46kcLAxVM8JgYbtm2cUg6IY7pjpYgWMwUvDO9QI30N5JAukOKoT8KD3Q0PQ==} + dependencies: + cookie-es: 1.0.0 + defu: 6.1.2 + destr: 2.0.1 + iron-webcrypto: 0.10.1 + radix3: 1.1.0 + ufo: 1.3.1 + uncrypto: 0.1.3 + unenv: 1.7.4 + dev: false + /hammerjs@2.0.8: resolution: {integrity: sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==} engines: {node: '>=0.8.0'} @@ -3954,6 +4014,11 @@ packages: resolve-from: 4.0.0 dev: false + /inflation@2.1.0: + resolution: {integrity: sha512-t54PPJHG1Pp7VQvxyVCJ9mBbjG3Hqryges9bXoOO6GExCPa+//i/d5GSuFtpx3ALLd7lgIAur6zrIlBQyJuMlQ==} + engines: {node: '>= 0.8.0'} + dev: false + /inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} dependencies: @@ -4004,6 +4069,10 @@ packages: engines: {node: '>= 0.10'} dev: false + /iron-webcrypto@0.10.1: + resolution: {integrity: sha512-QGOS8MRMnj/UiOa+aMIgfyHcvkhqNUsUxb1XzskENvbo+rEfp6TOwqd1KPuDzXC4OnGHcMSVxDGRoilqB8ViqA==} + dev: false + /is-any-array@2.0.1: resolution: {integrity: sha512-UtilS7hLRu++wb/WBAw9bNuP1Eg04Ivn1vERJck8zJthEvXCBEBpGR/33u/xLKWEQf95803oalHrVDptcAvFdQ==} dev: false @@ -4316,6 +4385,10 @@ packages: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} dev: false + /lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + dev: false + /lodash.includes@4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} dev: false @@ -4487,6 +4560,12 @@ packages: hasBin: true dev: false + /mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + dev: false + /minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: @@ -4643,6 +4722,10 @@ packages: engines: {node: '>= 0.4.0'} dev: false + /node-fetch-native@1.4.0: + resolution: {integrity: sha512-F5kfEj95kX8tkDhUCYdV8dg3/8Olx/94zB8+ZNthFs6Bz31UpUi8Xh40TN3thLwXgrwXry1pEg9lJ++tLWTcqA==} + dev: false + /node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -4655,6 +4738,22 @@ packages: whatwg-url: 5.0.0 dev: false + /node-mocks-http@1.13.0: + resolution: {integrity: sha512-lArD6sJMPJ53WF50GX0nJ89B1nkV1TdMvNwq8WXXFrUXF80ujSyye1T30mgiHh4h2It0/svpF3C4kZ2OAONVlg==} + engines: {node: '>=14'} + dependencies: + accepts: 1.3.8 + content-disposition: 0.5.4 + depd: 1.1.2 + fresh: 0.5.2 + merge-descriptors: 1.0.1 + methods: 1.1.2 + mime: 1.6.0 + parseurl: 1.3.3 + range-parser: 1.2.1 + type-is: 1.6.18 + dev: false + /node-releases@2.0.13: resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} dev: true @@ -4758,6 +4857,10 @@ packages: dependencies: wrappy: 1.0.2 + /openapi-types@12.1.3: + resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} + dev: false + /pac-proxy-agent@7.0.1: resolution: {integrity: sha512-ASV8yU4LLKBAjqIPMbrgtaKIvxQri/yh2OpI+S6hVa9JRkUI3Y3NPFbfngDtY7oFtSMD3w31Xns89mDa3Feo5A==} engines: {node: '>= 14'} @@ -4852,6 +4955,10 @@ packages: engines: {node: '>=8'} dev: false + /pathe@1.1.1: + resolution: {integrity: sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==} + dev: false + /pause@0.0.1: resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==} dev: false @@ -5137,6 +5244,10 @@ packages: resolution: {integrity: sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==} dev: false + /radix3@1.1.0: + resolution: {integrity: sha512-pNsHDxbGORSvuSScqNJ+3Km6QAVqk8CfsCBIEoDgpqLrkD2f3QM4I7d1ozJJ172OmIcoUcerZaNWqtLkRXTV3A==} + dev: false + /range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -6433,6 +6544,20 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + /swagger-ui-dist@5.9.0: + resolution: {integrity: sha512-NUHSYoe5XRTk/Are8jPJ6phzBh3l9l33nEyXosM17QInoV95/jng8+PuSGtbD407QoPf93MH3Bkh773OgesJpA==} + dev: false + + /swagger-ui-express@5.0.0(express@4.18.2): + resolution: {integrity: sha512-tsU9tODVvhyfkNSvf03E6FAk+z+5cU3lXAzMy6Pv4av2Gt2xA0++fogwC4qo19XuFf6hdxevPuVCSKFuMHJhFA==} + engines: {node: '>= v0.10.32'} + peerDependencies: + express: '>=4.0.0 || >=5.0.0-beta' + dependencies: + express: 4.18.2 + swagger-ui-dist: 5.9.0 + dev: false + /tailwindcss@3.3.3(ts-node@10.9.1): resolution: {integrity: sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==} engines: {node: '>=14.0.0'} @@ -6600,6 +6725,22 @@ packages: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} dev: false + /trpc-openapi@1.2.0(@trpc/server@10.38.4)(zod@3.22.2): + resolution: {integrity: sha512-pfYoCd/3KYXWXvUPZBKJw455OOwngKN/6SIcj7Yit19OMLJ+8yVZkEvGEeg5wUSwfsiTdRsKuvqkRPXVSwV7ew==} + peerDependencies: + '@trpc/server': ^10.0.0 + zod: ^3.14.4 + dependencies: + '@trpc/server': 10.38.4 + co-body: 6.1.0 + h3: 1.8.2 + lodash.clonedeep: 4.5.0 + node-mocks-http: 1.13.0 + openapi-types: 12.1.3 + zod: 3.22.2 + zod-to-json-schema: 3.21.4(zod@3.22.2) + dev: false + /ts-easing@0.2.0: resolution: {integrity: sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==} dev: false @@ -6710,6 +6851,10 @@ packages: resolution: {integrity: sha512-veRf7dawaj9xaWEu9HoTVn5Pggtc/qj+kqTOFvNiN1l0YdxwC1kvel57UCjThjGa3BHBihE8/UJAHI+uQHmd/g==} dev: false + /ufo@1.3.1: + resolution: {integrity: sha512-uY/99gMLIOlJPwATcMVYfqDSxUR9//AUcgZMzwfSTJPDKzA1S8mX4VLqa+fiAtveraQUBCz4FFcwVZBGbwBXIw==} + dev: false + /uglify-js@2.8.29: resolution: {integrity: sha512-qLq/4y2pjcU3vhlhseXGGJ7VbFO4pBANu0kwl8VCa9KEI0V8VfZIx2Fy3w01iSTA/pGwKZSmu/+I4etLNDdt5w==} engines: {node: '>=0.8.0'} @@ -6743,10 +6888,24 @@ packages: through: 2.3.8 dev: false + /uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + dev: false + /undefsafe@2.0.5: resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} dev: true + /unenv@1.7.4: + resolution: {integrity: sha512-fjYsXYi30It0YCQYqLOcT6fHfMXsBr2hw9XC7ycf8rTG7Xxpe3ZssiqUnD0khrjiZEmkBXWLwm42yCSCH46fMw==} + dependencies: + consola: 3.2.3 + defu: 6.1.2 + mime: 3.0.0 + node-fetch-native: 1.4.0 + pathe: 1.1.1 + dev: false + /universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} @@ -7049,6 +7208,14 @@ packages: type-fest: 2.19.0 dev: false + /zod-to-json-schema@3.21.4(zod@3.22.2): + resolution: {integrity: sha512-fjUZh4nQ1s6HMccgIeE0VP4QG/YRGPmyjO9sAh890aQKPEk3nqbfUXhMFaC+Dr5KvYBm8BCyvfpZf2jY9aGSsw==} + peerDependencies: + zod: ^3.21.4 + dependencies: + zod: 3.22.2 + dev: false + /zod@3.22.2: resolution: {integrity: sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg==} dev: false diff --git a/src/server/main.ts b/src/server/main.ts index 1b1dfa9..0445ce9 100644 --- a/src/server/main.ts +++ b/src/server/main.ts @@ -4,17 +4,23 @@ import express from 'express'; import 'express-async-errors'; import ViteExpress from 'vite-express'; import compression from 'compression'; +import swaggerUI from 'swagger-ui-express'; import passport from 'passport'; import morgan from 'morgan'; import { websiteRouter } from './router/website'; import { workspaceRouter } from './router/workspace'; import { telemetryRouter } from './router/telemetry'; -import { trpcExpressMiddleware } from './trpc'; +import { + trpcExpressMiddleware, + trpcOpenapiDocument, + trpcOpenapiHttpHandler, +} from './trpc'; import { initUdpServer } from './udp/server'; import { createServer } from 'http'; import { initSocketio } from './ws'; import { monitorManager } from './model/monitor'; import { settings } from './utils/settings'; +import { env } from './utils/env'; const port = settings.port; @@ -39,6 +45,10 @@ app.use('/api/website', websiteRouter); app.use('/api/workspace', workspaceRouter); app.use('/telemetry', telemetryRouter); +if (env.allowOpenapi) { + app.use('/open/_ui', swaggerUI.serve, swaggerUI.setup(trpcOpenapiDocument)); + app.use('/open', trpcOpenapiHttpHandler); +} app.use('/trpc', trpcExpressMiddleware); app.use((err: any, req: any, res: any, next: any) => { @@ -49,6 +59,9 @@ app.use((err: any, req: any, res: any, next: any) => { httpServer.listen(port, () => { ViteExpress.bind(app, httpServer, () => { console.log(`Server is listening on port ${port}...`); + if (env.allowOpenapi) { + console.log(`Openapi UI: http://127.0.0.1:${port}/open/_ui`); + } console.log(`Website: http://127.0.0.1:${port}`); }); }); diff --git a/src/server/model/_schema/index.ts b/src/server/model/_schema/index.ts new file mode 100644 index 0000000..9d32562 --- /dev/null +++ b/src/server/model/_schema/index.ts @@ -0,0 +1,23 @@ +import { z } from 'zod'; + +export const workspaceSchema = z.object({ + id: z.string(), + name: z.string(), + dashboardOrder: z.array(z.string()), +}); + +export const userInfoSchema = z.object({ + username: z.string(), + id: z.string(), + role: z.string(), + createdAt: z.date(), + updatedAt: z.date(), + deletedAt: z.date().nullable(), + currentWorkspace: workspaceSchema.nullable(), + workspaces: z.array( + z.object({ + role: z.string(), + workspace: workspaceSchema, + }) + ), +}); diff --git a/src/server/trpc/index.ts b/src/server/trpc/index.ts index 369f051..671fe98 100644 --- a/src/server/trpc/index.ts +++ b/src/server/trpc/index.ts @@ -1,6 +1,10 @@ import * as trpcExpress from '@trpc/server/adapters/express'; -import { createContext, router } from './trpc'; +import { createContext } from './trpc'; import { appRouter } from './routers'; +import { + createOpenApiHttpHandler, + generateOpenApiDocument, +} from 'trpc-openapi'; export const trpcExpressMiddleware = trpcExpress.createExpressMiddleware({ router: appRouter, @@ -9,3 +13,15 @@ export const trpcExpressMiddleware = trpcExpress.createExpressMiddleware({ console.error('Error:', path, error); }, }); + +export const trpcOpenapiHttpHandler = createOpenApiHttpHandler({ + router: appRouter, + createContext, +}); + +export const trpcOpenapiDocument = generateOpenApiDocument(appRouter, { + title: 'Tianji OpenAPI', + description: 'Insight into everything', + version: '1.0.0', + baseUrl: '/open', +}); diff --git a/src/server/trpc/routers/user.ts b/src/server/trpc/routers/user.ts index 42ace88..b777bd8 100644 --- a/src/server/trpc/routers/user.ts +++ b/src/server/trpc/routers/user.ts @@ -10,15 +10,27 @@ import { } from '../../model/user'; import { jwtSign } from '../../middleware/auth'; import { TRPCError } from '@trpc/server'; +import { env } from '../../utils/env'; +import { userInfoSchema } from '../../model/_schema/index'; +import { OPENAPI_TAG } from '../../utils/const'; export const userRouter = router({ login: publicProcedure + .meta({ + openapi: { method: 'POST', path: '/login', tags: [OPENAPI_TAG.USER] }, + }) .input( z.object({ username: z.string(), password: z.string(), }) ) + .output( + z.object({ + info: userInfoSchema, + token: z.string(), + }) + ) .mutation(async ({ input }) => { const { username, password } = input; const user = await authUser(username, password); @@ -28,11 +40,24 @@ export const userRouter = router({ return { info: user, token }; }), loginWithToken: publicProcedure + .meta({ + openapi: { + method: 'POST', + path: '/loginWithToken', + tags: [OPENAPI_TAG.USER], + }, + }) .input( z.object({ token: z.string(), }) ) + .output( + z.object({ + info: userInfoSchema, + token: z.string(), + }) + ) .mutation(async ({ input }) => { const { token } = input; @@ -51,13 +76,34 @@ export const userRouter = router({ } }), register: publicProcedure + .meta({ + openapi: { + enabled: env.allowRegister, + method: 'POST', + path: '/register', + tags: [OPENAPI_TAG.USER], + }, + }) .input( z.object({ username: z.string(), password: z.string(), }) ) + .output( + z.object({ + info: userInfoSchema, + token: z.string(), + }) + ) .mutation(async ({ input }) => { + if (!env.allowRegister) { + throw new TRPCError({ + code: 'FORBIDDEN', + message: 'Not allow register', + }); + } + const { username, password } = input; const userCount = await getUserCount(); diff --git a/src/server/trpc/trpc.ts b/src/server/trpc/trpc.ts index ccb9ed9..1371a77 100644 --- a/src/server/trpc/trpc.ts +++ b/src/server/trpc/trpc.ts @@ -5,6 +5,7 @@ import { jwtVerify } from '../middleware/auth'; import { getWorkspaceUser } from '../model/workspace'; import { ROLES, SYSTEM_ROLES } from '../utils/const'; import type { IncomingMessage } from 'http'; +import { OpenApiMeta } from 'trpc-openapi'; export function createContext({ req }: { req: IncomingMessage }) { const authorization = req.headers['authorization'] ?? ''; @@ -14,7 +15,7 @@ export function createContext({ req }: { req: IncomingMessage }) { } type Context = inferAsyncReturnType; -const t = initTRPC.context().create(); +const t = initTRPC.context().meta().create(); export const middleware = t.middleware; export const router = t.router; diff --git a/src/server/utils/const.ts b/src/server/utils/const.ts index b4ab365..f05fb01 100644 --- a/src/server/utils/const.ts +++ b/src/server/utils/const.ts @@ -113,3 +113,7 @@ export const FILTER_COLUMNS = { }; export const DEFAULT_RESET_DATE = '2000-01-01'; + +export enum OPENAPI_TAG { + USER = 'User', +} diff --git a/src/server/utils/env.ts b/src/server/utils/env.ts new file mode 100644 index 0000000..cf6f826 --- /dev/null +++ b/src/server/utils/env.ts @@ -0,0 +1,9 @@ +export const env = { + allowRegister: checkEnvTrusty(process.env.ALLOW_REGISTER), + allowOpenapi: checkEnvTrusty(process.env.ALLOW_OPENAPI), + websiteId: process.env.WEBSITE_ID, +}; + +export function checkEnvTrusty(env: string | undefined): boolean { + return env === '1' || env === 'true'; +}