diff --git a/README.md b/README.md index aa35682..b79b5dc 100644 --- a/README.md +++ b/README.md @@ -112,12 +112,12 @@ pnpm --filter web test - [x] tanstack react-query integration - [x] add members - [x] realtime unread count -- [ ] leave group, transfer ownership, delete group +- [x] leave group, transfer ownership, delete group +- [x] delete group - [ ] alert component - [ ] confirm dialog -- [ ] delete group -- [ ] e2e encryption - [ ] read receipts +- [ ] e2e encryption ### extras diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 234cd29..a423235 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -86,6 +86,12 @@ importers: socket.io: specifier: ^4.7.5 version: 4.7.5 + swagger-autogen: + specifier: ^2.23.7 + version: 2.23.7 + swagger-ui-express: + specifier: ^5.0.1 + version: 5.0.1(express@4.19.2) devDependencies: '@eslint/js': specifier: ^9.6.0 @@ -114,6 +120,9 @@ importers: '@types/pg': specifier: ^8.11.6 version: 8.11.6 + '@types/swagger-ui-express': + specifier: ^4.1.6 + version: 4.1.6 drizzle-kit: specifier: ^0.22.8 version: 0.22.8 @@ -1805,6 +1814,9 @@ packages: '@types/serve-static@1.15.7': resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} + '@types/swagger-ui-express@4.1.6': + resolution: {integrity: sha512-UVSiGYXa5IzdJJG3hrc86e8KdZWLYxyEsVoUI4iPXc7CO4VZ3AfNP8d/8+hrDRIqz+HAaSMtZSqAsF3Nq2X/Dg==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -1991,6 +2003,11 @@ packages: resolution: {integrity: sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==} engines: {node: '>=0.4.0'} + acorn@7.4.1: + resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} + engines: {node: '>=0.4.0'} + hasBin: true + acorn@8.12.0: resolution: {integrity: sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==} engines: {node: '>=0.4.0'} @@ -4255,6 +4272,18 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + swagger-autogen@2.23.7: + resolution: {integrity: sha512-vr7uRmuV0DCxWc0wokLJAwX3GwQFJ0jwN+AWk0hKxre2EZwusnkGSGdVFd82u7fQLgwSTnbWkxUL7HXuz5LTZQ==} + + swagger-ui-dist@5.17.14: + resolution: {integrity: sha512-CVbSfaLpstV65OnSjbXfVd6Sta3q3F7Cj/yYuvHMp1P90LztOLs6PfUnKEVAeiIVQt9u2SaPwv0LiH/OyMjHRw==} + + swagger-ui-express@5.0.1: + resolution: {integrity: sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==} + engines: {node: '>= v0.10.32'} + peerDependencies: + express: '>=4.0.0 || >=5.0.0-beta' + symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -6315,6 +6344,11 @@ snapshots: '@types/node': 20.14.5 '@types/send': 0.17.4 + '@types/swagger-ui-express@4.1.6': + dependencies: + '@types/express': 4.17.21 + '@types/serve-static': 1.15.7 + '@types/trusted-types@2.0.7': {} '@typescript-eslint/eslint-plugin@7.13.1(@typescript-eslint/parser@7.13.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)': @@ -6573,6 +6607,8 @@ snapshots: dependencies: acorn: 8.12.0 + acorn@7.4.1: {} + acorn@8.12.0: {} acorn@8.12.1: {} @@ -8140,7 +8176,7 @@ snapshots: mlly@1.7.1: dependencies: - acorn: 8.12.0 + acorn: 8.12.1 pathe: 1.1.2 pkg-types: 1.1.3 ufo: 1.5.3 @@ -8901,6 +8937,20 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + swagger-autogen@2.23.7: + dependencies: + acorn: 7.4.1 + deepmerge: 4.3.1 + glob: 7.2.3 + json5: 2.2.3 + + swagger-ui-dist@5.17.14: {} + + swagger-ui-express@5.0.1(express@4.19.2): + dependencies: + express: 4.19.2 + swagger-ui-dist: 5.17.14 + symbol-tree@3.2.4: {} tailwind-merge@2.3.0: diff --git a/server/package.json b/server/package.json index 251cda9..1ab0981 100644 --- a/server/package.json +++ b/server/package.json @@ -10,7 +10,8 @@ "lint": "tsc && eslint src/**/*.ts", "migrate:gen": "drizzle-kit generate", "migrate:run": "tsx src/scripts/migrate.ts", - "seed": "tsx src/scripts/seed.ts" + "seed": "tsx src/scripts/seed.ts", + "swag:gen": "tsx src/scripts/swagger.ts" }, "keywords": [ "socket.io", @@ -34,7 +35,9 @@ "lodash": "^4.17.21", "morgan": "^1.10.0", "pg": "^8.12.0", - "socket.io": "^4.7.5" + "socket.io": "^4.7.5", + "swagger-autogen": "^2.23.7", + "swagger-ui-express": "^5.0.1" }, "devDependencies": { "@eslint/js": "^9.6.0", @@ -46,6 +49,7 @@ "@types/morgan": "^1.9.9", "@types/node": "^20.14.5", "@types/pg": "^8.11.6", + "@types/swagger-ui-express": "^4.1.6", "drizzle-kit": "^0.22.8", "eslint": "9.x", "globals": "^15.7.0", diff --git a/server/src/index.ts b/server/src/index.ts index 6338afe..c0f0cc3 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -12,10 +12,11 @@ import cluster from 'node:cluster' import { createServer } from 'node:http' import { availableParallelism } from 'node:os' import { Server } from 'socket.io' +import swaggerUi from 'swagger-ui-express' import { config } from './config' import { connectDB } from './database' -import { auth, errorHandler } from './middlewares' -import * as routes from './routes' +import { errorHandler } from './middlewares' +import rootRouter from './routes' import { registerSocketEvents } from './socket/events' import { socketAuthMiddleware } from './socket/middlewares' import { @@ -24,6 +25,7 @@ import { ServerToClientEvents, SocketData, } from './socket/socket.interface' +import swaggerDocument from './swagger-output.json' const createApp = async () => { if (cluster.isPrimary && config.isProd) { @@ -99,9 +101,10 @@ const createApp = async () => { res.send('

