mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-11 03:24:16 +00:00
added message_server
package
This commit is contained in:
parent
dd71c810d1
commit
a99592bad8
2
packages/message_server/.env-example
Executable file
2
packages/message_server/.env-example
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
MAIN_API_URL="https://api.example.com"
|
||||||
|
SERVER_TOKEN="ABCDEFGH12345678"
|
19
packages/message_server/Dockerfile
Executable file
19
packages/message_server/Dockerfile
Executable file
@ -0,0 +1,19 @@
|
|||||||
|
FROM node:16-bullseye
|
||||||
|
|
||||||
|
RUN apt update
|
||||||
|
RUN apt install python -y
|
||||||
|
RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app
|
||||||
|
|
||||||
|
WORKDIR /home/node/app
|
||||||
|
USER node
|
||||||
|
|
||||||
|
EXPOSE 3010
|
||||||
|
|
||||||
|
COPY package.json ./
|
||||||
|
COPY --chown=node:node . .
|
||||||
|
|
||||||
|
RUN chmod -R 777 /home/node/app
|
||||||
|
RUN npm install
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
CMD ["node", "/home/node/app/dist/index.js"]
|
30
packages/message_server/package.json
Executable file
30
packages/message_server/package.json
Executable file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "@comty/message_server",
|
||||||
|
"version": "0.35.1",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "corenode-cli build",
|
||||||
|
"dev": "nodemon --ignore dist/ --exec corenode-node ./src/index.js"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@foxify/events": "^2.1.0",
|
||||||
|
"axios": "^1.2.1",
|
||||||
|
"bcrypt": "5.0.1",
|
||||||
|
"corenode": "0.28.26",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.0.3",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"jsonwebtoken": "8.5.1",
|
||||||
|
"luxon": "^3.0.4",
|
||||||
|
"moment": "2.29.4",
|
||||||
|
"moment-timezone": "0.5.37",
|
||||||
|
"morgan": "^1.10.0",
|
||||||
|
"nanoid": "3.2.0",
|
||||||
|
"socket.io": "^4.5.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"nodemon": "^2.0.15"
|
||||||
|
}
|
||||||
|
}
|
229
packages/message_server/src/api.js
Executable file
229
packages/message_server/src/api.js
Executable file
@ -0,0 +1,229 @@
|
|||||||
|
import express from "express"
|
||||||
|
import http from "http"
|
||||||
|
import cors from "cors"
|
||||||
|
import morgan from "morgan"
|
||||||
|
import socketio from "socket.io"
|
||||||
|
import EventEmitter from "@foxify/events"
|
||||||
|
import jwt from "jsonwebtoken"
|
||||||
|
import axios from "axios"
|
||||||
|
|
||||||
|
import routes from "./routes"
|
||||||
|
|
||||||
|
const mainAPI = axios.create({
|
||||||
|
baseURL: process.env.MAIN_API_URL ?? "http://localhost:3000",
|
||||||
|
headers: {
|
||||||
|
"server-token": process.env.SERVER_TOKEN,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
class TextRoomServer {
|
||||||
|
constructor(server, options = {}) {
|
||||||
|
this.io = socketio(server, {
|
||||||
|
cors: {
|
||||||
|
origin: "*",
|
||||||
|
methods: ["GET", "POST"],
|
||||||
|
credentials: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.limitations = {
|
||||||
|
maxMessageLength: 540,
|
||||||
|
...options.limitations,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionPool = []
|
||||||
|
|
||||||
|
roomEventsHandlers = {
|
||||||
|
"send:message": (socket, payload) => {
|
||||||
|
const { connectedRoom } = socket
|
||||||
|
let { message } = payload
|
||||||
|
|
||||||
|
if (message.length > this.limitations.maxMessageLength) {
|
||||||
|
message = message.substring(0, this.limitations.maxMessageLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.io.to(connectedRoom).emit("message", {
|
||||||
|
timestamp: payload.timestamp ?? Date.now(),
|
||||||
|
content: String(message),
|
||||||
|
user: {
|
||||||
|
username: socket.userData.username,
|
||||||
|
fullName: socket.userData.fullName,
|
||||||
|
avatar: socket.userData.avatar,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeSocketIO = () => {
|
||||||
|
this.io.use(async (socket, next) => {
|
||||||
|
const token = socket.handshake.auth.token
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return next(new Error(`auth:token_missing`))
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await mainAPI.post("/validate_session", {
|
||||||
|
session: token
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
return res.data
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!session || !session?.valid) {
|
||||||
|
return next(new Error(`auth:token_invalid`))
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: userData } = await mainAPI.get("/user/public_data", {
|
||||||
|
params: {
|
||||||
|
username: session.username
|
||||||
|
}
|
||||||
|
}).catch((err) => {
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!userData) {
|
||||||
|
return next(new Error(`auth:user_failed`))
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// try to decode the token and get the user's username
|
||||||
|
const decodedToken = jwt.decode(token)
|
||||||
|
|
||||||
|
socket.userData = userData
|
||||||
|
socket.token = token
|
||||||
|
socket.decodedToken = decodedToken
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
return next(new Error(`auth:decode_failed`))
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[${socket.id}] connected`)
|
||||||
|
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.io.on("connection", (socket) => {
|
||||||
|
socket.on("join", (...args) => this.handleClientJoin(socket, ...args))
|
||||||
|
|
||||||
|
socket.on("disconnect", () => {
|
||||||
|
this.handleClientDisconnect(socket)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleClientJoin(socket, payload, cb) {
|
||||||
|
const { room } = payload
|
||||||
|
|
||||||
|
socket.connectedRoom = room
|
||||||
|
|
||||||
|
const pool = await this.attachClientToPool(socket, room).catch((err) => {
|
||||||
|
cb(err)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!pool) return
|
||||||
|
|
||||||
|
console.log(`[${socket.id}] joined room [${room}]`)
|
||||||
|
|
||||||
|
socket.join(room)
|
||||||
|
|
||||||
|
Object.keys(this.roomEventsHandlers).forEach((event) => {
|
||||||
|
socket.on(event, (...args) => this.roomEventsHandlers[event](socket, ...args))
|
||||||
|
})
|
||||||
|
|
||||||
|
const roomConnections = this.connectionPool.filter((client) => client.room === room).length
|
||||||
|
|
||||||
|
cb(null, {
|
||||||
|
roomConnections,
|
||||||
|
limitations: this.limitations,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClientDisconnect(socket) {
|
||||||
|
const index = this.connectionPool.findIndex((client) => client.id === socket.id)
|
||||||
|
|
||||||
|
if (index === -1) return
|
||||||
|
|
||||||
|
return this.connectionPool.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async attachClientToPool(socket, room) {
|
||||||
|
// TODO: check if user can join room or is privated
|
||||||
|
|
||||||
|
if (!room) {
|
||||||
|
throw new Error(`room:invalid`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.connectionPool.push({
|
||||||
|
id: socket.id,
|
||||||
|
room,
|
||||||
|
socket
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Server {
|
||||||
|
constructor(options = {}) {
|
||||||
|
this.app = express()
|
||||||
|
this.httpServer = http.createServer(this.app)
|
||||||
|
|
||||||
|
this.textRoomServer = new TextRoomServer(this.httpServer)
|
||||||
|
|
||||||
|
this.options = {
|
||||||
|
listenPort: process.env.PORT || 3020,
|
||||||
|
...options
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eventBus = global.eventBus = new EventEmitter()
|
||||||
|
|
||||||
|
initialize = async () => {
|
||||||
|
this.app.use(cors())
|
||||||
|
this.app.use(express.json({ extended: false }))
|
||||||
|
this.app.use(express.urlencoded({ extended: true }))
|
||||||
|
|
||||||
|
// Use logger if not in production
|
||||||
|
if (!process.env.NODE_ENV === "production") {
|
||||||
|
this.app.use(morgan("dev"))
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.textRoomServer.initializeSocketIO()
|
||||||
|
|
||||||
|
await this.registerBaseRoute()
|
||||||
|
await this.registerRoutes()
|
||||||
|
|
||||||
|
await this.httpServer.listen(this.options.listenPort)
|
||||||
|
|
||||||
|
return {
|
||||||
|
listenPort: this.options.listenPort,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async registerBaseRoute() {
|
||||||
|
await this.app.get("/", async (req, res) => {
|
||||||
|
return res.json({
|
||||||
|
uptimeMinutes: Math.floor(process.uptime() / 60),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
registerRoutes() {
|
||||||
|
routes.forEach((route) => {
|
||||||
|
const order = []
|
||||||
|
|
||||||
|
if (route.middlewares) {
|
||||||
|
route.middlewares.forEach((middleware) => {
|
||||||
|
order.push(middleware)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
order.push(route.routes)
|
||||||
|
|
||||||
|
this.app.use(route.use, ...order)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
49
packages/message_server/src/index.js
Executable file
49
packages/message_server/src/index.js
Executable file
@ -0,0 +1,49 @@
|
|||||||
|
require("dotenv").config()
|
||||||
|
|
||||||
|
// patches
|
||||||
|
const { Buffer } = require("buffer")
|
||||||
|
|
||||||
|
global.b64Decode = (data) => {
|
||||||
|
return Buffer.from(data, "base64").toString("utf-8")
|
||||||
|
}
|
||||||
|
global.b64Encode = (data) => {
|
||||||
|
return Buffer.from(data, "utf-8").toString("base64")
|
||||||
|
}
|
||||||
|
|
||||||
|
Array.prototype.updateFromObjectKeys = function (obj) {
|
||||||
|
this.forEach((value, index) => {
|
||||||
|
if (obj[value] !== undefined) {
|
||||||
|
this[index] = obj[value]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
global.toBoolean = (value) => {
|
||||||
|
if (typeof value === "boolean") {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return value.toLowerCase() === "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
import pkg from "../package.json"
|
||||||
|
import API from "./api"
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const api = new API()
|
||||||
|
|
||||||
|
console.log(`\n▶️ Initializing ${pkg.name} ...\n`)
|
||||||
|
const init = await api.initialize()
|
||||||
|
|
||||||
|
console.log(`\n🚀 ${pkg.name} v${pkg.version} is running on port ${init.listenPort}.\n`)
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error(`🆘 [FATAL ERROR] >`, error)
|
||||||
|
})
|
5
packages/message_server/src/middlewares/errorHandler/index.js
Executable file
5
packages/message_server/src/middlewares/errorHandler/index.js
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
export const errorHandler = (error, req, res, next) => {
|
||||||
|
res.json({ error: error.message })
|
||||||
|
}
|
||||||
|
|
||||||
|
export default errorHandler
|
30
packages/message_server/src/middlewares/hasPermissions/index.js
Executable file
30
packages/message_server/src/middlewares/hasPermissions/index.js
Executable file
@ -0,0 +1,30 @@
|
|||||||
|
export const hasPermissions = (req, res, next) => {
|
||||||
|
if (typeof (req.userData) == "undefined") {
|
||||||
|
return res.status(403).json(`User data is not available, please ensure if you are authenticated`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { _id, username, roles } = req.userData
|
||||||
|
const { permissions } = req.body
|
||||||
|
|
||||||
|
req.userPermissions = roles
|
||||||
|
|
||||||
|
let check = []
|
||||||
|
|
||||||
|
if (Array.isArray(permissions)) {
|
||||||
|
check = permissions
|
||||||
|
} else {
|
||||||
|
check.push(permissions)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (check.length > 0) {
|
||||||
|
check.forEach((role) => {
|
||||||
|
if (!roles.includes(role)) {
|
||||||
|
return res.status(403).json(`${username} not have permissions ${permissions}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
|
||||||
|
export default hasPermissions
|
12
packages/message_server/src/middlewares/index.js
Executable file
12
packages/message_server/src/middlewares/index.js
Executable file
@ -0,0 +1,12 @@
|
|||||||
|
// const fileUpload = require("@nanoexpress/middleware-file-upload/cjs")()
|
||||||
|
|
||||||
|
export { default as withAuthentication } from "./withAuthentication"
|
||||||
|
export { default as withOptionalAuthentication } from "./withOptionalAuthentication"
|
||||||
|
|
||||||
|
export { default as errorHandler } from "./errorHandler"
|
||||||
|
export { default as hasPermissions } from "./hasPermissions"
|
||||||
|
export { default as roles } from "./roles"
|
||||||
|
export { default as onlyAdmin } from "./onlyAdmin"
|
||||||
|
export { default as permissions } from "./permissions"
|
||||||
|
|
||||||
|
// export { fileUpload as fileUpload }
|
7
packages/message_server/src/middlewares/onlyAdmin/index.js
Executable file
7
packages/message_server/src/middlewares/onlyAdmin/index.js
Executable file
@ -0,0 +1,7 @@
|
|||||||
|
export default (req, res, next) => {
|
||||||
|
if (!req.user.roles.includes("admin")) {
|
||||||
|
return res.status(403).json({ error: "To make this request it is necessary to have administrator permissions" })
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
}
|
39
packages/message_server/src/middlewares/permissions/index.js
Executable file
39
packages/message_server/src/middlewares/permissions/index.js
Executable file
@ -0,0 +1,39 @@
|
|||||||
|
import { Config } from "../../models"
|
||||||
|
|
||||||
|
export default (req, res, next) => {
|
||||||
|
const requestedPath = `${req.method.toLowerCase()}${req.path.toLowerCase()}`
|
||||||
|
|
||||||
|
Config.findOne({ key: "permissions" }, undefined, {
|
||||||
|
lean: true,
|
||||||
|
}).then(({ value }) => {
|
||||||
|
req.assertedPermissions = []
|
||||||
|
|
||||||
|
const pathRoles = value.pathRoles ?? {}
|
||||||
|
|
||||||
|
if (typeof pathRoles[requestedPath] === "undefined") {
|
||||||
|
console.warn(`[Permissions] No permissions defined for path ${requestedPath}`)
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
const requiredRoles = Array.isArray(pathRoles[requestedPath]) ? pathRoles[requestedPath] : [pathRoles[requestedPath]]
|
||||||
|
|
||||||
|
requiredRoles.forEach((role) => {
|
||||||
|
if (req.user.roles.includes(role)) {
|
||||||
|
req.assertedPermissions.push(role)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (req.user.roles.includes("admin")) {
|
||||||
|
req.assertedPermissions.push("admin")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.assertedPermissions.length === 0 && !req.user.roles.includes("admin")) {
|
||||||
|
return res.status(403).json({
|
||||||
|
error: "forbidden",
|
||||||
|
message: "You don't have permission to access this resource",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
}
|
19
packages/message_server/src/middlewares/roles/index.js
Executable file
19
packages/message_server/src/middlewares/roles/index.js
Executable file
@ -0,0 +1,19 @@
|
|||||||
|
export default (req, res, next) => {
|
||||||
|
req.isAdmin = () => {
|
||||||
|
if (req.user.roles.includes("admin")) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
req.hasRole = (role) => {
|
||||||
|
if (req.user.roles.includes(role)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
}
|
83
packages/message_server/src/middlewares/withAuthentication/index.js
Executable file
83
packages/message_server/src/middlewares/withAuthentication/index.js
Executable file
@ -0,0 +1,83 @@
|
|||||||
|
import { Session, User } from "../../models"
|
||||||
|
import { Token } from "../../lib"
|
||||||
|
import jwt from "jsonwebtoken"
|
||||||
|
|
||||||
|
export default (req, res, next) => {
|
||||||
|
function reject(description) {
|
||||||
|
return res.status(403).json({ error: `${description ?? "Invalid session"}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
const authHeader = req.headers?.authorization?.split(" ")
|
||||||
|
|
||||||
|
if (authHeader && authHeader[0] === "Bearer") {
|
||||||
|
const token = authHeader[1]
|
||||||
|
let decoded = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
decoded = jwt.decode(token)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!decoded) {
|
||||||
|
return reject("Cannot decode token")
|
||||||
|
}
|
||||||
|
|
||||||
|
jwt.verify(token, global.jwtStrategy.secretOrKey, async (err) => {
|
||||||
|
const sessions = await Session.find({ user_id: decoded.user_id })
|
||||||
|
const currentSession = sessions.find((session) => session.token === token)
|
||||||
|
|
||||||
|
if (!currentSession) {
|
||||||
|
return reject("Cannot find session")
|
||||||
|
}
|
||||||
|
|
||||||
|
const userData = await User.findOne({ _id: currentSession.user_id }).select("+refreshToken")
|
||||||
|
|
||||||
|
if (!userData) {
|
||||||
|
return res.status(404).json({ error: "No user data found" })
|
||||||
|
}
|
||||||
|
|
||||||
|
// if cannot verify token, start regeneration process
|
||||||
|
if (err) {
|
||||||
|
// first check if token is only expired, if is corrupted, reject
|
||||||
|
if (err.name !== "TokenExpiredError") {
|
||||||
|
return reject("Invalid token, cannot regenerate")
|
||||||
|
}
|
||||||
|
|
||||||
|
let regenerationToken = null
|
||||||
|
|
||||||
|
// check if this expired token has a regeneration token associated
|
||||||
|
const associatedRegenerationToken = await Token.getRegenerationToken(token)
|
||||||
|
|
||||||
|
if (associatedRegenerationToken) {
|
||||||
|
regenerationToken = associatedRegenerationToken.refreshToken
|
||||||
|
} else {
|
||||||
|
// create a new regeneration token with the expired token
|
||||||
|
regenerationToken = await Token.createNewRegenerationToken(token).catch((error) => {
|
||||||
|
// in case of error, reject
|
||||||
|
reject(error.message)
|
||||||
|
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!regenerationToken) return
|
||||||
|
|
||||||
|
// now send the regeneration token to the client (start Expired Exception Event [EEE])
|
||||||
|
return res.status(401).json({
|
||||||
|
error: "Token expired",
|
||||||
|
refreshToken: regenerationToken.refreshToken,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
req.user = userData
|
||||||
|
req.jwtToken = token
|
||||||
|
req.decodedToken = decoded
|
||||||
|
req.currentSession = currentSession
|
||||||
|
|
||||||
|
return next()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return reject("Missing token header")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
import withAuthentication from "../withAuthentication"
|
||||||
|
|
||||||
|
export default (req, res, next) => {
|
||||||
|
if (req.headers?.authorization) {
|
||||||
|
withAuthentication(req, res, next)
|
||||||
|
} else {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
3
packages/message_server/src/routes/index.js
Normal file
3
packages/message_server/src/routes/index.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export default [
|
||||||
|
|
||||||
|
]
|
Loading…
x
Reference in New Issue
Block a user