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/*"]
}