Welcome to mChat API

') }) - app.use('/api/users', routes.userRoutes) - app.use('/api/groups', auth, routes.groupRoutes) - app.use('/api/members', auth, routes.memberRoutes) + app.use(rootRouter) + + app.use('/api-docs', swaggerUi.serve) + app.get('/api-docs', swaggerUi.setup(swaggerDocument)) app.use(errorHandler) diff --git a/server/src/routes.ts b/server/src/routes.ts index c14b5a8..a5a86da 100644 --- a/server/src/routes.ts +++ b/server/src/routes.ts @@ -1,3 +1,14 @@ -export { router as groupRoutes } from '@/modules/groups/groups.routes' -export { router as memberRoutes } from '@/modules/members/members.routes' -export { router as userRoutes } from '@/modules/users/users.routes' +import { Router } from 'express' +import { auth } from './middlewares' + +import { router as groupRoutes } from '@/modules/groups/groups.routes' +import { router as memberRoutes } from '@/modules/members/members.routes' +import { router as userRoutes } from '@/modules/users/users.routes' + +const rootRouter = Router() + +rootRouter.use('/api/users', userRoutes) +rootRouter.use('/api/groups', auth, groupRoutes) +rootRouter.use('/api/members', auth, memberRoutes) + +export default rootRouter diff --git a/server/src/scripts/swagger.ts b/server/src/scripts/swagger.ts new file mode 100644 index 0000000..24cf91f --- /dev/null +++ b/server/src/scripts/swagger.ts @@ -0,0 +1,27 @@ +import swaggerAutogen from 'swagger-autogen' + +const doc = { + info: { + version: '1.0.0', + title: 'mChat', + description: 'Realtime messenger powered by socket.io', + }, + host: 'localhost:3000', + components: { + securitySchemes: { + bearerAuth: { + type: 'http', + scheme: 'bearer', + }, + }, + }, + schemes: ['http', 'https'], +} + +const outputFile = '../swagger-output.json' +const routes = ['./../routes.ts'] + +/* NOTE: If you are using the express Router, you must pass in the 'routes' only the +root file where the route starts, such as index.js, app.js, routes.js, etc ... */ + +swaggerAutogen()(outputFile, routes, doc) diff --git a/server/src/swagger-output.json b/server/src/swagger-output.json new file mode 100644 index 0000000..8c09f6c --- /dev/null +++ b/server/src/swagger-output.json @@ -0,0 +1,618 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0.0", + "title": "mChat", + "description": "Realtime messenger powered by socket.io" + }, + "host": "localhost:3000", + "basePath": "/", + "schemes": [ + "http", + "https" + ], + "paths": { + "/api/users/": { + "post": { + "description": "", + "parameters": [ + { + "name": "body", + "in": "body", + "schema": { + "type": "object", + "properties": { + "username": { + "example": "any" + }, + "password": { + "example": "any" + }, + "fullName": { + "example": "any" + } + } + } + } + ], + "responses": { + "201": { + "description": "Created" + }, + "400": { + "description": "Bad Request" + } + } + }, + "get": { + "description": "", + "parameters": [ + { + "name": "authorization", + "in": "header", + "type": "string" + }, + { + "name": "limit", + "in": "query", + "type": "string" + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/users/login": { + "post": { + "description": "", + "parameters": [ + { + "name": "body", + "in": "body", + "schema": { + "type": "object", + "properties": { + "username": { + "example": "any" + }, + "password": { + "example": "any" + } + } + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + } + } + } + }, + "/api/users/{userId}/groups": { + "get": { + "description": "", + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "authorization", + "in": "header", + "type": "string" + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/groups/": { + "post": { + "description": "", + "parameters": [ + { + "name": "authorization", + "in": "header", + "type": "string" + }, + { + "name": "body", + "in": "body", + "schema": { + "type": "object", + "properties": { + "name": { + "example": "any" + }, + "memberIds": { + "example": "any" + } + } + } + } + ], + "responses": { + "201": { + "description": "Created" + } + } + }, + "get": { + "description": "", + "parameters": [ + { + "name": "authorization", + "in": "header", + "type": "string" + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/groups/{groupId}": { + "get": { + "description": "", + "parameters": [ + { + "name": "groupId", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "authorization", + "in": "header", + "type": "string" + }, + { + "name": "groupId", + "in": "query", + "type": "string" + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "description": "", + "parameters": [ + { + "name": "groupId", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "authorization", + "in": "header", + "type": "string" + }, + { + "name": "groupId", + "in": "query", + "type": "string" + }, + { + "name": "body", + "in": "body", + "schema": { + "type": "object", + "properties": { + "groupId": { + "example": "any" + } + } + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/groups/{groupId}/leave": { + "delete": { + "description": "", + "parameters": [ + { + "name": "groupId", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "authorization", + "in": "header", + "type": "string" + }, + { + "name": "groupId", + "in": "query", + "type": "string" + }, + { + "name": "body", + "in": "body", + "schema": { + "type": "object", + "properties": { + "groupId": { + "example": "any" + }, + "newOwnerId": { + "example": "any" + } + } + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/groups/{groupId}/members/{memberId}": { + "delete": { + "description": "", + "parameters": [ + { + "name": "groupId", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "memberId", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "authorization", + "in": "header", + "type": "string" + }, + { + "name": "groupId", + "in": "query", + "type": "string" + }, + { + "name": "body", + "in": "body", + "schema": { + "type": "object", + "properties": { + "groupId": { + "example": "any" + } + } + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/groups/{groupId}/members": { + "post": { + "description": "", + "parameters": [ + { + "name": "groupId", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "authorization", + "in": "header", + "type": "string" + }, + { + "name": "groupId", + "in": "query", + "type": "string" + }, + { + "name": "body", + "in": "body", + "schema": { + "type": "object", + "properties": { + "groupId": { + "example": "any" + }, + "memberIds": { + "example": "any" + } + } + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "get": { + "description": "", + "parameters": [ + { + "name": "groupId", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "authorization", + "in": "header", + "type": "string" + }, + { + "name": "groupId", + "in": "query", + "type": "string" + }, + { + "name": "query", + "in": "query", + "type": "string" + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/groups/{groupId}/members/{userId}": { + "get": { + "description": "", + "parameters": [ + { + "name": "groupId", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "userId", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "authorization", + "in": "header", + "type": "string" + }, + { + "name": "groupId", + "in": "query", + "type": "string" + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "patch": { + "description": "", + "parameters": [ + { + "name": "groupId", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "userId", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "authorization", + "in": "header", + "type": "string" + }, + { + "name": "groupId", + "in": "query", + "type": "string" + }, + { + "name": "body", + "in": "body", + "schema": { + "type": "object", + "properties": { + "groupId": { + "example": "any" + }, + "role": { + "example": "any" + } + } + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/groups/{groupId}/non-members": { + "get": { + "description": "", + "parameters": [ + { + "name": "groupId", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "authorization", + "in": "header", + "type": "string" + }, + { + "name": "groupId", + "in": "query", + "type": "string" + }, + { + "name": "limit", + "in": "query", + "type": "string" + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/groups/{groupId}/messages": { + "get": { + "description": "", + "parameters": [ + { + "name": "groupId", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "authorization", + "in": "header", + "type": "string" + }, + { + "name": "groupId", + "in": "query", + "type": "string" + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "post": { + "description": "", + "parameters": [ + { + "name": "groupId", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "authorization", + "in": "header", + "type": "string" + }, + { + "name": "groupId", + "in": "query", + "type": "string" + }, + { + "name": "body", + "in": "body", + "schema": { + "type": "object", + "properties": { + "groupId": { + "example": "any" + }, + "text": { + "example": "any" + } + } + } + } + ], + "responses": { + "201": { + "description": "Created" + } + } + } + }, + "/api/members/": { + "post": { + "description": "", + "parameters": [ + { + "name": "authorization", + "in": "header", + "type": "string" + }, + { + "name": "body", + "in": "body", + "schema": { + "type": "object", + "properties": { + "groupIds": { + "example": "any" + } + } + } + } + ], + "responses": { + "201": { + "description": "Created" + } + } + } + } + }, + "components": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer" + } + } + } +} \ No newline at end of file diff --git a/server/tsconfig.json b/server/tsconfig.json index 826589c..c5d3741 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -9,6 +9,7 @@ "forceConsistentCasingInFileNames": true, "typeRoots": ["global.d.ts"], "outDir": "dist", + "resolveJsonModule": true, "paths": { "@/*": ["./src/*"] }