├── .gitignore ├── README.md ├── constants └── user.constant.ts ├── image-main.png ├── image.png ├── package-lock.json ├── package.json ├── server.ts ├── src ├── app.ts ├── config │ ├── db.ts │ └── dotenv.config.ts ├── controllers │ ├── auth.controller.ts │ ├── messages.controller.ts │ ├── server.controller.ts │ ├── statistics.controller.ts │ └── users.controller.ts ├── middleware │ ├── auth.ts │ └── errorHandler.ts ├── models │ ├── invitation.model.ts │ ├── messages.model.ts │ ├── server.model.ts │ ├── statistics.model.ts │ └── user.model.ts ├── routes │ ├── auth.routes.ts │ ├── messages.route.ts │ └── server.routes.ts ├── sockets │ └── index.socket.ts ├── types │ ├── error.class.ts │ ├── message.interface.ts │ ├── server-socket.interface.ts │ └── server.interface.ts └── utils │ ├── constants.ts │ ├── index.util.ts │ ├── messages.utli.ts │ ├── server.util.ts │ └── user.util.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | .env.* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Project Snapshot](https://github.com/fabiconcept/secret-room-backend/blob/main/image.png) 2 | # 🕵️‍♂️ Secret Room - Backend 3 | 4 |
5 |
6 | Last Commit 7 | TypeScript Usage 8 | Languages 9 |
10 | 11 |

Built with these tools and technologies:

12 | 13 |
14 | Express 15 | JSON 16 | Socket.io 17 | npm 18 | Mongoose 19 |
20 | 21 |
22 | .ENV 23 | Nodemon 24 | TypeScript 25 | ts-node 26 | Socket 27 |
28 |
29 | 30 | **Secret Room** is a fully anonymous, real-time chat web app that lets users create temporary servers and invite others to chat without ever revealing their identity. This is the backend service powering the chat experience, built with **Node.js**, **Express**, **MongoDB**, and **Socket.io**. 31 | 32 | > 💡 This is my first personal Node.js backend project. It focuses heavily on secure communication, temporary server lifecycles, and anonymous user management. 33 | 34 | --- 35 | 36 | ## 🚀 Features 37 | 38 | - 🔒 JWT Authentication for secure access 39 | - 🧠 Anonymous user identification via fingerprinting 40 | - 💬 Real-time chat between host and guests (powered by Socket.io) 41 | - ⚡ Instant join/leave/typing notifications 42 | - ⏳ Self-destructing servers based on a lifespan 43 | - 📨 Global & Unique server invitation system 44 | - 👻 Deterministic anonymous usernames per server 45 | - 📦 Clean modular structure: Models, Routes, Controllers, Middleware 46 | 47 | --- 48 | 49 | ## 📁 Project Structure 50 | 51 | ``` 52 | . 53 | ├── controllers/ 54 | │ ├── userController.js 55 | │ ├── serverController.js 56 | │ └── messagesController.js 57 | ├── middleware/ 58 | │ └── authenticateJWT.js 59 | ├── models/ 60 | │ ├── User.js 61 | │ ├── Server.js 62 | │ ├── Message.js 63 | │ └── Invitation.js 64 | ├── socket/ 65 | │ └── socketHandler.js 66 | ├── routes/ 67 | │ └── ... 68 | ``` 69 | 70 | --- 71 | 72 | ## 📦 Tech Stack 73 | 74 | - **Node.js** 75 | - **Express** 76 | - **MongoDB** (Mongoose) 77 | - **Socket.io** for real-time communication 78 | - **JWT** for authentication 79 | - **UUID + Custom Encryption** for anonymous ID and secure communication 80 | 81 | --- 82 | 83 | ## 🔐 Authentication 84 | 85 | - Uses JWT tokens to secure private routes 86 | - Fingerprinting used to generate a unique ID for users (without collecting sensitive info) 87 | 88 | --- 89 | 90 | ## 💬 Real-time Communication (Socket.io) 91 | 92 | - Users join a room corresponding to the serverId 93 | - On joining, a deterministic **anonymous username** is generated using a `serverId-userId` combo (see below) 94 | - Events include: 95 | - `user:join` – Notify server members of a new user 96 | - `user:leave` – Broadcast when a user disconnects 97 | - `user:active` - Broadcast when the user is active 98 | - `server:deleted` - Broadcast when the server is deleted 99 | - `message:send` – Relay messages between users 100 | - `message:read` – Relay messages between users 101 | - Socket connections are JWT-authenticated and scoped to server rooms 102 | 103 | --- 104 | 105 | ## 👻 Username Generation (Anonymity) 106 | 107 | Usernames are deterministically generated per server using a salted pattern based on the combination: `serverId-userId`. 108 | 109 | ```ts 110 | // utils/generateUsername.ts 111 | export function generateUsername(salt: string): string { 112 | // uses custom hash + either consonant-vowel or alphanumeric pattern 113 | } 114 | ``` 115 | 116 | ### Why this matters: 117 | - Ensures each user has a **unique, anonymous identity** _per server_ 118 | - Zero chance of real user data leaks 119 | - No usernames are ever reused across different servers 120 | 121 | --- 122 | 123 | ## 🧪 API Endpoints 124 | 125 | ### 🔐 Authentication Required 126 | 127 | | Method | Endpoint | 128 | |--------|----------| 129 | | `POST` | `/` – Create a new server | 130 | | `POST` | `/invitation/:globalInvitationId` – Join with a global invite | 131 | | `POST` | `/unique-invitation/:inviteCode` – Join with a unique invite | 132 | | `GET` | `/:serverId` – Get server data | 133 | | `GET` | `/:serverId/active-users` – Get current active users | 134 | | `GET` | `/:serverId/generate-unique-server-invitation-id` – Generate a unique invite | 135 | | `GET` | `/:serverId/messages` – Fetch messages in the server | 136 | | `DELETE` | `/:serverId` – Delete a server | 137 | 138 | --- 139 | 140 | ## 🧬 Mongoose Models 141 | 142 | ### 🧑‍💻 User 143 | ```ts 144 | userId, isOnline, lastSeen, currentServer, createdAt, bgColor, textColor 145 | ``` 146 | 147 | ### 💬 Message 148 | ```ts 149 | serverId, senderId, receiverId, content, attachmentUrl, sent, readBySender, readByReceiver 150 | ``` 151 | 152 | ### 🏠 Server 153 | ```ts 154 | serverId, owner, serverName, salt, globalInvitationId, expiresAt, approvedUsers, allUsers 155 | ``` 156 | 157 | ### 🎟️ Invitation 158 | ```ts 159 | inviteCode, used, serverId, expiresAt 160 | ``` 161 | 162 | --- 163 | 164 | ## ⚙️ Setup Instructions 165 | 166 | ```bash 167 | # 1. Clone the repo 168 | git clone [https://github.com/your-username/secret-room-backend](https://github.com/fabiconcept/secret-room-backend.git) 169 | 170 | # 2. Navigate into the folder 171 | cd secret-room-backend 172 | 173 | # 3. Install dependencies 174 | npm install 175 | 176 | # 4. Create a `.env` file and add: 177 | # PORT 178 | # HOST 179 | # CORS_ORIGIN 180 | # JWT_SECRET 181 | # MONGODB_URI 182 | # API_KEY 183 | 184 | # 5. Run the server 185 | npm run dev 186 | ``` 187 | 188 | 189 | --- 190 | 191 | ## 🙌 Author 192 | 193 | Made with ❤️ by [Favour Tochukwu Ajokubi](https://github.com/FavourBE) 194 | 195 | --- 196 | 197 | ## 🛡️ License 198 | 199 | This project is open-source and available under the [MIT License](LICENSE). 200 | -------------------------------------------------------------------------------- /constants/user.constant.ts: -------------------------------------------------------------------------------- 1 | export const adjectives = [ 2 | // Original adjectives 3 | 'cool', 'dark', 'epic', 'fast', 'gold', 'holy', 'iron', 'jade', 'kind', 'lost', 4 | 'mega', 'neon', 'okay', 'pure', 'quit', 'rare', 'safe', 'true', 'uber', 'void', 5 | 6 | // Natural elements 7 | 'azure', 'blaze', 'coral', 'ember', 'frost', 'glade', 'haze', 'icy', 'lunar', 'misty', 8 | 'north', 'ocean', 'polar', 'quake', 'river', 'storm', 'tidal', 'urban', 'vapor', 'wind', 9 | 10 | // Colors and visual properties 11 | 'amber', 'black', 'chrome', 'denim', 'ebony', 'faded', 'green', 'honey', 'ivory', 'jet', 12 | 'khaki', 'lilac', 'mauve', 'navy', 'opal', 'pink', 'quartz', 'ruby', 'silver', 'teal', 13 | 14 | // Qualities and characteristics 15 | 'able', 'bold', 'calm', 'deft', 'eager', 'fierce', 'grand', 'hardy', 'ideal', 'jolly', 16 | 'keen', 'lively', 'mighty', 'noble', 'odd', 'prime', 'quick', 'royal', 'sharp', 'tough', 17 | 18 | // Fantasy and sci-fi themed 19 | 'astral', 'brave', 'cyber', 'dire', 'ether', 'feral', 'gamma', 'hyper', 'intel', 'jedi', 20 | 'krewe', 'laser', 'mecha', 'nexus', 'omega', 'phase', 'quantum', 'rune', 'solar', 'twin', 21 | 22 | // Emotional states 23 | 'angry', 'busy', 'crazy', 'dizzy', 'eager', 'funky', 'giddy', 'happy', 'irate', 'jolly', 24 | 'kooky', 'loopy', 'moody', 'nervy', 'peppy', 'quirky', 'rowdy', 'silly', 'testy', 'wild' 25 | ]; 26 | 27 | export const nouns = [ 28 | // Original nouns 29 | 'ace', 'bear', 'cat', 'dove', 'elf', 'fox', 'goat', 'hawk', 'ibex', 'jay', 30 | 'king', 'lion', 'mage', 'nuke', 'owl', 'puma', 'rook', 'sage', 'tide', 'wolf', 31 | 32 | // More animals 33 | 'ape', 'bison', 'crow', 'drake', 'eagle', 'fawn', 'gecko', 'hound', 'impala', 'jaguar', 34 | 'koala', 'lynx', 'moose', 'newt', 'orca', 'panda', 'quail', 'rhino', 'shark', 'tiger', 35 | 36 | // Fantasy creatures 37 | 'angel', 'beast', 'cipher', 'demon', 'eidolon', 'fairy', 'giant', 'hobbit', 'imp', 'joker', 38 | 'knight', 'lucifer', 'mimic', 'ninja', 'oracle', 'pirate', 'queen', 'rebel', 'specter', 'titan', 39 | 40 | // Objects and items 41 | 'arrow', 'blade', 'charm', 'dagger', 'ember', 'flare', 'glyph', 'hammer', 'icon', 'jewel', 42 | 'katana', 'locket', 'mallet', 'needle', 'orb', 'prism', 'quiver', 'relic', 'shield', 'torch', 43 | 44 | // Natural elements and phenomena 45 | 'ash', 'breeze', 'comet', 'dune', 'echo', 'flame', 'gust', 'hail', 'ice', 'jungle', 46 | 'kite', 'leaf', 'mist', 'nova', 'oasis', 'peak', 'quake', 'reef', 'stone', 'tempest', 47 | 48 | // Technology and modern terms 49 | 'atom', 'byte', 'code', 'data', 'echo', 'flux', 'grid', 'hack', 'intel', 'joule', 50 | 'key', 'link', 'mech', 'node', 'optic', 'pulse', 'query', 'router', 'server', 'tech' 51 | ]; -------------------------------------------------------------------------------- /image-main.png: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabiconcept/secret-room-backend/b781f85e513b4da479d98fbd90697ddc36ffc82e/image.png -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "server", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@types/cors": "^2.8.17", 13 | "@types/express": "^5.0.0", 14 | "@types/jsonwebtoken": "^9.0.9", 15 | "@types/node": "^22.13.9", 16 | "cors": "^2.8.5", 17 | "dotenv": "^16.4.7", 18 | "express": "^4.21.2", 19 | "jsonwebtoken": "^9.0.2", 20 | "mongoose": "^8.12.1", 21 | "node-cron": "^3.0.3", 22 | "nodemon": "^3.1.9", 23 | "socket.io": "^4.8.1", 24 | "typescript": "^5.8.2", 25 | "uuid": "^11.1.0" 26 | }, 27 | "devDependencies": { 28 | "@types/node-cron": "^3.0.11", 29 | "ts-node": "^10.9.2" 30 | } 31 | }, 32 | "node_modules/@cspotcode/source-map-support": { 33 | "version": "0.8.1", 34 | "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", 35 | "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", 36 | "dev": true, 37 | "license": "MIT", 38 | "dependencies": { 39 | "@jridgewell/trace-mapping": "0.3.9" 40 | }, 41 | "engines": { 42 | "node": ">=12" 43 | } 44 | }, 45 | "node_modules/@jridgewell/resolve-uri": { 46 | "version": "3.1.2", 47 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", 48 | "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", 49 | "dev": true, 50 | "license": "MIT", 51 | "engines": { 52 | "node": ">=6.0.0" 53 | } 54 | }, 55 | "node_modules/@jridgewell/sourcemap-codec": { 56 | "version": "1.5.0", 57 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", 58 | "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", 59 | "dev": true, 60 | "license": "MIT" 61 | }, 62 | "node_modules/@jridgewell/trace-mapping": { 63 | "version": "0.3.9", 64 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", 65 | "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", 66 | "dev": true, 67 | "license": "MIT", 68 | "dependencies": { 69 | "@jridgewell/resolve-uri": "^3.0.3", 70 | "@jridgewell/sourcemap-codec": "^1.4.10" 71 | } 72 | }, 73 | "node_modules/@mongodb-js/saslprep": { 74 | "version": "1.2.0", 75 | "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.0.tgz", 76 | "integrity": "sha512-+ywrb0AqkfaYuhHs6LxKWgqbh3I72EpEgESCw37o+9qPx9WTCkgDm2B+eMrwehGtHBWHFU4GXvnSCNiFhhausg==", 77 | "license": "MIT", 78 | "dependencies": { 79 | "sparse-bitfield": "^3.0.3" 80 | } 81 | }, 82 | "node_modules/@socket.io/component-emitter": { 83 | "version": "3.1.2", 84 | "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", 85 | "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", 86 | "license": "MIT" 87 | }, 88 | "node_modules/@tsconfig/node10": { 89 | "version": "1.0.11", 90 | "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", 91 | "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", 92 | "dev": true, 93 | "license": "MIT" 94 | }, 95 | "node_modules/@tsconfig/node12": { 96 | "version": "1.0.11", 97 | "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", 98 | "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", 99 | "dev": true, 100 | "license": "MIT" 101 | }, 102 | "node_modules/@tsconfig/node14": { 103 | "version": "1.0.3", 104 | "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", 105 | "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", 106 | "dev": true, 107 | "license": "MIT" 108 | }, 109 | "node_modules/@tsconfig/node16": { 110 | "version": "1.0.4", 111 | "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", 112 | "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", 113 | "dev": true, 114 | "license": "MIT" 115 | }, 116 | "node_modules/@types/body-parser": { 117 | "version": "1.19.5", 118 | "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", 119 | "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", 120 | "license": "MIT", 121 | "dependencies": { 122 | "@types/connect": "*", 123 | "@types/node": "*" 124 | } 125 | }, 126 | "node_modules/@types/connect": { 127 | "version": "3.4.38", 128 | "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", 129 | "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", 130 | "license": "MIT", 131 | "dependencies": { 132 | "@types/node": "*" 133 | } 134 | }, 135 | "node_modules/@types/cors": { 136 | "version": "2.8.17", 137 | "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", 138 | "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", 139 | "license": "MIT", 140 | "dependencies": { 141 | "@types/node": "*" 142 | } 143 | }, 144 | "node_modules/@types/express": { 145 | "version": "5.0.0", 146 | "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", 147 | "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", 148 | "license": "MIT", 149 | "dependencies": { 150 | "@types/body-parser": "*", 151 | "@types/express-serve-static-core": "^5.0.0", 152 | "@types/qs": "*", 153 | "@types/serve-static": "*" 154 | } 155 | }, 156 | "node_modules/@types/express-serve-static-core": { 157 | "version": "5.0.6", 158 | "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", 159 | "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", 160 | "license": "MIT", 161 | "dependencies": { 162 | "@types/node": "*", 163 | "@types/qs": "*", 164 | "@types/range-parser": "*", 165 | "@types/send": "*" 166 | } 167 | }, 168 | "node_modules/@types/http-errors": { 169 | "version": "2.0.4", 170 | "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", 171 | "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", 172 | "license": "MIT" 173 | }, 174 | "node_modules/@types/jsonwebtoken": { 175 | "version": "9.0.9", 176 | "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz", 177 | "integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==", 178 | "license": "MIT", 179 | "dependencies": { 180 | "@types/ms": "*", 181 | "@types/node": "*" 182 | } 183 | }, 184 | "node_modules/@types/mime": { 185 | "version": "1.3.5", 186 | "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", 187 | "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", 188 | "license": "MIT" 189 | }, 190 | "node_modules/@types/ms": { 191 | "version": "2.1.0", 192 | "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", 193 | "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", 194 | "license": "MIT" 195 | }, 196 | "node_modules/@types/node": { 197 | "version": "22.13.9", 198 | "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz", 199 | "integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==", 200 | "license": "MIT", 201 | "dependencies": { 202 | "undici-types": "~6.20.0" 203 | } 204 | }, 205 | "node_modules/@types/node-cron": { 206 | "version": "3.0.11", 207 | "resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.11.tgz", 208 | "integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==", 209 | "dev": true, 210 | "license": "MIT" 211 | }, 212 | "node_modules/@types/qs": { 213 | "version": "6.9.18", 214 | "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", 215 | "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", 216 | "license": "MIT" 217 | }, 218 | "node_modules/@types/range-parser": { 219 | "version": "1.2.7", 220 | "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", 221 | "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", 222 | "license": "MIT" 223 | }, 224 | "node_modules/@types/send": { 225 | "version": "0.17.4", 226 | "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", 227 | "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", 228 | "license": "MIT", 229 | "dependencies": { 230 | "@types/mime": "^1", 231 | "@types/node": "*" 232 | } 233 | }, 234 | "node_modules/@types/serve-static": { 235 | "version": "1.15.7", 236 | "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", 237 | "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", 238 | "license": "MIT", 239 | "dependencies": { 240 | "@types/http-errors": "*", 241 | "@types/node": "*", 242 | "@types/send": "*" 243 | } 244 | }, 245 | "node_modules/@types/webidl-conversions": { 246 | "version": "7.0.3", 247 | "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", 248 | "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", 249 | "license": "MIT" 250 | }, 251 | "node_modules/@types/whatwg-url": { 252 | "version": "11.0.5", 253 | "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", 254 | "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", 255 | "license": "MIT", 256 | "dependencies": { 257 | "@types/webidl-conversions": "*" 258 | } 259 | }, 260 | "node_modules/accepts": { 261 | "version": "1.3.8", 262 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 263 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 264 | "license": "MIT", 265 | "dependencies": { 266 | "mime-types": "~2.1.34", 267 | "negotiator": "0.6.3" 268 | }, 269 | "engines": { 270 | "node": ">= 0.6" 271 | } 272 | }, 273 | "node_modules/acorn": { 274 | "version": "8.14.1", 275 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", 276 | "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", 277 | "dev": true, 278 | "license": "MIT", 279 | "bin": { 280 | "acorn": "bin/acorn" 281 | }, 282 | "engines": { 283 | "node": ">=0.4.0" 284 | } 285 | }, 286 | "node_modules/acorn-walk": { 287 | "version": "8.3.4", 288 | "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", 289 | "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", 290 | "dev": true, 291 | "license": "MIT", 292 | "dependencies": { 293 | "acorn": "^8.11.0" 294 | }, 295 | "engines": { 296 | "node": ">=0.4.0" 297 | } 298 | }, 299 | "node_modules/anymatch": { 300 | "version": "3.1.3", 301 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", 302 | "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", 303 | "license": "ISC", 304 | "dependencies": { 305 | "normalize-path": "^3.0.0", 306 | "picomatch": "^2.0.4" 307 | }, 308 | "engines": { 309 | "node": ">= 8" 310 | } 311 | }, 312 | "node_modules/arg": { 313 | "version": "4.1.3", 314 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", 315 | "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", 316 | "dev": true, 317 | "license": "MIT" 318 | }, 319 | "node_modules/array-flatten": { 320 | "version": "1.1.1", 321 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 322 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", 323 | "license": "MIT" 324 | }, 325 | "node_modules/balanced-match": { 326 | "version": "1.0.2", 327 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 328 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 329 | "license": "MIT" 330 | }, 331 | "node_modules/base64id": { 332 | "version": "2.0.0", 333 | "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", 334 | "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", 335 | "license": "MIT", 336 | "engines": { 337 | "node": "^4.5.0 || >= 5.9" 338 | } 339 | }, 340 | "node_modules/binary-extensions": { 341 | "version": "2.3.0", 342 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", 343 | "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", 344 | "license": "MIT", 345 | "engines": { 346 | "node": ">=8" 347 | }, 348 | "funding": { 349 | "url": "https://github.com/sponsors/sindresorhus" 350 | } 351 | }, 352 | "node_modules/body-parser": { 353 | "version": "1.20.3", 354 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", 355 | "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", 356 | "license": "MIT", 357 | "dependencies": { 358 | "bytes": "3.1.2", 359 | "content-type": "~1.0.5", 360 | "debug": "2.6.9", 361 | "depd": "2.0.0", 362 | "destroy": "1.2.0", 363 | "http-errors": "2.0.0", 364 | "iconv-lite": "0.4.24", 365 | "on-finished": "2.4.1", 366 | "qs": "6.13.0", 367 | "raw-body": "2.5.2", 368 | "type-is": "~1.6.18", 369 | "unpipe": "1.0.0" 370 | }, 371 | "engines": { 372 | "node": ">= 0.8", 373 | "npm": "1.2.8000 || >= 1.4.16" 374 | } 375 | }, 376 | "node_modules/brace-expansion": { 377 | "version": "1.1.11", 378 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 379 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 380 | "license": "MIT", 381 | "dependencies": { 382 | "balanced-match": "^1.0.0", 383 | "concat-map": "0.0.1" 384 | } 385 | }, 386 | "node_modules/braces": { 387 | "version": "3.0.3", 388 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", 389 | "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", 390 | "license": "MIT", 391 | "dependencies": { 392 | "fill-range": "^7.1.1" 393 | }, 394 | "engines": { 395 | "node": ">=8" 396 | } 397 | }, 398 | "node_modules/bson": { 399 | "version": "6.10.3", 400 | "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.3.tgz", 401 | "integrity": "sha512-MTxGsqgYTwfshYWTRdmZRC+M7FnG1b4y7RO7p2k3X24Wq0yv1m77Wsj0BzlPzd/IowgESfsruQCUToa7vbOpPQ==", 402 | "license": "Apache-2.0", 403 | "engines": { 404 | "node": ">=16.20.1" 405 | } 406 | }, 407 | "node_modules/buffer-equal-constant-time": { 408 | "version": "1.0.1", 409 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", 410 | "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", 411 | "license": "BSD-3-Clause" 412 | }, 413 | "node_modules/bytes": { 414 | "version": "3.1.2", 415 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 416 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 417 | "license": "MIT", 418 | "engines": { 419 | "node": ">= 0.8" 420 | } 421 | }, 422 | "node_modules/call-bind-apply-helpers": { 423 | "version": "1.0.2", 424 | "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", 425 | "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", 426 | "license": "MIT", 427 | "dependencies": { 428 | "es-errors": "^1.3.0", 429 | "function-bind": "^1.1.2" 430 | }, 431 | "engines": { 432 | "node": ">= 0.4" 433 | } 434 | }, 435 | "node_modules/call-bound": { 436 | "version": "1.0.4", 437 | "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", 438 | "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", 439 | "license": "MIT", 440 | "dependencies": { 441 | "call-bind-apply-helpers": "^1.0.2", 442 | "get-intrinsic": "^1.3.0" 443 | }, 444 | "engines": { 445 | "node": ">= 0.4" 446 | }, 447 | "funding": { 448 | "url": "https://github.com/sponsors/ljharb" 449 | } 450 | }, 451 | "node_modules/chokidar": { 452 | "version": "3.6.0", 453 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", 454 | "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", 455 | "license": "MIT", 456 | "dependencies": { 457 | "anymatch": "~3.1.2", 458 | "braces": "~3.0.2", 459 | "glob-parent": "~5.1.2", 460 | "is-binary-path": "~2.1.0", 461 | "is-glob": "~4.0.1", 462 | "normalize-path": "~3.0.0", 463 | "readdirp": "~3.6.0" 464 | }, 465 | "engines": { 466 | "node": ">= 8.10.0" 467 | }, 468 | "funding": { 469 | "url": "https://paulmillr.com/funding/" 470 | }, 471 | "optionalDependencies": { 472 | "fsevents": "~2.3.2" 473 | } 474 | }, 475 | "node_modules/concat-map": { 476 | "version": "0.0.1", 477 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 478 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 479 | "license": "MIT" 480 | }, 481 | "node_modules/content-disposition": { 482 | "version": "0.5.4", 483 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 484 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 485 | "license": "MIT", 486 | "dependencies": { 487 | "safe-buffer": "5.2.1" 488 | }, 489 | "engines": { 490 | "node": ">= 0.6" 491 | } 492 | }, 493 | "node_modules/content-type": { 494 | "version": "1.0.5", 495 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 496 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 497 | "license": "MIT", 498 | "engines": { 499 | "node": ">= 0.6" 500 | } 501 | }, 502 | "node_modules/cookie": { 503 | "version": "0.7.1", 504 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", 505 | "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", 506 | "license": "MIT", 507 | "engines": { 508 | "node": ">= 0.6" 509 | } 510 | }, 511 | "node_modules/cookie-signature": { 512 | "version": "1.0.6", 513 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 514 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", 515 | "license": "MIT" 516 | }, 517 | "node_modules/cors": { 518 | "version": "2.8.5", 519 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", 520 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", 521 | "license": "MIT", 522 | "dependencies": { 523 | "object-assign": "^4", 524 | "vary": "^1" 525 | }, 526 | "engines": { 527 | "node": ">= 0.10" 528 | } 529 | }, 530 | "node_modules/create-require": { 531 | "version": "1.1.1", 532 | "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", 533 | "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", 534 | "dev": true, 535 | "license": "MIT" 536 | }, 537 | "node_modules/debug": { 538 | "version": "2.6.9", 539 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 540 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 541 | "license": "MIT", 542 | "dependencies": { 543 | "ms": "2.0.0" 544 | } 545 | }, 546 | "node_modules/depd": { 547 | "version": "2.0.0", 548 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 549 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 550 | "license": "MIT", 551 | "engines": { 552 | "node": ">= 0.8" 553 | } 554 | }, 555 | "node_modules/destroy": { 556 | "version": "1.2.0", 557 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 558 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", 559 | "license": "MIT", 560 | "engines": { 561 | "node": ">= 0.8", 562 | "npm": "1.2.8000 || >= 1.4.16" 563 | } 564 | }, 565 | "node_modules/diff": { 566 | "version": "4.0.2", 567 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", 568 | "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", 569 | "dev": true, 570 | "license": "BSD-3-Clause", 571 | "engines": { 572 | "node": ">=0.3.1" 573 | } 574 | }, 575 | "node_modules/dotenv": { 576 | "version": "16.4.7", 577 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", 578 | "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", 579 | "license": "BSD-2-Clause", 580 | "engines": { 581 | "node": ">=12" 582 | }, 583 | "funding": { 584 | "url": "https://dotenvx.com" 585 | } 586 | }, 587 | "node_modules/dunder-proto": { 588 | "version": "1.0.1", 589 | "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", 590 | "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", 591 | "license": "MIT", 592 | "dependencies": { 593 | "call-bind-apply-helpers": "^1.0.1", 594 | "es-errors": "^1.3.0", 595 | "gopd": "^1.2.0" 596 | }, 597 | "engines": { 598 | "node": ">= 0.4" 599 | } 600 | }, 601 | "node_modules/ecdsa-sig-formatter": { 602 | "version": "1.0.11", 603 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", 604 | "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", 605 | "license": "Apache-2.0", 606 | "dependencies": { 607 | "safe-buffer": "^5.0.1" 608 | } 609 | }, 610 | "node_modules/ee-first": { 611 | "version": "1.1.1", 612 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 613 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", 614 | "license": "MIT" 615 | }, 616 | "node_modules/encodeurl": { 617 | "version": "2.0.0", 618 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", 619 | "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", 620 | "license": "MIT", 621 | "engines": { 622 | "node": ">= 0.8" 623 | } 624 | }, 625 | "node_modules/engine.io": { 626 | "version": "6.6.4", 627 | "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", 628 | "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", 629 | "license": "MIT", 630 | "dependencies": { 631 | "@types/cors": "^2.8.12", 632 | "@types/node": ">=10.0.0", 633 | "accepts": "~1.3.4", 634 | "base64id": "2.0.0", 635 | "cookie": "~0.7.2", 636 | "cors": "~2.8.5", 637 | "debug": "~4.3.1", 638 | "engine.io-parser": "~5.2.1", 639 | "ws": "~8.17.1" 640 | }, 641 | "engines": { 642 | "node": ">=10.2.0" 643 | } 644 | }, 645 | "node_modules/engine.io-parser": { 646 | "version": "5.2.3", 647 | "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", 648 | "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", 649 | "license": "MIT", 650 | "engines": { 651 | "node": ">=10.0.0" 652 | } 653 | }, 654 | "node_modules/engine.io/node_modules/cookie": { 655 | "version": "0.7.2", 656 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", 657 | "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", 658 | "license": "MIT", 659 | "engines": { 660 | "node": ">= 0.6" 661 | } 662 | }, 663 | "node_modules/engine.io/node_modules/debug": { 664 | "version": "4.3.7", 665 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", 666 | "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", 667 | "license": "MIT", 668 | "dependencies": { 669 | "ms": "^2.1.3" 670 | }, 671 | "engines": { 672 | "node": ">=6.0" 673 | }, 674 | "peerDependenciesMeta": { 675 | "supports-color": { 676 | "optional": true 677 | } 678 | } 679 | }, 680 | "node_modules/engine.io/node_modules/ms": { 681 | "version": "2.1.3", 682 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 683 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 684 | "license": "MIT" 685 | }, 686 | "node_modules/es-define-property": { 687 | "version": "1.0.1", 688 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", 689 | "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", 690 | "license": "MIT", 691 | "engines": { 692 | "node": ">= 0.4" 693 | } 694 | }, 695 | "node_modules/es-errors": { 696 | "version": "1.3.0", 697 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 698 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 699 | "license": "MIT", 700 | "engines": { 701 | "node": ">= 0.4" 702 | } 703 | }, 704 | "node_modules/es-object-atoms": { 705 | "version": "1.1.1", 706 | "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", 707 | "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", 708 | "license": "MIT", 709 | "dependencies": { 710 | "es-errors": "^1.3.0" 711 | }, 712 | "engines": { 713 | "node": ">= 0.4" 714 | } 715 | }, 716 | "node_modules/escape-html": { 717 | "version": "1.0.3", 718 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 719 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", 720 | "license": "MIT" 721 | }, 722 | "node_modules/etag": { 723 | "version": "1.8.1", 724 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 725 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 726 | "license": "MIT", 727 | "engines": { 728 | "node": ">= 0.6" 729 | } 730 | }, 731 | "node_modules/express": { 732 | "version": "4.21.2", 733 | "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", 734 | "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", 735 | "license": "MIT", 736 | "dependencies": { 737 | "accepts": "~1.3.8", 738 | "array-flatten": "1.1.1", 739 | "body-parser": "1.20.3", 740 | "content-disposition": "0.5.4", 741 | "content-type": "~1.0.4", 742 | "cookie": "0.7.1", 743 | "cookie-signature": "1.0.6", 744 | "debug": "2.6.9", 745 | "depd": "2.0.0", 746 | "encodeurl": "~2.0.0", 747 | "escape-html": "~1.0.3", 748 | "etag": "~1.8.1", 749 | "finalhandler": "1.3.1", 750 | "fresh": "0.5.2", 751 | "http-errors": "2.0.0", 752 | "merge-descriptors": "1.0.3", 753 | "methods": "~1.1.2", 754 | "on-finished": "2.4.1", 755 | "parseurl": "~1.3.3", 756 | "path-to-regexp": "0.1.12", 757 | "proxy-addr": "~2.0.7", 758 | "qs": "6.13.0", 759 | "range-parser": "~1.2.1", 760 | "safe-buffer": "5.2.1", 761 | "send": "0.19.0", 762 | "serve-static": "1.16.2", 763 | "setprototypeof": "1.2.0", 764 | "statuses": "2.0.1", 765 | "type-is": "~1.6.18", 766 | "utils-merge": "1.0.1", 767 | "vary": "~1.1.2" 768 | }, 769 | "engines": { 770 | "node": ">= 0.10.0" 771 | }, 772 | "funding": { 773 | "type": "opencollective", 774 | "url": "https://opencollective.com/express" 775 | } 776 | }, 777 | "node_modules/fill-range": { 778 | "version": "7.1.1", 779 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", 780 | "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", 781 | "license": "MIT", 782 | "dependencies": { 783 | "to-regex-range": "^5.0.1" 784 | }, 785 | "engines": { 786 | "node": ">=8" 787 | } 788 | }, 789 | "node_modules/finalhandler": { 790 | "version": "1.3.1", 791 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", 792 | "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", 793 | "license": "MIT", 794 | "dependencies": { 795 | "debug": "2.6.9", 796 | "encodeurl": "~2.0.0", 797 | "escape-html": "~1.0.3", 798 | "on-finished": "2.4.1", 799 | "parseurl": "~1.3.3", 800 | "statuses": "2.0.1", 801 | "unpipe": "~1.0.0" 802 | }, 803 | "engines": { 804 | "node": ">= 0.8" 805 | } 806 | }, 807 | "node_modules/forwarded": { 808 | "version": "0.2.0", 809 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 810 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 811 | "license": "MIT", 812 | "engines": { 813 | "node": ">= 0.6" 814 | } 815 | }, 816 | "node_modules/fresh": { 817 | "version": "0.5.2", 818 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 819 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", 820 | "license": "MIT", 821 | "engines": { 822 | "node": ">= 0.6" 823 | } 824 | }, 825 | "node_modules/fsevents": { 826 | "version": "2.3.3", 827 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 828 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 829 | "hasInstallScript": true, 830 | "license": "MIT", 831 | "optional": true, 832 | "os": [ 833 | "darwin" 834 | ], 835 | "engines": { 836 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 837 | } 838 | }, 839 | "node_modules/function-bind": { 840 | "version": "1.1.2", 841 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 842 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 843 | "license": "MIT", 844 | "funding": { 845 | "url": "https://github.com/sponsors/ljharb" 846 | } 847 | }, 848 | "node_modules/get-intrinsic": { 849 | "version": "1.3.0", 850 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", 851 | "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", 852 | "license": "MIT", 853 | "dependencies": { 854 | "call-bind-apply-helpers": "^1.0.2", 855 | "es-define-property": "^1.0.1", 856 | "es-errors": "^1.3.0", 857 | "es-object-atoms": "^1.1.1", 858 | "function-bind": "^1.1.2", 859 | "get-proto": "^1.0.1", 860 | "gopd": "^1.2.0", 861 | "has-symbols": "^1.1.0", 862 | "hasown": "^2.0.2", 863 | "math-intrinsics": "^1.1.0" 864 | }, 865 | "engines": { 866 | "node": ">= 0.4" 867 | }, 868 | "funding": { 869 | "url": "https://github.com/sponsors/ljharb" 870 | } 871 | }, 872 | "node_modules/get-proto": { 873 | "version": "1.0.1", 874 | "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", 875 | "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", 876 | "license": "MIT", 877 | "dependencies": { 878 | "dunder-proto": "^1.0.1", 879 | "es-object-atoms": "^1.0.0" 880 | }, 881 | "engines": { 882 | "node": ">= 0.4" 883 | } 884 | }, 885 | "node_modules/glob-parent": { 886 | "version": "5.1.2", 887 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 888 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 889 | "license": "ISC", 890 | "dependencies": { 891 | "is-glob": "^4.0.1" 892 | }, 893 | "engines": { 894 | "node": ">= 6" 895 | } 896 | }, 897 | "node_modules/gopd": { 898 | "version": "1.2.0", 899 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", 900 | "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", 901 | "license": "MIT", 902 | "engines": { 903 | "node": ">= 0.4" 904 | }, 905 | "funding": { 906 | "url": "https://github.com/sponsors/ljharb" 907 | } 908 | }, 909 | "node_modules/has-flag": { 910 | "version": "3.0.0", 911 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 912 | "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", 913 | "license": "MIT", 914 | "engines": { 915 | "node": ">=4" 916 | } 917 | }, 918 | "node_modules/has-symbols": { 919 | "version": "1.1.0", 920 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", 921 | "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", 922 | "license": "MIT", 923 | "engines": { 924 | "node": ">= 0.4" 925 | }, 926 | "funding": { 927 | "url": "https://github.com/sponsors/ljharb" 928 | } 929 | }, 930 | "node_modules/hasown": { 931 | "version": "2.0.2", 932 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 933 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 934 | "license": "MIT", 935 | "dependencies": { 936 | "function-bind": "^1.1.2" 937 | }, 938 | "engines": { 939 | "node": ">= 0.4" 940 | } 941 | }, 942 | "node_modules/http-errors": { 943 | "version": "2.0.0", 944 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 945 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 946 | "license": "MIT", 947 | "dependencies": { 948 | "depd": "2.0.0", 949 | "inherits": "2.0.4", 950 | "setprototypeof": "1.2.0", 951 | "statuses": "2.0.1", 952 | "toidentifier": "1.0.1" 953 | }, 954 | "engines": { 955 | "node": ">= 0.8" 956 | } 957 | }, 958 | "node_modules/iconv-lite": { 959 | "version": "0.4.24", 960 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 961 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 962 | "license": "MIT", 963 | "dependencies": { 964 | "safer-buffer": ">= 2.1.2 < 3" 965 | }, 966 | "engines": { 967 | "node": ">=0.10.0" 968 | } 969 | }, 970 | "node_modules/ignore-by-default": { 971 | "version": "1.0.1", 972 | "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", 973 | "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", 974 | "license": "ISC" 975 | }, 976 | "node_modules/inherits": { 977 | "version": "2.0.4", 978 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 979 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 980 | "license": "ISC" 981 | }, 982 | "node_modules/ipaddr.js": { 983 | "version": "1.9.1", 984 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 985 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 986 | "license": "MIT", 987 | "engines": { 988 | "node": ">= 0.10" 989 | } 990 | }, 991 | "node_modules/is-binary-path": { 992 | "version": "2.1.0", 993 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 994 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 995 | "license": "MIT", 996 | "dependencies": { 997 | "binary-extensions": "^2.0.0" 998 | }, 999 | "engines": { 1000 | "node": ">=8" 1001 | } 1002 | }, 1003 | "node_modules/is-extglob": { 1004 | "version": "2.1.1", 1005 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 1006 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 1007 | "license": "MIT", 1008 | "engines": { 1009 | "node": ">=0.10.0" 1010 | } 1011 | }, 1012 | "node_modules/is-glob": { 1013 | "version": "4.0.3", 1014 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 1015 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 1016 | "license": "MIT", 1017 | "dependencies": { 1018 | "is-extglob": "^2.1.1" 1019 | }, 1020 | "engines": { 1021 | "node": ">=0.10.0" 1022 | } 1023 | }, 1024 | "node_modules/is-number": { 1025 | "version": "7.0.0", 1026 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 1027 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 1028 | "license": "MIT", 1029 | "engines": { 1030 | "node": ">=0.12.0" 1031 | } 1032 | }, 1033 | "node_modules/jsonwebtoken": { 1034 | "version": "9.0.2", 1035 | "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", 1036 | "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", 1037 | "license": "MIT", 1038 | "dependencies": { 1039 | "jws": "^3.2.2", 1040 | "lodash.includes": "^4.3.0", 1041 | "lodash.isboolean": "^3.0.3", 1042 | "lodash.isinteger": "^4.0.4", 1043 | "lodash.isnumber": "^3.0.3", 1044 | "lodash.isplainobject": "^4.0.6", 1045 | "lodash.isstring": "^4.0.1", 1046 | "lodash.once": "^4.0.0", 1047 | "ms": "^2.1.1", 1048 | "semver": "^7.5.4" 1049 | }, 1050 | "engines": { 1051 | "node": ">=12", 1052 | "npm": ">=6" 1053 | } 1054 | }, 1055 | "node_modules/jsonwebtoken/node_modules/ms": { 1056 | "version": "2.1.3", 1057 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1058 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1059 | "license": "MIT" 1060 | }, 1061 | "node_modules/jwa": { 1062 | "version": "1.4.1", 1063 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", 1064 | "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", 1065 | "license": "MIT", 1066 | "dependencies": { 1067 | "buffer-equal-constant-time": "1.0.1", 1068 | "ecdsa-sig-formatter": "1.0.11", 1069 | "safe-buffer": "^5.0.1" 1070 | } 1071 | }, 1072 | "node_modules/jws": { 1073 | "version": "3.2.2", 1074 | "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", 1075 | "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", 1076 | "license": "MIT", 1077 | "dependencies": { 1078 | "jwa": "^1.4.1", 1079 | "safe-buffer": "^5.0.1" 1080 | } 1081 | }, 1082 | "node_modules/kareem": { 1083 | "version": "2.6.3", 1084 | "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", 1085 | "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", 1086 | "license": "Apache-2.0", 1087 | "engines": { 1088 | "node": ">=12.0.0" 1089 | } 1090 | }, 1091 | "node_modules/lodash.includes": { 1092 | "version": "4.3.0", 1093 | "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", 1094 | "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", 1095 | "license": "MIT" 1096 | }, 1097 | "node_modules/lodash.isboolean": { 1098 | "version": "3.0.3", 1099 | "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", 1100 | "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", 1101 | "license": "MIT" 1102 | }, 1103 | "node_modules/lodash.isinteger": { 1104 | "version": "4.0.4", 1105 | "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", 1106 | "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", 1107 | "license": "MIT" 1108 | }, 1109 | "node_modules/lodash.isnumber": { 1110 | "version": "3.0.3", 1111 | "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", 1112 | "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", 1113 | "license": "MIT" 1114 | }, 1115 | "node_modules/lodash.isplainobject": { 1116 | "version": "4.0.6", 1117 | "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", 1118 | "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", 1119 | "license": "MIT" 1120 | }, 1121 | "node_modules/lodash.isstring": { 1122 | "version": "4.0.1", 1123 | "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", 1124 | "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", 1125 | "license": "MIT" 1126 | }, 1127 | "node_modules/lodash.once": { 1128 | "version": "4.1.1", 1129 | "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", 1130 | "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", 1131 | "license": "MIT" 1132 | }, 1133 | "node_modules/make-error": { 1134 | "version": "1.3.6", 1135 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", 1136 | "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", 1137 | "dev": true, 1138 | "license": "ISC" 1139 | }, 1140 | "node_modules/math-intrinsics": { 1141 | "version": "1.1.0", 1142 | "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", 1143 | "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", 1144 | "license": "MIT", 1145 | "engines": { 1146 | "node": ">= 0.4" 1147 | } 1148 | }, 1149 | "node_modules/media-typer": { 1150 | "version": "0.3.0", 1151 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 1152 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", 1153 | "license": "MIT", 1154 | "engines": { 1155 | "node": ">= 0.6" 1156 | } 1157 | }, 1158 | "node_modules/memory-pager": { 1159 | "version": "1.5.0", 1160 | "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", 1161 | "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", 1162 | "license": "MIT" 1163 | }, 1164 | "node_modules/merge-descriptors": { 1165 | "version": "1.0.3", 1166 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", 1167 | "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", 1168 | "license": "MIT", 1169 | "funding": { 1170 | "url": "https://github.com/sponsors/sindresorhus" 1171 | } 1172 | }, 1173 | "node_modules/methods": { 1174 | "version": "1.1.2", 1175 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 1176 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", 1177 | "license": "MIT", 1178 | "engines": { 1179 | "node": ">= 0.6" 1180 | } 1181 | }, 1182 | "node_modules/mime": { 1183 | "version": "1.6.0", 1184 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 1185 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 1186 | "license": "MIT", 1187 | "bin": { 1188 | "mime": "cli.js" 1189 | }, 1190 | "engines": { 1191 | "node": ">=4" 1192 | } 1193 | }, 1194 | "node_modules/mime-db": { 1195 | "version": "1.52.0", 1196 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 1197 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 1198 | "license": "MIT", 1199 | "engines": { 1200 | "node": ">= 0.6" 1201 | } 1202 | }, 1203 | "node_modules/mime-types": { 1204 | "version": "2.1.35", 1205 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 1206 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 1207 | "license": "MIT", 1208 | "dependencies": { 1209 | "mime-db": "1.52.0" 1210 | }, 1211 | "engines": { 1212 | "node": ">= 0.6" 1213 | } 1214 | }, 1215 | "node_modules/minimatch": { 1216 | "version": "3.1.2", 1217 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 1218 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 1219 | "license": "ISC", 1220 | "dependencies": { 1221 | "brace-expansion": "^1.1.7" 1222 | }, 1223 | "engines": { 1224 | "node": "*" 1225 | } 1226 | }, 1227 | "node_modules/mongodb": { 1228 | "version": "6.14.2", 1229 | "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.14.2.tgz", 1230 | "integrity": "sha512-kMEHNo0F3P6QKDq17zcDuPeaywK/YaJVCEQRzPF3TOM/Bl9MFg64YE5Tu7ifj37qZJMhwU1tl2Ioivws5gRG5Q==", 1231 | "license": "Apache-2.0", 1232 | "dependencies": { 1233 | "@mongodb-js/saslprep": "^1.1.9", 1234 | "bson": "^6.10.3", 1235 | "mongodb-connection-string-url": "^3.0.0" 1236 | }, 1237 | "engines": { 1238 | "node": ">=16.20.1" 1239 | }, 1240 | "peerDependencies": { 1241 | "@aws-sdk/credential-providers": "^3.188.0", 1242 | "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", 1243 | "gcp-metadata": "^5.2.0", 1244 | "kerberos": "^2.0.1", 1245 | "mongodb-client-encryption": ">=6.0.0 <7", 1246 | "snappy": "^7.2.2", 1247 | "socks": "^2.7.1" 1248 | }, 1249 | "peerDependenciesMeta": { 1250 | "@aws-sdk/credential-providers": { 1251 | "optional": true 1252 | }, 1253 | "@mongodb-js/zstd": { 1254 | "optional": true 1255 | }, 1256 | "gcp-metadata": { 1257 | "optional": true 1258 | }, 1259 | "kerberos": { 1260 | "optional": true 1261 | }, 1262 | "mongodb-client-encryption": { 1263 | "optional": true 1264 | }, 1265 | "snappy": { 1266 | "optional": true 1267 | }, 1268 | "socks": { 1269 | "optional": true 1270 | } 1271 | } 1272 | }, 1273 | "node_modules/mongodb-connection-string-url": { 1274 | "version": "3.0.2", 1275 | "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", 1276 | "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", 1277 | "license": "Apache-2.0", 1278 | "dependencies": { 1279 | "@types/whatwg-url": "^11.0.2", 1280 | "whatwg-url": "^14.1.0 || ^13.0.0" 1281 | } 1282 | }, 1283 | "node_modules/mongoose": { 1284 | "version": "8.12.1", 1285 | "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.12.1.tgz", 1286 | "integrity": "sha512-UW22y8QFVYmrb36hm8cGncfn4ARc/XsYWQwRTaj0gxtQk1rDuhzDO1eBantS+hTTatfAIS96LlRCJrcNHvW5+Q==", 1287 | "license": "MIT", 1288 | "dependencies": { 1289 | "bson": "^6.10.3", 1290 | "kareem": "2.6.3", 1291 | "mongodb": "~6.14.0", 1292 | "mpath": "0.9.0", 1293 | "mquery": "5.0.0", 1294 | "ms": "2.1.3", 1295 | "sift": "17.1.3" 1296 | }, 1297 | "engines": { 1298 | "node": ">=16.20.1" 1299 | }, 1300 | "funding": { 1301 | "type": "opencollective", 1302 | "url": "https://opencollective.com/mongoose" 1303 | } 1304 | }, 1305 | "node_modules/mongoose/node_modules/ms": { 1306 | "version": "2.1.3", 1307 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1308 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1309 | "license": "MIT" 1310 | }, 1311 | "node_modules/mpath": { 1312 | "version": "0.9.0", 1313 | "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", 1314 | "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", 1315 | "license": "MIT", 1316 | "engines": { 1317 | "node": ">=4.0.0" 1318 | } 1319 | }, 1320 | "node_modules/mquery": { 1321 | "version": "5.0.0", 1322 | "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", 1323 | "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", 1324 | "license": "MIT", 1325 | "dependencies": { 1326 | "debug": "4.x" 1327 | }, 1328 | "engines": { 1329 | "node": ">=14.0.0" 1330 | } 1331 | }, 1332 | "node_modules/mquery/node_modules/debug": { 1333 | "version": "4.4.0", 1334 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", 1335 | "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", 1336 | "license": "MIT", 1337 | "dependencies": { 1338 | "ms": "^2.1.3" 1339 | }, 1340 | "engines": { 1341 | "node": ">=6.0" 1342 | }, 1343 | "peerDependenciesMeta": { 1344 | "supports-color": { 1345 | "optional": true 1346 | } 1347 | } 1348 | }, 1349 | "node_modules/mquery/node_modules/ms": { 1350 | "version": "2.1.3", 1351 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1352 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1353 | "license": "MIT" 1354 | }, 1355 | "node_modules/ms": { 1356 | "version": "2.0.0", 1357 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 1358 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", 1359 | "license": "MIT" 1360 | }, 1361 | "node_modules/negotiator": { 1362 | "version": "0.6.3", 1363 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 1364 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", 1365 | "license": "MIT", 1366 | "engines": { 1367 | "node": ">= 0.6" 1368 | } 1369 | }, 1370 | "node_modules/node-cron": { 1371 | "version": "3.0.3", 1372 | "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", 1373 | "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==", 1374 | "license": "ISC", 1375 | "dependencies": { 1376 | "uuid": "8.3.2" 1377 | }, 1378 | "engines": { 1379 | "node": ">=6.0.0" 1380 | } 1381 | }, 1382 | "node_modules/node-cron/node_modules/uuid": { 1383 | "version": "8.3.2", 1384 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", 1385 | "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", 1386 | "license": "MIT", 1387 | "bin": { 1388 | "uuid": "dist/bin/uuid" 1389 | } 1390 | }, 1391 | "node_modules/nodemon": { 1392 | "version": "3.1.9", 1393 | "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", 1394 | "integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==", 1395 | "license": "MIT", 1396 | "dependencies": { 1397 | "chokidar": "^3.5.2", 1398 | "debug": "^4", 1399 | "ignore-by-default": "^1.0.1", 1400 | "minimatch": "^3.1.2", 1401 | "pstree.remy": "^1.1.8", 1402 | "semver": "^7.5.3", 1403 | "simple-update-notifier": "^2.0.0", 1404 | "supports-color": "^5.5.0", 1405 | "touch": "^3.1.0", 1406 | "undefsafe": "^2.0.5" 1407 | }, 1408 | "bin": { 1409 | "nodemon": "bin/nodemon.js" 1410 | }, 1411 | "engines": { 1412 | "node": ">=10" 1413 | }, 1414 | "funding": { 1415 | "type": "opencollective", 1416 | "url": "https://opencollective.com/nodemon" 1417 | } 1418 | }, 1419 | "node_modules/nodemon/node_modules/debug": { 1420 | "version": "4.4.0", 1421 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", 1422 | "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", 1423 | "license": "MIT", 1424 | "dependencies": { 1425 | "ms": "^2.1.3" 1426 | }, 1427 | "engines": { 1428 | "node": ">=6.0" 1429 | }, 1430 | "peerDependenciesMeta": { 1431 | "supports-color": { 1432 | "optional": true 1433 | } 1434 | } 1435 | }, 1436 | "node_modules/nodemon/node_modules/ms": { 1437 | "version": "2.1.3", 1438 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1439 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1440 | "license": "MIT" 1441 | }, 1442 | "node_modules/normalize-path": { 1443 | "version": "3.0.0", 1444 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 1445 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 1446 | "license": "MIT", 1447 | "engines": { 1448 | "node": ">=0.10.0" 1449 | } 1450 | }, 1451 | "node_modules/object-assign": { 1452 | "version": "4.1.1", 1453 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 1454 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 1455 | "license": "MIT", 1456 | "engines": { 1457 | "node": ">=0.10.0" 1458 | } 1459 | }, 1460 | "node_modules/object-inspect": { 1461 | "version": "1.13.4", 1462 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", 1463 | "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", 1464 | "license": "MIT", 1465 | "engines": { 1466 | "node": ">= 0.4" 1467 | }, 1468 | "funding": { 1469 | "url": "https://github.com/sponsors/ljharb" 1470 | } 1471 | }, 1472 | "node_modules/on-finished": { 1473 | "version": "2.4.1", 1474 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 1475 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 1476 | "license": "MIT", 1477 | "dependencies": { 1478 | "ee-first": "1.1.1" 1479 | }, 1480 | "engines": { 1481 | "node": ">= 0.8" 1482 | } 1483 | }, 1484 | "node_modules/parseurl": { 1485 | "version": "1.3.3", 1486 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 1487 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 1488 | "license": "MIT", 1489 | "engines": { 1490 | "node": ">= 0.8" 1491 | } 1492 | }, 1493 | "node_modules/path-to-regexp": { 1494 | "version": "0.1.12", 1495 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", 1496 | "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", 1497 | "license": "MIT" 1498 | }, 1499 | "node_modules/picomatch": { 1500 | "version": "2.3.1", 1501 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 1502 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 1503 | "license": "MIT", 1504 | "engines": { 1505 | "node": ">=8.6" 1506 | }, 1507 | "funding": { 1508 | "url": "https://github.com/sponsors/jonschlinkert" 1509 | } 1510 | }, 1511 | "node_modules/proxy-addr": { 1512 | "version": "2.0.7", 1513 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 1514 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 1515 | "license": "MIT", 1516 | "dependencies": { 1517 | "forwarded": "0.2.0", 1518 | "ipaddr.js": "1.9.1" 1519 | }, 1520 | "engines": { 1521 | "node": ">= 0.10" 1522 | } 1523 | }, 1524 | "node_modules/pstree.remy": { 1525 | "version": "1.1.8", 1526 | "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", 1527 | "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", 1528 | "license": "MIT" 1529 | }, 1530 | "node_modules/punycode": { 1531 | "version": "2.3.1", 1532 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", 1533 | "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", 1534 | "license": "MIT", 1535 | "engines": { 1536 | "node": ">=6" 1537 | } 1538 | }, 1539 | "node_modules/qs": { 1540 | "version": "6.13.0", 1541 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", 1542 | "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", 1543 | "license": "BSD-3-Clause", 1544 | "dependencies": { 1545 | "side-channel": "^1.0.6" 1546 | }, 1547 | "engines": { 1548 | "node": ">=0.6" 1549 | }, 1550 | "funding": { 1551 | "url": "https://github.com/sponsors/ljharb" 1552 | } 1553 | }, 1554 | "node_modules/range-parser": { 1555 | "version": "1.2.1", 1556 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 1557 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 1558 | "license": "MIT", 1559 | "engines": { 1560 | "node": ">= 0.6" 1561 | } 1562 | }, 1563 | "node_modules/raw-body": { 1564 | "version": "2.5.2", 1565 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", 1566 | "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", 1567 | "license": "MIT", 1568 | "dependencies": { 1569 | "bytes": "3.1.2", 1570 | "http-errors": "2.0.0", 1571 | "iconv-lite": "0.4.24", 1572 | "unpipe": "1.0.0" 1573 | }, 1574 | "engines": { 1575 | "node": ">= 0.8" 1576 | } 1577 | }, 1578 | "node_modules/readdirp": { 1579 | "version": "3.6.0", 1580 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 1581 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 1582 | "license": "MIT", 1583 | "dependencies": { 1584 | "picomatch": "^2.2.1" 1585 | }, 1586 | "engines": { 1587 | "node": ">=8.10.0" 1588 | } 1589 | }, 1590 | "node_modules/safe-buffer": { 1591 | "version": "5.2.1", 1592 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 1593 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 1594 | "funding": [ 1595 | { 1596 | "type": "github", 1597 | "url": "https://github.com/sponsors/feross" 1598 | }, 1599 | { 1600 | "type": "patreon", 1601 | "url": "https://www.patreon.com/feross" 1602 | }, 1603 | { 1604 | "type": "consulting", 1605 | "url": "https://feross.org/support" 1606 | } 1607 | ], 1608 | "license": "MIT" 1609 | }, 1610 | "node_modules/safer-buffer": { 1611 | "version": "2.1.2", 1612 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1613 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 1614 | "license": "MIT" 1615 | }, 1616 | "node_modules/semver": { 1617 | "version": "7.7.1", 1618 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", 1619 | "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", 1620 | "license": "ISC", 1621 | "bin": { 1622 | "semver": "bin/semver.js" 1623 | }, 1624 | "engines": { 1625 | "node": ">=10" 1626 | } 1627 | }, 1628 | "node_modules/send": { 1629 | "version": "0.19.0", 1630 | "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", 1631 | "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", 1632 | "license": "MIT", 1633 | "dependencies": { 1634 | "debug": "2.6.9", 1635 | "depd": "2.0.0", 1636 | "destroy": "1.2.0", 1637 | "encodeurl": "~1.0.2", 1638 | "escape-html": "~1.0.3", 1639 | "etag": "~1.8.1", 1640 | "fresh": "0.5.2", 1641 | "http-errors": "2.0.0", 1642 | "mime": "1.6.0", 1643 | "ms": "2.1.3", 1644 | "on-finished": "2.4.1", 1645 | "range-parser": "~1.2.1", 1646 | "statuses": "2.0.1" 1647 | }, 1648 | "engines": { 1649 | "node": ">= 0.8.0" 1650 | } 1651 | }, 1652 | "node_modules/send/node_modules/encodeurl": { 1653 | "version": "1.0.2", 1654 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 1655 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", 1656 | "license": "MIT", 1657 | "engines": { 1658 | "node": ">= 0.8" 1659 | } 1660 | }, 1661 | "node_modules/send/node_modules/ms": { 1662 | "version": "2.1.3", 1663 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1664 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1665 | "license": "MIT" 1666 | }, 1667 | "node_modules/serve-static": { 1668 | "version": "1.16.2", 1669 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", 1670 | "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", 1671 | "license": "MIT", 1672 | "dependencies": { 1673 | "encodeurl": "~2.0.0", 1674 | "escape-html": "~1.0.3", 1675 | "parseurl": "~1.3.3", 1676 | "send": "0.19.0" 1677 | }, 1678 | "engines": { 1679 | "node": ">= 0.8.0" 1680 | } 1681 | }, 1682 | "node_modules/setprototypeof": { 1683 | "version": "1.2.0", 1684 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 1685 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", 1686 | "license": "ISC" 1687 | }, 1688 | "node_modules/side-channel": { 1689 | "version": "1.1.0", 1690 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", 1691 | "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", 1692 | "license": "MIT", 1693 | "dependencies": { 1694 | "es-errors": "^1.3.0", 1695 | "object-inspect": "^1.13.3", 1696 | "side-channel-list": "^1.0.0", 1697 | "side-channel-map": "^1.0.1", 1698 | "side-channel-weakmap": "^1.0.2" 1699 | }, 1700 | "engines": { 1701 | "node": ">= 0.4" 1702 | }, 1703 | "funding": { 1704 | "url": "https://github.com/sponsors/ljharb" 1705 | } 1706 | }, 1707 | "node_modules/side-channel-list": { 1708 | "version": "1.0.0", 1709 | "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", 1710 | "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", 1711 | "license": "MIT", 1712 | "dependencies": { 1713 | "es-errors": "^1.3.0", 1714 | "object-inspect": "^1.13.3" 1715 | }, 1716 | "engines": { 1717 | "node": ">= 0.4" 1718 | }, 1719 | "funding": { 1720 | "url": "https://github.com/sponsors/ljharb" 1721 | } 1722 | }, 1723 | "node_modules/side-channel-map": { 1724 | "version": "1.0.1", 1725 | "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", 1726 | "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", 1727 | "license": "MIT", 1728 | "dependencies": { 1729 | "call-bound": "^1.0.2", 1730 | "es-errors": "^1.3.0", 1731 | "get-intrinsic": "^1.2.5", 1732 | "object-inspect": "^1.13.3" 1733 | }, 1734 | "engines": { 1735 | "node": ">= 0.4" 1736 | }, 1737 | "funding": { 1738 | "url": "https://github.com/sponsors/ljharb" 1739 | } 1740 | }, 1741 | "node_modules/side-channel-weakmap": { 1742 | "version": "1.0.2", 1743 | "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", 1744 | "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", 1745 | "license": "MIT", 1746 | "dependencies": { 1747 | "call-bound": "^1.0.2", 1748 | "es-errors": "^1.3.0", 1749 | "get-intrinsic": "^1.2.5", 1750 | "object-inspect": "^1.13.3", 1751 | "side-channel-map": "^1.0.1" 1752 | }, 1753 | "engines": { 1754 | "node": ">= 0.4" 1755 | }, 1756 | "funding": { 1757 | "url": "https://github.com/sponsors/ljharb" 1758 | } 1759 | }, 1760 | "node_modules/sift": { 1761 | "version": "17.1.3", 1762 | "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", 1763 | "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", 1764 | "license": "MIT" 1765 | }, 1766 | "node_modules/simple-update-notifier": { 1767 | "version": "2.0.0", 1768 | "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", 1769 | "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", 1770 | "license": "MIT", 1771 | "dependencies": { 1772 | "semver": "^7.5.3" 1773 | }, 1774 | "engines": { 1775 | "node": ">=10" 1776 | } 1777 | }, 1778 | "node_modules/socket.io": { 1779 | "version": "4.8.1", 1780 | "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", 1781 | "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", 1782 | "license": "MIT", 1783 | "dependencies": { 1784 | "accepts": "~1.3.4", 1785 | "base64id": "~2.0.0", 1786 | "cors": "~2.8.5", 1787 | "debug": "~4.3.2", 1788 | "engine.io": "~6.6.0", 1789 | "socket.io-adapter": "~2.5.2", 1790 | "socket.io-parser": "~4.2.4" 1791 | }, 1792 | "engines": { 1793 | "node": ">=10.2.0" 1794 | } 1795 | }, 1796 | "node_modules/socket.io-adapter": { 1797 | "version": "2.5.5", 1798 | "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", 1799 | "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", 1800 | "license": "MIT", 1801 | "dependencies": { 1802 | "debug": "~4.3.4", 1803 | "ws": "~8.17.1" 1804 | } 1805 | }, 1806 | "node_modules/socket.io-adapter/node_modules/debug": { 1807 | "version": "4.3.7", 1808 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", 1809 | "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", 1810 | "license": "MIT", 1811 | "dependencies": { 1812 | "ms": "^2.1.3" 1813 | }, 1814 | "engines": { 1815 | "node": ">=6.0" 1816 | }, 1817 | "peerDependenciesMeta": { 1818 | "supports-color": { 1819 | "optional": true 1820 | } 1821 | } 1822 | }, 1823 | "node_modules/socket.io-adapter/node_modules/ms": { 1824 | "version": "2.1.3", 1825 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1826 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1827 | "license": "MIT" 1828 | }, 1829 | "node_modules/socket.io-parser": { 1830 | "version": "4.2.4", 1831 | "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", 1832 | "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", 1833 | "license": "MIT", 1834 | "dependencies": { 1835 | "@socket.io/component-emitter": "~3.1.0", 1836 | "debug": "~4.3.1" 1837 | }, 1838 | "engines": { 1839 | "node": ">=10.0.0" 1840 | } 1841 | }, 1842 | "node_modules/socket.io-parser/node_modules/debug": { 1843 | "version": "4.3.7", 1844 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", 1845 | "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", 1846 | "license": "MIT", 1847 | "dependencies": { 1848 | "ms": "^2.1.3" 1849 | }, 1850 | "engines": { 1851 | "node": ">=6.0" 1852 | }, 1853 | "peerDependenciesMeta": { 1854 | "supports-color": { 1855 | "optional": true 1856 | } 1857 | } 1858 | }, 1859 | "node_modules/socket.io-parser/node_modules/ms": { 1860 | "version": "2.1.3", 1861 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1862 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1863 | "license": "MIT" 1864 | }, 1865 | "node_modules/socket.io/node_modules/debug": { 1866 | "version": "4.3.7", 1867 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", 1868 | "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", 1869 | "license": "MIT", 1870 | "dependencies": { 1871 | "ms": "^2.1.3" 1872 | }, 1873 | "engines": { 1874 | "node": ">=6.0" 1875 | }, 1876 | "peerDependenciesMeta": { 1877 | "supports-color": { 1878 | "optional": true 1879 | } 1880 | } 1881 | }, 1882 | "node_modules/socket.io/node_modules/ms": { 1883 | "version": "2.1.3", 1884 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1885 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1886 | "license": "MIT" 1887 | }, 1888 | "node_modules/sparse-bitfield": { 1889 | "version": "3.0.3", 1890 | "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", 1891 | "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", 1892 | "license": "MIT", 1893 | "dependencies": { 1894 | "memory-pager": "^1.0.2" 1895 | } 1896 | }, 1897 | "node_modules/statuses": { 1898 | "version": "2.0.1", 1899 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 1900 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 1901 | "license": "MIT", 1902 | "engines": { 1903 | "node": ">= 0.8" 1904 | } 1905 | }, 1906 | "node_modules/supports-color": { 1907 | "version": "5.5.0", 1908 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 1909 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 1910 | "license": "MIT", 1911 | "dependencies": { 1912 | "has-flag": "^3.0.0" 1913 | }, 1914 | "engines": { 1915 | "node": ">=4" 1916 | } 1917 | }, 1918 | "node_modules/to-regex-range": { 1919 | "version": "5.0.1", 1920 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 1921 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 1922 | "license": "MIT", 1923 | "dependencies": { 1924 | "is-number": "^7.0.0" 1925 | }, 1926 | "engines": { 1927 | "node": ">=8.0" 1928 | } 1929 | }, 1930 | "node_modules/toidentifier": { 1931 | "version": "1.0.1", 1932 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 1933 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 1934 | "license": "MIT", 1935 | "engines": { 1936 | "node": ">=0.6" 1937 | } 1938 | }, 1939 | "node_modules/touch": { 1940 | "version": "3.1.1", 1941 | "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", 1942 | "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", 1943 | "license": "ISC", 1944 | "bin": { 1945 | "nodetouch": "bin/nodetouch.js" 1946 | } 1947 | }, 1948 | "node_modules/tr46": { 1949 | "version": "5.0.0", 1950 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", 1951 | "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", 1952 | "license": "MIT", 1953 | "dependencies": { 1954 | "punycode": "^2.3.1" 1955 | }, 1956 | "engines": { 1957 | "node": ">=18" 1958 | } 1959 | }, 1960 | "node_modules/ts-node": { 1961 | "version": "10.9.2", 1962 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", 1963 | "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", 1964 | "dev": true, 1965 | "license": "MIT", 1966 | "dependencies": { 1967 | "@cspotcode/source-map-support": "^0.8.0", 1968 | "@tsconfig/node10": "^1.0.7", 1969 | "@tsconfig/node12": "^1.0.7", 1970 | "@tsconfig/node14": "^1.0.0", 1971 | "@tsconfig/node16": "^1.0.2", 1972 | "acorn": "^8.4.1", 1973 | "acorn-walk": "^8.1.1", 1974 | "arg": "^4.1.0", 1975 | "create-require": "^1.1.0", 1976 | "diff": "^4.0.1", 1977 | "make-error": "^1.1.1", 1978 | "v8-compile-cache-lib": "^3.0.1", 1979 | "yn": "3.1.1" 1980 | }, 1981 | "bin": { 1982 | "ts-node": "dist/bin.js", 1983 | "ts-node-cwd": "dist/bin-cwd.js", 1984 | "ts-node-esm": "dist/bin-esm.js", 1985 | "ts-node-script": "dist/bin-script.js", 1986 | "ts-node-transpile-only": "dist/bin-transpile.js", 1987 | "ts-script": "dist/bin-script-deprecated.js" 1988 | }, 1989 | "peerDependencies": { 1990 | "@swc/core": ">=1.2.50", 1991 | "@swc/wasm": ">=1.2.50", 1992 | "@types/node": "*", 1993 | "typescript": ">=2.7" 1994 | }, 1995 | "peerDependenciesMeta": { 1996 | "@swc/core": { 1997 | "optional": true 1998 | }, 1999 | "@swc/wasm": { 2000 | "optional": true 2001 | } 2002 | } 2003 | }, 2004 | "node_modules/type-is": { 2005 | "version": "1.6.18", 2006 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 2007 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 2008 | "license": "MIT", 2009 | "dependencies": { 2010 | "media-typer": "0.3.0", 2011 | "mime-types": "~2.1.24" 2012 | }, 2013 | "engines": { 2014 | "node": ">= 0.6" 2015 | } 2016 | }, 2017 | "node_modules/typescript": { 2018 | "version": "5.8.2", 2019 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", 2020 | "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", 2021 | "license": "Apache-2.0", 2022 | "bin": { 2023 | "tsc": "bin/tsc", 2024 | "tsserver": "bin/tsserver" 2025 | }, 2026 | "engines": { 2027 | "node": ">=14.17" 2028 | } 2029 | }, 2030 | "node_modules/undefsafe": { 2031 | "version": "2.0.5", 2032 | "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", 2033 | "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", 2034 | "license": "MIT" 2035 | }, 2036 | "node_modules/undici-types": { 2037 | "version": "6.20.0", 2038 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", 2039 | "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", 2040 | "license": "MIT" 2041 | }, 2042 | "node_modules/unpipe": { 2043 | "version": "1.0.0", 2044 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 2045 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 2046 | "license": "MIT", 2047 | "engines": { 2048 | "node": ">= 0.8" 2049 | } 2050 | }, 2051 | "node_modules/utils-merge": { 2052 | "version": "1.0.1", 2053 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 2054 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", 2055 | "license": "MIT", 2056 | "engines": { 2057 | "node": ">= 0.4.0" 2058 | } 2059 | }, 2060 | "node_modules/uuid": { 2061 | "version": "11.1.0", 2062 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", 2063 | "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", 2064 | "funding": [ 2065 | "https://github.com/sponsors/broofa", 2066 | "https://github.com/sponsors/ctavan" 2067 | ], 2068 | "license": "MIT", 2069 | "bin": { 2070 | "uuid": "dist/esm/bin/uuid" 2071 | } 2072 | }, 2073 | "node_modules/v8-compile-cache-lib": { 2074 | "version": "3.0.1", 2075 | "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", 2076 | "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", 2077 | "dev": true, 2078 | "license": "MIT" 2079 | }, 2080 | "node_modules/vary": { 2081 | "version": "1.1.2", 2082 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 2083 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 2084 | "license": "MIT", 2085 | "engines": { 2086 | "node": ">= 0.8" 2087 | } 2088 | }, 2089 | "node_modules/webidl-conversions": { 2090 | "version": "7.0.0", 2091 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", 2092 | "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", 2093 | "license": "BSD-2-Clause", 2094 | "engines": { 2095 | "node": ">=12" 2096 | } 2097 | }, 2098 | "node_modules/whatwg-url": { 2099 | "version": "14.1.1", 2100 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.1.tgz", 2101 | "integrity": "sha512-mDGf9diDad/giZ/Sm9Xi2YcyzaFpbdLpJPr+E9fSkyQ7KpQD4SdFcugkRQYzhmfI4KeV4Qpnn2sKPdo+kmsgRQ==", 2102 | "license": "MIT", 2103 | "dependencies": { 2104 | "tr46": "^5.0.0", 2105 | "webidl-conversions": "^7.0.0" 2106 | }, 2107 | "engines": { 2108 | "node": ">=18" 2109 | } 2110 | }, 2111 | "node_modules/ws": { 2112 | "version": "8.17.1", 2113 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", 2114 | "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", 2115 | "license": "MIT", 2116 | "engines": { 2117 | "node": ">=10.0.0" 2118 | }, 2119 | "peerDependencies": { 2120 | "bufferutil": "^4.0.1", 2121 | "utf-8-validate": ">=5.0.2" 2122 | }, 2123 | "peerDependenciesMeta": { 2124 | "bufferutil": { 2125 | "optional": true 2126 | }, 2127 | "utf-8-validate": { 2128 | "optional": true 2129 | } 2130 | } 2131 | }, 2132 | "node_modules/yn": { 2133 | "version": "3.1.1", 2134 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", 2135 | "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", 2136 | "dev": true, 2137 | "license": "MIT", 2138 | "engines": { 2139 | "node": ">=6" 2140 | } 2141 | } 2142 | } 2143 | } 2144 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "nodemon dist/server.js", 9 | "build": "tsc", 10 | "dev": "nodemon server.ts" 11 | }, 12 | "dependencies": { 13 | "@types/cors": "^2.8.17", 14 | "@types/express": "^5.0.0", 15 | "@types/jsonwebtoken": "^9.0.9", 16 | "@types/node": "^22.13.9", 17 | "cors": "^2.8.5", 18 | "dotenv": "^16.4.7", 19 | "express": "^4.21.2", 20 | "jsonwebtoken": "^9.0.2", 21 | "mongoose": "^8.12.1", 22 | "node-cron": "^3.0.3", 23 | "nodemon": "^3.1.9", 24 | "socket.io": "^4.8.1", 25 | "typescript": "^5.8.2", 26 | "uuid": "^11.1.0" 27 | }, 28 | "devDependencies": { 29 | "@types/node-cron": "^3.0.11", 30 | "ts-node": "^10.9.2" 31 | }, 32 | "keywords": [], 33 | "author": "", 34 | "license": "ISC", 35 | "type": "commonjs" 36 | } 37 | -------------------------------------------------------------------------------- /server.ts: -------------------------------------------------------------------------------- 1 | import "./src/config/dotenv.config"; 2 | import app from "./src/app"; 3 | import { createServer } from "http"; 4 | import { Server } from "socket.io"; 5 | import { config } from "./src/config/dotenv.config"; 6 | import connectDB from "./src/config/db"; 7 | import cron from "node-cron"; 8 | import { CheckAndDestroyExpiredServers } from "./src/controllers/server.controller"; 9 | 10 | // Constants 11 | const PORT = config.PORT; 12 | const HOST = config.HOST; 13 | 14 | // HTTP Server 15 | const server = createServer(app); 16 | 17 | // Socket.IO 18 | export const io = new Server(server, { 19 | cors: { 20 | origin: process.env.CORS_ORIGIN, 21 | credentials: true 22 | } 23 | }); 24 | 25 | import "./src/sockets/index.socket"; 26 | 27 | 28 | // Set up cron job to check for expired servers every minute 29 | cron.schedule('* * * * *', async () => { 30 | console.log("Checking for expired servers..."); 31 | await CheckAndDestroyExpiredServers(); 32 | console.log("Cleaner job completed"); 33 | }); 34 | 35 | // Start the server 36 | server.listen(PORT, async () => { 37 | await connectDB(); 38 | console.log(`Server is running on http://${HOST}:${PORT}`); 39 | }); -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import cors from "cors"; 3 | import errorHandler from "./middleware/errorHandler"; 4 | import { config } from "./config/dotenv.config"; 5 | import * as ServerRoute from "./routes/server.routes"; 6 | import messagesRouter from "./routes/messages.route"; 7 | import authRouter from "./routes/auth.routes"; 8 | 9 | const app = express(); 10 | 11 | // Middlewares 12 | app.use(express.json()); 13 | app.use(express.urlencoded({ extended: false })); 14 | app.use(cors({ 15 | origin: config.CORS_ORIGIN, 16 | credentials: true 17 | })); 18 | 19 | app.use("/api/server", ServerRoute.default); 20 | app.use("/api/messages", messagesRouter); 21 | app.use("/api/auth", authRouter); 22 | app.use("/", (_, res) => { 23 | res.status(404).send("How TF did you get here? 😂"); 24 | }); 25 | 26 | app.use(errorHandler); 27 | 28 | export default app; -------------------------------------------------------------------------------- /src/config/db.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import { config } from "./dotenv.config"; 3 | 4 | const connectDB = async () => { 5 | try { 6 | await mongoose.connect(config.MONGODB_URI); 7 | console.log("MongoDB connected"); 8 | } catch (error) { 9 | console.error("MongoDB connection error:", error); 10 | process.exit(1); 11 | } 12 | }; 13 | 14 | export default connectDB; -------------------------------------------------------------------------------- /src/config/dotenv.config.ts: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | 3 | (() => { 4 | dotenv.config(); 5 | })(); 6 | 7 | export const config = { 8 | PORT: parseInt(process.env.PORT || "3001"), 9 | HOST: process.env.HOST || "localhost", 10 | JWT_SECRET: process.env.JWT_SECRET || "supersecretkey", 11 | MONGODB_URI: process.env.MONGODB_URI || "mongodb://localhost:27017/secret-room", 12 | CORS_ORIGIN: process.env.CORS_ORIGIN || "http://localhost:3000", 13 | API_KEY: process.env.API_KEY || "supersecretkey", 14 | STATS_ID: process.env.STATS_ID || "app-demo-stats", 15 | } -------------------------------------------------------------------------------- /src/controllers/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Server } from "../models/server.model"; 2 | import { AuthRequest } from "../middleware/auth"; 3 | import { authMiddleware } from "../middleware/auth"; 4 | import AppError from "../types/error.class"; 5 | import { isUserInServer } from "./users.controller"; 6 | import { User } from "../models/user.model"; 7 | import { NextFunction, Request, Response } from "express"; 8 | 9 | export class AuthController { 10 | private static instance: AuthController; 11 | private constructor() {} 12 | 13 | public static getInstance(): AuthController { 14 | if (!AuthController.instance) { 15 | AuthController.instance = new AuthController(); 16 | } 17 | return AuthController.instance; 18 | } 19 | 20 | public refreshToken = async (req: Request, res: Response, next: NextFunction): Promise => { 21 | try { 22 | const { userId, serverId } = req.body; 23 | 24 | if (!serverId) { 25 | throw new AppError(400, 'Server ID is required'); 26 | } 27 | 28 | const server = await Server.findOne({ serverId }); 29 | 30 | if (!server) { 31 | throw new AppError(404, 'Server not found'); 32 | } 33 | 34 | const user = await User.findOne({ userId }); 35 | if (!user) { 36 | throw new AppError(404, 'User not found'); 37 | } 38 | const isServerUser = await isUserInServer(serverId, userId); 39 | 40 | if (!isServerUser) { 41 | throw new AppError(403, 'You are not a member of this server'); 42 | } 43 | const token = authMiddleware.generateToken({ userId, serverId }); 44 | res.status(200).json({ 45 | message: 'Token generated successfully', 46 | token 47 | }); 48 | } catch (error) { 49 | next(error); 50 | } 51 | } 52 | } 53 | 54 | export const authController = AuthController.getInstance(); 55 | 56 | export const refreshToken = (req: Request, res: Response, next: NextFunction): Promise => 57 | authController.refreshToken(req, res, next); 58 | -------------------------------------------------------------------------------- /src/controllers/messages.controller.ts: -------------------------------------------------------------------------------- 1 | import { Message } from "../models/messages.model"; 2 | import AppError from "../types/error.class"; 3 | import { Server } from "../models/server.model"; 4 | import { isUserInServer } from "./users.controller"; 5 | import { getSocketService } from "../sockets/index.socket"; 6 | import { NextFunction, Response } from "express"; 7 | import { AuthRequest } from "../middleware/auth"; 8 | import { decryptMessage } from "../utils/messages.utli"; 9 | import { StatisticsController } from "./statistics.controller"; 10 | 11 | class MessagesController { 12 | private static instance: MessagesController; 13 | 14 | private constructor() {} 15 | 16 | public static getInstance(): MessagesController { 17 | if (!MessagesController.instance) { 18 | MessagesController.instance = new MessagesController(); 19 | } 20 | return MessagesController.instance; 21 | } 22 | 23 | public async sendMessage(serverId: string, content: string, senderId: string, receiverId: string, attachmentUrl?: string, sent: boolean = true): Promise { 24 | if (!serverId || !senderId || !receiverId) { 25 | throw new AppError(400, 'Missing required fields'); 26 | } 27 | 28 | if (!content && !attachmentUrl) { 29 | throw new AppError(400, 'Message content or attachment is required'); 30 | } 31 | 32 | const server = await Server.findOne({ serverId }); 33 | if (!server) { 34 | throw new AppError(404, 'Server not found'); 35 | } 36 | const isSenderInServer = isUserInServer(serverId, senderId); 37 | if (!isSenderInServer) { 38 | throw new AppError(403, 'Sender is not a member of this server'); 39 | } 40 | const isReceiverInServer = isUserInServer(serverId, receiverId); 41 | if (!isReceiverInServer) { 42 | throw new AppError(403, 'Receiver is not a member of this server'); 43 | } 44 | 45 | const payload = { 46 | serverId, 47 | content, 48 | senderId, 49 | receiverId, 50 | attachmentUrl: attachmentUrl || null, 51 | readBySender: false, 52 | readByReceiver: false, 53 | sent: sent || false, 54 | createdAt: new Date() 55 | }; 56 | 57 | const message = await new Message(payload).save(); 58 | 59 | // Update statistics 60 | await StatisticsController.incrementTotalMessages(); 61 | if (attachmentUrl) { 62 | await StatisticsController.incrementTotalFileUploads(); 63 | const ext = attachmentUrl.toLowerCase(); 64 | if (ext.match(/\.(jpg|jpeg|png|webp)$/)) await StatisticsController.incrementFileType('images'); 65 | else if (ext.match(/\.(mp4|webm|mov)$/)) await StatisticsController.incrementFileType('videos'); 66 | else if (ext.match(/\.gif$/)) await StatisticsController.incrementFileType('gifs'); 67 | else if (ext.match(/\.pdf$/)) await StatisticsController.incrementFileType('pdfs'); 68 | else await StatisticsController.incrementFileType('others'); 69 | } 70 | 71 | const output = { 72 | serverId: message.serverId, 73 | content: payload.content, 74 | senderId: message.senderId, 75 | receiverId: message.receiverId, 76 | readBySender: message.readBySender, 77 | readByReceiver: message.readByReceiver, 78 | messageId: message.messageId, 79 | createdAt: message.createdAt, 80 | sent: message.sent, 81 | attachmentUrl: payload.attachmentUrl || undefined 82 | } 83 | 84 | const socketService = getSocketService(); 85 | socketService.broadcastNewMessage(serverId, output); 86 | } 87 | 88 | public async getMessages(request: AuthRequest, response: Response, next: NextFunction): Promise { 89 | try { 90 | const { serverId } = request.params; 91 | const userId = request.user?.userId; 92 | 93 | if (!userId) { 94 | throw new AppError(400, "User ID is required"); 95 | } 96 | 97 | if (!serverId) { 98 | throw new AppError(400, "Server ID is required"); 99 | } 100 | 101 | const server = await Server.findOne({ serverId }); 102 | if (!server) { 103 | throw new AppError(404, "Server not found"); 104 | } 105 | 106 | const isServerUser = isUserInServer(serverId, userId); 107 | if (!isServerUser) { 108 | throw new AppError(403, "You are not a member of this server"); 109 | } 110 | 111 | const messages = await Message.find({ serverId }); 112 | 113 | response.status(200).json({ 114 | message: "Messages found", 115 | data: messages.map(message => ({ 116 | ...message.toObject(), 117 | content: decryptMessage(message.content, server.salt), 118 | attachmentUrl: message.attachmentUrl ? decryptMessage(message.attachmentUrl, server.salt) : null, 119 | })) 120 | }); 121 | } catch (error) { 122 | next(error); 123 | } 124 | } 125 | 126 | public async markMessageRead(messageId: string): Promise { 127 | try { 128 | const message = await Message.findOne({ messageId }); 129 | if (!message) { 130 | throw new AppError(404, "Message not found"); 131 | } 132 | message.readByReceiver = true; 133 | await message.save(); 134 | 135 | const socketService = getSocketService(); 136 | socketService.broadcastMessageRead(messageId, message.serverId); 137 | } catch (error) { 138 | throw error; 139 | } 140 | } 141 | } 142 | 143 | const messagesController = MessagesController.getInstance(); 144 | 145 | export default messagesController; -------------------------------------------------------------------------------- /src/controllers/server.controller.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | import { Server } from "../models/server.model"; 3 | import { generateId } from "../utils/index.util"; 4 | import AppError from "../types/error.class"; 5 | import { 6 | CreateServerRequest, 7 | ServerResponse, 8 | ServerData, 9 | IServerController 10 | } from "../types/server.interface"; 11 | import { getSocketService } from "../sockets/index.socket"; 12 | import { addUserToServer, getActiveUsers, clearServerUsers, isUserInServer } from "./users.controller"; 13 | import { generateUsername } from "../utils/user.util"; 14 | import { Message } from "../models/messages.model"; 15 | import Invitation from "../models/invitation.model"; 16 | import { generateToken } from "../middleware/auth"; 17 | import { AuthRequest } from "../middleware/auth"; 18 | import { ServerAction } from "../types/server-socket.interface"; 19 | import { User } from "../models/user.model"; 20 | import { StatisticsController } from "./statistics.controller"; 21 | import messagesController from "./messages.controller"; 22 | 23 | class ServerController implements IServerController { 24 | private static instance: ServerController; 25 | private readonly MIN_SERVER_NAME_LENGTH = 3; 26 | private readonly MAX_SERVER_NAME_LENGTH = 50; 27 | private readonly MIN_ENCRYPTION_KEY_LENGTH = 8; 28 | private readonly MIN_LIFESPAN = 1000 * 60 * 60; 29 | private readonly MAX_LIFESPAN = 1000 * 60 * 60 * 24; 30 | 31 | // Character sets for encryption key validation 32 | private readonly VALID_CHARS = { 33 | uppercase: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 34 | lowercase: 'abcdefghijklmnopqrstuvwxyz', 35 | numbers: '0123456789', 36 | specialChars: '!@#$%^&*()_+-=[]{}|;:,.<>?', 37 | hexChars: 'ABCDEF0123456789', 38 | base64Chars: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' 39 | }; 40 | 41 | public static getInstance(): ServerController { 42 | if (!ServerController.instance) { 43 | ServerController.instance = new ServerController(); 44 | } 45 | return ServerController.instance; 46 | } 47 | 48 | public validateRequest(body: any): body is CreateServerRequest { 49 | const { serverName, encryptionKey, lifeSpan, fingerprint } = body; 50 | 51 | if (!serverName || !encryptionKey || !lifeSpan || !fingerprint) { 52 | throw new AppError(400, "All fields are required"); 53 | } 54 | 55 | if (typeof serverName !== 'string' || 56 | typeof encryptionKey !== 'string' || 57 | typeof lifeSpan !== 'number' || 58 | typeof fingerprint !== 'string') { 59 | throw new AppError(400, "Invalid field types. Expected: serverName (string), encryptionKey (string), lifeSpan (number), fingerprint (string)"); 60 | } 61 | 62 | this.validateServerName(serverName); 63 | this.validateEncryptionKey(encryptionKey); 64 | this.validateLifeSpan(lifeSpan); 65 | 66 | return true; 67 | } 68 | 69 | private validateServerName(serverName: string): void { 70 | if (serverName.length < this.MIN_SERVER_NAME_LENGTH) { 71 | throw new AppError(400, `Server name must be at least ${this.MIN_SERVER_NAME_LENGTH} characters long`); 72 | } 73 | if (serverName.length > this.MAX_SERVER_NAME_LENGTH) { 74 | throw new AppError(400, `Server name cannot exceed ${this.MAX_SERVER_NAME_LENGTH} characters`); 75 | } 76 | if (!/^[a-zA-Z0-9-_\s]+$/.test(serverName)) { 77 | throw new AppError(400, "Server name can only contain letters, numbers, spaces, hyphens, and underscores"); 78 | } 79 | } 80 | 81 | private validateEncryptionKey(encryptionKey: string): void { 82 | if (encryptionKey.length < this.MIN_ENCRYPTION_KEY_LENGTH) { 83 | throw new AppError(400, `Encryption key must be at least ${this.MIN_ENCRYPTION_KEY_LENGTH} characters long`); 84 | } 85 | 86 | const validChars = new Set([ 87 | ...this.VALID_CHARS.uppercase, 88 | ...this.VALID_CHARS.lowercase, 89 | ...this.VALID_CHARS.numbers, 90 | ...this.VALID_CHARS.specialChars, 91 | ...this.VALID_CHARS.base64Chars 92 | ]); 93 | 94 | const invalidChars = [...encryptionKey].filter(char => !validChars.has(char)); 95 | if (invalidChars.length > 0) { 96 | throw new AppError(400, `Invalid characters in encryption key: ${invalidChars.join(' ')}`); 97 | } 98 | } 99 | 100 | private validateLifeSpan(lifeSpan: number): void { 101 | if (lifeSpan < this.MIN_LIFESPAN) { 102 | throw new AppError(400, "Lifespan must be at least 1 hour"); 103 | } 104 | if (lifeSpan > this.MAX_LIFESPAN) { 105 | throw new AppError(400, "Lifespan cannot exceed 1 day"); 106 | } 107 | } 108 | 109 | public generateServerData(request: CreateServerRequest): ServerData { 110 | return { 111 | serverId: `server-${generateId()}`, 112 | serverName: request.serverName.trim(), 113 | salt: request.encryptionKey, 114 | expiresAt: new Date(Date.now() + request.lifeSpan) 115 | }; 116 | } 117 | 118 | public formatResponse(server: ServerData): ServerResponse { 119 | return { 120 | server_name: server.serverName, 121 | server_id: server.serverId, 122 | expiration: server.expiresAt 123 | }; 124 | } 125 | 126 | public async createServer(request: Request, response: Response, next: NextFunction): Promise { 127 | try { 128 | this.validateRequest(request.body); 129 | 130 | const createUserIdentity = { 131 | userId: request.body.fingerprint, 132 | } 133 | 134 | const serverData = this.generateServerData(request.body); 135 | const server = new Server({ 136 | ...serverData, 137 | type: "Private", 138 | owner: createUserIdentity.userId, 139 | globalInvitationId: `global-${generateId()}` 140 | }); 141 | 142 | await server.save(); 143 | 144 | const formattedResponse = this.formatResponse(serverData); 145 | 146 | addUserToServer(server.serverId, { 147 | ...createUserIdentity, 148 | isOnline: true, 149 | lastSeen: new Date() 150 | }); 151 | 152 | // Generate JWT token for the new user 153 | const token = generateToken({ 154 | userId: createUserIdentity.userId, 155 | serverId: serverData.serverId 156 | }); 157 | 158 | // Get socket service and emit server creation event 159 | const socketService = getSocketService(); 160 | socketService.emitToServer(serverData.serverId, { 161 | type: 'status', 162 | content: 'Server created successfully', 163 | timestamp: Date.now() 164 | }); 165 | 166 | await StatisticsController.incrementTotalServers(); 167 | await StatisticsController.incrementActiveServers(); 168 | 169 | 170 | response.status(201).json({ 171 | message: "Server created successfully", 172 | data: { 173 | ...formattedResponse, 174 | global_invitation_id: server.globalInvitationId, 175 | owner: server.owner 176 | }, 177 | user: createUserIdentity, 178 | token 179 | }); 180 | } catch (error) { 181 | next(error); 182 | } 183 | } 184 | 185 | public async getServer(request: AuthRequest, response: Response, next: NextFunction): Promise { 186 | try { 187 | if (!request.user) { 188 | throw new AppError(401, "Unauthorized"); 189 | } 190 | 191 | const { serverId } = request.params; 192 | const userId = request.user.userId; 193 | const refreshToken = request.header("X-Refresh-Token"); 194 | 195 | if (!serverId) { 196 | throw new AppError(400, "Server ID is required"); 197 | } 198 | 199 | if (!userId) { 200 | throw new AppError(400, "User ID is required"); 201 | } 202 | 203 | const server = await Server.findOne({ serverId }); 204 | 205 | if (!server) { 206 | throw new AppError(404, "Server not found"); 207 | } 208 | 209 | // Check if user is in server 210 | const isActive = await isUserInServer(serverId, userId as string); 211 | const username = generateUsername(`${userId}-${serverId}`); 212 | 213 | if (!isActive) { 214 | // If user is not active but is the owner, add them back 215 | if (server.owner === userId) { 216 | addUserToServer(serverId, { 217 | userId: userId as string, 218 | isOnline: true, 219 | lastSeen: new Date() 220 | }); 221 | } else { 222 | throw new AppError(403, "You are not a member of this server"); 223 | } 224 | } 225 | 226 | // Connect to socket 227 | const socketService = getSocketService(); 228 | 229 | socketService.emitServerActions(serverId, ServerAction.JOIN, username); 230 | 231 | response.status(200).json({ 232 | message: "Server found", 233 | data: { 234 | server_name: server.serverName, 235 | server_id: server.serverId, 236 | expiration: server.expiresAt, 237 | global_invitation_id: server.globalInvitationId, 238 | owner: server.owner, 239 | username 240 | }, 241 | refresh_token: refreshToken 242 | }); 243 | } catch (error) { 244 | next(error); 245 | } 246 | } 247 | 248 | public async getServerActiveUsers(request: AuthRequest, response: Response, next: NextFunction): Promise { 249 | try { 250 | const { serverId } = request.params; 251 | const userId = request.user?.userId; 252 | const refreshToken = request.header("X-Refresh-Token"); 253 | 254 | if (!serverId) { 255 | throw new AppError(400, "Server ID is required"); 256 | } 257 | 258 | if (!userId) { 259 | throw new AppError(400, "User Fingerprint is required"); 260 | } 261 | 262 | const isServerUser = await isUserInServer(serverId, userId); 263 | if (!isServerUser) { 264 | throw new AppError(403, "You are not a member of this server"); 265 | } 266 | 267 | const server = await Server.findOne({ serverId }); 268 | if (!server) { 269 | throw new AppError(404, "Server not found"); 270 | } 271 | 272 | const activeUsers = await getActiveUsers(server.serverId); 273 | 274 | response.status(200).json({ 275 | message: "Active users retrieved successfully", 276 | data: activeUsers, 277 | refresh_token: refreshToken 278 | }); 279 | } catch (error) { 280 | next(error); 281 | } 282 | } 283 | 284 | public async globalInvitation(request: Request, response: Response, next: NextFunction): Promise { 285 | try { 286 | const { globalInvitationId } = request.params; 287 | const { fingerprint } = request.body; 288 | 289 | if (!globalInvitationId) { 290 | throw new AppError(400, "Global invitation ID is required"); 291 | } 292 | 293 | if (!fingerprint) { 294 | throw new AppError(400, "A fingerprint ID is required"); 295 | } 296 | 297 | // Find server by global invitation ID 298 | const server = await Server.findOne({ globalInvitationId }); 299 | 300 | 301 | if (!server) { 302 | throw new AppError(404, "Invalid invitation link"); 303 | } 304 | 305 | // Check if server has expired 306 | if (server.expiresAt < new Date() && server.type === "Private") { 307 | throw new AppError(411, "This server has expired 02"); 308 | } 309 | 310 | // Check if user already exists in server 311 | const userIdentityExist = await isUserInServer(server.serverId, fingerprint) || false; 312 | 313 | const userIdentity = { 314 | userId: fingerprint, 315 | }; 316 | 317 | // If user doesn't exist, create new identity 318 | if (userIdentityExist) { 319 | throw new AppError(409, `You are already in this server_${server.serverId}`); 320 | } 321 | 322 | addUserToServer(server.serverId, { 323 | ...userIdentity, 324 | isOnline: true, 325 | lastSeen: new Date() 326 | }); 327 | 328 | const socketService = getSocketService(); 329 | socketService.emitServerActions(server.serverId, ServerAction.JOIN, generateUsername(`${userIdentity.userId}-${server.serverId}`)); 330 | 331 | // Generate JWT token for the user 332 | const token = generateToken({ 333 | userId: userIdentity.userId, 334 | serverId: server.serverId 335 | }); 336 | 337 | // send user a message if they joined a public server 338 | if (server.type === "Public") { 339 | const message = { 340 | content: "You’re anonymous now. What’s something you’ve been wanting to say out loud?", 341 | senderId: server.owner, 342 | receiverId: userIdentity.userId, 343 | attachmentUrl: undefined 344 | }; 345 | 346 | const message_2 = { 347 | content: "You can send messages, share files, and much more.", 348 | senderId: server.owner, 349 | receiverId: userIdentity.userId, 350 | attachmentUrl: undefined 351 | }; 352 | 353 | const message_3 = { 354 | content: "💀", 355 | senderId: server.owner, 356 | receiverId: userIdentity.userId, 357 | attachmentUrl: undefined 358 | }; 359 | 360 | await messagesController.sendMessage(server.serverId, message.content, message.senderId, message.receiverId, message.attachmentUrl); 361 | await messagesController.sendMessage(server.serverId, message_2.content, message_2.senderId, message_2.receiverId, message_2.attachmentUrl); 362 | await messagesController.sendMessage(server.serverId, message_3.content, message_3.senderId, message_3.receiverId, message_3.attachmentUrl); 363 | } 364 | 365 | // Return server details needed for joining 366 | response.status(200).json({ 367 | success: true, 368 | data: { 369 | serverId: server.serverId, 370 | serverName: server.serverName, 371 | expiresAt: server.expiresAt 372 | }, 373 | user: userIdentity, 374 | token 375 | }); 376 | 377 | } catch (error) { 378 | next(error); 379 | } 380 | } 381 | 382 | public async uniqueInvitation(request: Request, response: Response, next: NextFunction): Promise { 383 | try { 384 | const { inviteCode } = request.params; 385 | const { fingerprint } = request.body; 386 | 387 | if (!inviteCode) { 388 | throw new AppError(400, "Invite code is required"); 389 | } 390 | 391 | if (!fingerprint) { 392 | throw new AppError(400, "Fingerprint is required"); 393 | } 394 | 395 | const invitation = await Invitation.findOne({ inviteCode }); 396 | 397 | if (!invitation) { 398 | throw new AppError(404, "Server invitation not found"); 399 | } 400 | 401 | if (invitation.used) { 402 | throw new AppError(409, "Server invitation already used"); 403 | } 404 | 405 | const server = await Server.findOne({ serverId: invitation.serverId }); 406 | 407 | if (!server) { 408 | throw new AppError(404, "Server not found"); 409 | } 410 | 411 | // Check if user already exists in server 412 | const userIdentityExist = await isUserInServer(server.serverId, fingerprint) || false; 413 | 414 | const userIdentity = { 415 | userId: fingerprint, 416 | }; 417 | 418 | // If user doesn't exist, create new identity 419 | if (userIdentityExist) { 420 | throw new AppError(409, `You are already in this server_${server.serverId}`); 421 | } 422 | 423 | addUserToServer(server.serverId, { 424 | ...userIdentity, 425 | isOnline: true, 426 | lastSeen: new Date() 427 | }); 428 | 429 | const socketService = getSocketService(); 430 | const username = generateUsername(`${userIdentity.userId}-${server.serverId}`); 431 | 432 | socketService.emitServerActions(server.serverId, ServerAction.JOIN, username); 433 | 434 | // Generate JWT token for the user 435 | const token = generateToken({ 436 | userId: userIdentity.userId, 437 | serverId: server.serverId 438 | }); 439 | 440 | // Update invitation status 441 | await Invitation.updateOne({ inviteCode }, { used: true }); 442 | 443 | // Return server details needed for joining 444 | response.status(200).json({ 445 | success: true, 446 | data: { 447 | serverId: server.serverId, 448 | serverName: server.serverName, 449 | expiresAt: server.expiresAt 450 | }, 451 | user: userIdentity, 452 | token 453 | }); 454 | 455 | } catch (error) { 456 | next(error); 457 | } 458 | } 459 | 460 | public async generateUniqueServerInvitationId(request: Request, response: Response, next: NextFunction): Promise { 461 | try { 462 | const { serverId } = request.params; 463 | 464 | if (!serverId) { 465 | throw new AppError(400, "Server ID is required"); 466 | } 467 | 468 | const server = await Server.findOne({ serverId }); 469 | if (!server) { 470 | throw new AppError(404, "Server not found"); 471 | } 472 | 473 | const uniqueInvitationId = `invite-${generateId()}`; 474 | 475 | const invitation = new Invitation({ 476 | inviteCode: uniqueInvitationId, 477 | used: false, 478 | serverId, 479 | expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), 480 | }); 481 | 482 | await invitation.save(); 483 | 484 | response.status(200).json({ 485 | message: "Server invitation ID generated successfully", 486 | data: { 487 | inviteCode: uniqueInvitationId 488 | } 489 | }); 490 | } catch (error) { 491 | next(error); 492 | } 493 | } 494 | 495 | // Leave server 496 | public async leaveServer(serverId: string, userId: string): Promise { 497 | try { 498 | if (!serverId) { 499 | throw new AppError(400, "Server ID is required"); 500 | } 501 | 502 | const server = await Server.findOne({ serverId }); 503 | const username = generateUsername(`${userId}-${serverId}`); 504 | 505 | if (!server) { 506 | throw new AppError(404, "Server not found"); 507 | } 508 | 509 | if (!userId) { 510 | throw new AppError(400, "User ID is required"); 511 | } 512 | 513 | const isServerUser = await isUserInServer(serverId, userId); 514 | if (!isServerUser) { 515 | throw new AppError(403, "You are not a member of this server"); 516 | } 517 | 518 | // Remove user from server 519 | await Server.updateOne({ serverId }, { $pull: { approvedUsers: userId } }); 520 | 521 | // Remove server from user 522 | await User.updateOne({ userId }, { $pull: { servers: serverId } }); 523 | 524 | const socketService = getSocketService(); 525 | socketService.emitServerActions(server.serverId, ServerAction.LEAVE, username); 526 | } catch (error) { 527 | console.error('Error during leave process:', error); 528 | } 529 | } 530 | 531 | public async deleteServer(request: AuthRequest, response: Response, next: NextFunction): Promise { 532 | try { 533 | const { serverId } = request.params; 534 | const userId = request.user?.userId; 535 | 536 | if (!serverId) { 537 | throw new AppError(400, "Server ID is required"); 538 | } 539 | 540 | if (!userId) { 541 | throw new AppError(400, "User ID is required"); 542 | } 543 | 544 | const isServerUser = await isUserInServer(serverId, userId); 545 | if (!isServerUser) { 546 | throw new AppError(403, "You are not a member of this server"); 547 | } 548 | 549 | const server = await Server.findOne({ serverId: serverId }); 550 | 551 | if (!server) { 552 | throw new AppError(404, "Server not found"); 553 | } 554 | 555 | if (server.owner !== userId) { 556 | throw new AppError(403, "You are not the owner of this server"); 557 | } 558 | 559 | await this.deleteServerById(serverId); 560 | 561 | const socketService = getSocketService(); 562 | socketService.broadcastServerDeleted(serverId); 563 | 564 | await StatisticsController.increment('deletedServers'); 565 | 566 | response.status(200).json({ 567 | message: "Server deleted successfully" 568 | }); 569 | } catch (error) { 570 | next(error); 571 | } 572 | } 573 | 574 | private async deleteServerById(serverId: string) { 575 | try { 576 | const server = await Server.findOne({ serverId }); 577 | if (!server) { 578 | throw new AppError(404, "Server not found"); 579 | } 580 | 581 | // Remove server from user 582 | await User.updateMany({ servers: serverId }, { $pull: { servers: serverId } }); 583 | 584 | // Delete messages 585 | await Message.deleteMany({ serverId }); 586 | 587 | // Delete invitations 588 | await Invitation.deleteMany({ serverId }); 589 | 590 | // Delete server 591 | await Server.deleteOne({ serverId }); 592 | 593 | await StatisticsController.decrementActiveServers(1); 594 | 595 | const socketService = getSocketService(); 596 | socketService.broadcastServerDeleted(serverId); 597 | } catch (error) { 598 | throw error; 599 | } 600 | } 601 | 602 | public async checkAndDestroyExpiredServers() { 603 | let counter = 0; 604 | const publicServers = await Server.find({ type: "Public"}); 605 | for (const server of publicServers) { 606 | server.expiresAt = new Date(); 607 | await server.save(); 608 | } 609 | const expiredServers = await Server.find({ type: "Private"}); 610 | for (const server of expiredServers) { 611 | // Check if the server has expired 612 | const expirationDate = new Date(server.expiresAt).getTime(); 613 | 614 | if (expirationDate < new Date().getTime()) { 615 | counter++; 616 | await this.deleteServerById(server.serverId); 617 | await StatisticsController.increment('expiredServers'); 618 | } 619 | } 620 | 621 | await StatisticsController.updatePreviousStats(); 622 | 623 | } 624 | } 625 | 626 | // Export singleton instance 627 | export const serverController = ServerController.getInstance(); 628 | 629 | // Export controller method for route handler 630 | export const CreateServer = (req: Request, res: Response, next: NextFunction): Promise => 631 | serverController.createServer(req, res, next); 632 | 633 | export const GetServer = (req: Request, res: Response, next: NextFunction): Promise => 634 | serverController.getServer(req, res, next); 635 | 636 | export const GetServerActiveUsers = (req: Request, res: Response, next: NextFunction): Promise => 637 | serverController.getServerActiveUsers(req, res, next); 638 | 639 | export const GlobalInvitation = (req: Request, res: Response, next: NextFunction): Promise => 640 | serverController.globalInvitation(req, res, next); 641 | 642 | export const UniqueInvitation = (req: Request, res: Response, next: NextFunction): Promise => 643 | serverController.uniqueInvitation(req, res, next); 644 | 645 | export const GenerateUniqueServerInvitationId = (req: Request, res: Response, next: NextFunction): Promise => 646 | serverController.generateUniqueServerInvitationId(req, res, next); 647 | 648 | export const LeaveServer = (serverId: string, userId: string): Promise => 649 | serverController.leaveServer(serverId, userId); 650 | 651 | export const DeleteServer = (req: AuthRequest, res: Response, next: NextFunction): Promise => 652 | serverController.deleteServer(req, res, next); 653 | 654 | export const CheckAndDestroyExpiredServers = (): Promise => 655 | serverController.checkAndDestroyExpiredServers(); -------------------------------------------------------------------------------- /src/controllers/statistics.controller.ts: -------------------------------------------------------------------------------- 1 | // controllers/statistics.controller.ts 2 | import { AppStatistics, IAppStatistics } from '../models/statistics.model'; 3 | import { config } from '../config/dotenv.config'; 4 | import { initialStats } from '../utils/constants'; 5 | import { FilterQuery } from 'mongoose'; 6 | import { Server } from '../models/server.model'; 7 | import { User } from '../models/user.model'; 8 | import { Message } from '../models/messages.model'; 9 | 10 | const { STATS_ID } = config; 11 | 12 | type StatKey = 'totalServers' | 'totalMessages' | 'totalUsers' | 'activeServers' | 'activeUsers' | 'totalFileUploads' | 'fileTypes' | 'deletedServers' | 'expiredServers'; 13 | type FileTypeKey = keyof IAppStatistics['fileTypes']; 14 | type UpdateResult = { success: boolean; data?: any; error?: Error }; 15 | 16 | export class StatisticsController { 17 | public static async initialize() { 18 | try { 19 | const stats = await AppStatistics.findOne({ id: STATS_ID }); 20 | 21 | if (stats) return; 22 | 23 | const servers = await Server.find(); 24 | const users = await User.find(); 25 | const messages = await Message.find(); 26 | 27 | // Count file types from messages 28 | const initialStats = { 29 | totalServers: 0, 30 | totalMessages: 0, 31 | totalUsers: 0, 32 | activeServers: 0, 33 | activeUsers: 0, 34 | totalFileUploads: 0, 35 | deletedServers: 0, 36 | expiredServers: 0, 37 | fileTypes: { 38 | images: 0, 39 | videos: 0, 40 | gifs: 0, 41 | pdfs: 0, 42 | others: 0 43 | } 44 | }; 45 | 46 | const messagesWithAttachments = await Message.find({ attachmentUrl: { $exists: true, $ne: null } }); 47 | messagesWithAttachments.forEach(msg => { 48 | if (msg.attachmentUrl) { 49 | const ext = msg.attachmentUrl.toLowerCase(); 50 | if (ext.match(/\.(jpg|jpeg|png|webp)$/)) { 51 | initialStats.fileTypes.images++; 52 | } else if (ext.match(/\.(mp4|webm|mov)$/)) { 53 | initialStats.fileTypes.videos++; 54 | } else if (ext.match(/\.gif$/)) { 55 | initialStats.fileTypes.gifs++; 56 | } else if (ext.match(/\.pdf$/)) { 57 | initialStats.fileTypes.pdfs++; 58 | } else { 59 | initialStats.fileTypes.others++; 60 | } 61 | } 62 | }); 63 | 64 | const payload = { 65 | id: STATS_ID, 66 | ...initialStats, 67 | totalServers: servers.length, 68 | totalMessages: messages.length, 69 | totalUsers: users.length, 70 | totalFileUploads: messagesWithAttachments.length, 71 | activeUsers: users.filter((u) => u.isOnline === true).length, 72 | activeServers: servers.length, 73 | lastUpdated: new Date(), 74 | previousStats: [] 75 | }; 76 | 77 | await AppStatistics.create(payload); 78 | } catch (error) { 79 | console.error('Error initializing statistics:', error); 80 | } 81 | } 82 | 83 | /** 84 | * Increment total servers count 85 | * @param count Amount to increment (default: 1) 86 | */ 87 | static async incrementTotalServers(count = 1): Promise { 88 | return this.increment('totalServers', count); 89 | } 90 | 91 | /** 92 | * Set total servers count 93 | * @param value New value to set 94 | */ 95 | static async setTotalServers(value: number): Promise { 96 | return this.set('totalServers', value); 97 | } 98 | 99 | /** 100 | * Increment total messages count 101 | * @param count Amount to increment (default: 1) 102 | */ 103 | static async incrementTotalMessages(count = 1): Promise { 104 | return this.increment('totalMessages', count); 105 | } 106 | 107 | /** 108 | * Set total messages count 109 | * @param value New value to set 110 | */ 111 | static async setTotalMessages(value: number): Promise { 112 | return this.set('totalMessages', value); 113 | } 114 | 115 | /** 116 | * Increment total users count 117 | * @param count Amount to increment (default: 1) 118 | */ 119 | static async incrementTotalUsers(count = 1): Promise { 120 | return this.increment('totalUsers', count); 121 | } 122 | 123 | /** 124 | * Set total users count 125 | * @param value New value to set 126 | */ 127 | static async setTotalUsers(value: number): Promise { 128 | return this.set('totalUsers', value); 129 | } 130 | 131 | /** 132 | * Increment active servers count 133 | * @param count Amount to increment (default: 1) 134 | */ 135 | static async incrementActiveServers(count = 1): Promise { 136 | return this.increment('activeServers', count); 137 | } 138 | 139 | static async decrementActiveServers(count = 1): Promise { 140 | return this.increment('activeServers', -count); 141 | } 142 | 143 | /** 144 | * Set active servers count 145 | * @param value New value to set 146 | */ 147 | static async setActiveServers(value: number): Promise { 148 | return this.set('activeServers', value); 149 | } 150 | 151 | /** 152 | * Increment active users count 153 | * @param count Amount to increment (default: 1) 154 | */ 155 | static async incrementActiveUsers(count = 1): Promise { 156 | return this.increment('activeUsers', count); 157 | } 158 | 159 | static async decrementActiveUsers(count = 1): Promise { 160 | return this.increment('activeUsers', -count); 161 | } 162 | 163 | /** 164 | * Set active users count 165 | * @param value New value to set 166 | */ 167 | static async setActiveUsers(value: number): Promise { 168 | return this.set('activeUsers', value); 169 | } 170 | 171 | /** 172 | * Increment total file uploads count 173 | * @param count Amount to increment (default: 1) 174 | */ 175 | static async incrementTotalFileUploads(count = 1): Promise { 176 | return this.increment('totalFileUploads', count); 177 | } 178 | 179 | /** 180 | * Set total file uploads count 181 | * @param value New value to set 182 | */ 183 | static async setTotalFileUploads(value: number): Promise { 184 | return this.set('totalFileUploads', value); 185 | } 186 | 187 | /** 188 | * Increment image uploads count 189 | * @param count Amount to increment (default: 1) 190 | */ 191 | static async incrementImageUploads(count = 1): Promise { 192 | return this.incrementFileType('images', count); 193 | } 194 | 195 | /** 196 | * Increment video uploads count 197 | * @param count Amount to increment (default: 1) 198 | */ 199 | static async incrementVideoUploads(count = 1): Promise { 200 | return this.incrementFileType('videos', count); 201 | } 202 | 203 | /** 204 | * Increment gif uploads count 205 | * @param count Amount to increment (default: 1) 206 | */ 207 | static async incrementGifUploads(count = 1): Promise { 208 | return this.incrementFileType('gifs', count); 209 | } 210 | 211 | /** 212 | * Increment PDF uploads count 213 | * @param count Amount to increment (default: 1) 214 | */ 215 | static async incrementPdfUploads(count = 1): Promise { 216 | return this.incrementFileType('pdfs', count); 217 | } 218 | 219 | /** 220 | * Increment other file type uploads count 221 | * @param count Amount to increment (default: 1) 222 | */ 223 | static async incrementOtherFileUploads(count = 1): Promise { 224 | return this.incrementFileType('others', count); 225 | } 226 | 227 | /** 228 | * Get total servers count 229 | */ 230 | static async getTotalServers(): Promise { 231 | const result = await this.getStats(); 232 | return result.success ? result.data?.totalServers ?? null : null; 233 | } 234 | 235 | /** 236 | * Get total messages count 237 | */ 238 | static async getTotalMessages(): Promise { 239 | const result = await this.getStats(); 240 | return result.success ? result.data?.totalMessages ?? null : null; 241 | } 242 | 243 | /** 244 | * Get total users count 245 | */ 246 | static async getTotalUsers(): Promise { 247 | const result = await this.getStats(); 248 | return result.success ? result.data?.totalUsers ?? null : null; 249 | } 250 | 251 | /** 252 | * Get active servers count 253 | */ 254 | static async getActiveServers(): Promise { 255 | const result = await this.getStats(); 256 | return result.success ? result.data?.activeServers ?? null : null; 257 | } 258 | 259 | /** 260 | * Get active users count 261 | */ 262 | static async getActiveUsers(): Promise { 263 | const result = await this.getStats(); 264 | return result.success ? result.data?.activeUsers ?? null : null; 265 | } 266 | 267 | /** 268 | * Get total file uploads count 269 | */ 270 | static async getTotalFileUploads(): Promise { 271 | const result = await this.getStats(); 272 | return result.success ? result.data?.totalFileUploads ?? null : null; 273 | } 274 | 275 | /** 276 | * Get file type statistics 277 | */ 278 | static async getFileTypeStats(): Promise { 279 | const result = await this.getStats(); 280 | return result.success ? result.data?.fileTypes ?? null : null; 281 | } 282 | 283 | /** 284 | * Get a specific file type count 285 | * @param fileType The file type to get count for 286 | */ 287 | static async getFileTypeCount(fileType: FileTypeKey): Promise { 288 | const result = await this.getStats(); 289 | return result.success ? result.data?.fileTypes?.[fileType] ?? null : null; 290 | } 291 | 292 | /** 293 | * Update statistics for active usage 294 | * Used for tracking currently active users and servers 295 | * @param activeUsers Number of currently active users 296 | * @param activeServers Number of currently active servers 297 | */ 298 | static async updateActiveStats(activeUsers: number, activeServers: number): Promise { 299 | return this.batchUpdate({ 300 | activeUsers, 301 | activeServers 302 | }); 303 | } 304 | 305 | /** 306 | * Record a file upload with automatic type detection 307 | * @param fileType The type of file being uploaded 308 | */ 309 | static async recordFileUpload(fileType: FileTypeKey): Promise { 310 | // Increment both the specific file type and total file uploads 311 | return this.incrementFileType(fileType); 312 | } 313 | 314 | /** 315 | * Get the last time statistics were updated 316 | */ 317 | static async getLastUpdated(): Promise { 318 | const result = await this.getStats(); 319 | return result.success ? result.data?.lastUpdated ?? null : null; 320 | } 321 | /** 322 | * Increment a top-level statistic value 323 | * @param key The statistic key to increment 324 | * @param count The amount to increment by (default: 1) 325 | * @returns Promise with the operation result 326 | */ 327 | static async increment(key: StatKey, count = 1): Promise { 328 | try { 329 | // Validate increment amount 330 | if (isNaN(count) || count <= 0) { 331 | return { 332 | success: false, 333 | error: new Error(`Invalid increment value for "${key}": ${count}`) 334 | }; 335 | } 336 | 337 | const stats = await AppStatistics.findOneAndUpdate( 338 | { id: STATS_ID }, 339 | { 340 | $inc: { [key]: count }, 341 | $set: { lastUpdated: new Date() } 342 | }, 343 | { upsert: true, new: true } 344 | ); 345 | 346 | return { success: true, data: stats }; 347 | } catch (err) { 348 | console.error(`Failed to increment stat "${key}":`, err); 349 | return { success: false, error: err instanceof Error ? err : new Error(String(err)) }; 350 | } 351 | } 352 | 353 | /** 354 | * Increment a file type statistic 355 | * @param fileType The file type to increment 356 | * @param count The amount to increment by (default: 1) 357 | * @returns Promise with the operation result 358 | */ 359 | static async incrementFileType(fileType: FileTypeKey | string, count = 1): Promise { 360 | try { 361 | // Validate increment amount 362 | if (isNaN(count)) { 363 | return { 364 | success: false, 365 | error: new Error(`Invalid increment value for file type "${fileType}": ${count}`) 366 | }; 367 | } 368 | 369 | const stats = await AppStatistics.findOneAndUpdate( 370 | { id: STATS_ID }, 371 | { 372 | $inc: { [`fileTypes.${fileType}`]: count }, 373 | $set: { lastUpdated: new Date() } 374 | }, 375 | { upsert: true, new: true } 376 | ); 377 | 378 | return { success: true, data: stats }; 379 | } catch (err) { 380 | console.error(`Failed to increment file type "${fileType}":`, err); 381 | return { success: false, error: err instanceof Error ? err : new Error(String(err)) }; 382 | } 383 | } 384 | 385 | /** 386 | * Set a statistic to a specific value 387 | * @param key The statistic key to set 388 | * @param value The value to set 389 | * @returns Promise with the operation result 390 | */ 391 | static async set(key: StatKey, value: number): Promise { 392 | try { 393 | const stats = await AppStatistics.findOneAndUpdate( 394 | { id: STATS_ID }, 395 | { 396 | $set: { 397 | [key]: value, 398 | lastUpdated: new Date() 399 | } 400 | }, 401 | { upsert: true, new: true } 402 | ); 403 | 404 | return { success: true, data: stats }; 405 | } catch (err) { 406 | console.error(`Failed to set stat "${key}":`, err); 407 | return { success: false, error: err instanceof Error ? err : new Error(String(err)) }; 408 | } 409 | } 410 | 411 | /** 412 | * Get all statistics or filter by specific keys 413 | * @param filter Optional filter to apply to the query 414 | * @returns Promise with the operation result containing stats 415 | */ 416 | static async getStats(filter?: FilterQuery): Promise { 417 | try { 418 | const query = filter ? { id: STATS_ID, ...filter } : { id: STATS_ID }; 419 | const stats = await AppStatistics.findOne(query).lean(); 420 | 421 | return { success: true, data: stats }; 422 | } catch (err) { 423 | console.error('Failed to fetch stats:', err); 424 | return { success: false, error: err instanceof Error ? err : new Error(String(err)) }; 425 | } 426 | } 427 | 428 | /** 429 | * Reset statistics to initial values 430 | * @returns Promise with the operation result 431 | */ 432 | static async resetStats(): Promise { 433 | try { 434 | const stats = await AppStatistics.findOneAndUpdate( 435 | { id: STATS_ID }, 436 | { 437 | $set: { 438 | ...initialStats, 439 | lastUpdated: new Date() 440 | } 441 | }, 442 | { upsert: true, new: true } 443 | ); 444 | 445 | return { success: true, data: stats }; 446 | } catch (err) { 447 | console.error('Failed to reset stats:', err); 448 | return { success: false, error: err instanceof Error ? err : new Error(String(err)) }; 449 | } 450 | } 451 | 452 | /** 453 | * Batch update multiple statistics at once 454 | * @param updates Object containing stat keys and their new values 455 | * @returns Promise with the operation result 456 | */ 457 | static async batchUpdate(updates: Partial>): Promise { 458 | try { 459 | const updateObj: Record = {}; 460 | 461 | // Process each update 462 | Object.entries(updates).forEach(([key, value]) => { 463 | updateObj[key] = value; 464 | }); 465 | 466 | const stats = await AppStatistics.findOneAndUpdate( 467 | { id: STATS_ID }, 468 | { 469 | $set: { 470 | ...updateObj, 471 | lastUpdated: new Date() 472 | } 473 | }, 474 | { upsert: true, new: true } 475 | ); 476 | 477 | return { success: true, data: stats }; 478 | } catch (err) { 479 | console.error('Failed to batch update stats:', err); 480 | return { success: false, error: err instanceof Error ? err : new Error(String(err)) }; 481 | } 482 | } 483 | 484 | static async updatePreviousStats(): Promise { 485 | try { 486 | const currentStats = await AppStatistics.findOne({ id: STATS_ID }); 487 | 488 | if (!currentStats) { 489 | const stats = await AppStatistics.findOneAndUpdate( 490 | { id: STATS_ID }, 491 | { 492 | $set: { 493 | previousStats: [initialStats], 494 | lastUpdated: new Date() 495 | } 496 | }, 497 | { upsert: true, new: true } 498 | ); 499 | return { success: true, data: stats }; 500 | } 501 | 502 | const today = new Date(); 503 | const lastEntry = currentStats.previousStats[currentStats.previousStats.length - 1]; 504 | const isNotToday = lastEntry.recordedOn.getDate() !== today.getDate() || 505 | lastEntry.recordedOn.getMonth() !== today.getMonth() || 506 | lastEntry.recordedOn.getFullYear() !== today.getFullYear(); 507 | 508 | 509 | if (isNotToday) { 510 | const stats = await AppStatistics.findOneAndUpdate( 511 | { id: STATS_ID }, 512 | { 513 | $set: { 514 | previousStats: [...currentStats.previousStats, { 515 | totalServers: currentStats.totalServers, 516 | totalMessages: currentStats.totalMessages, 517 | totalUsers: currentStats.totalUsers, 518 | activeServers: currentStats.activeServers, 519 | activeUsers: currentStats.activeUsers, 520 | totalFileUploads: currentStats.totalFileUploads, 521 | deletedServers: currentStats.deletedServers, 522 | expiredServers: currentStats.expiredServers, 523 | fileTypes: currentStats.fileTypes, 524 | }], 525 | lastUpdated: today 526 | } 527 | }, 528 | { new: true } 529 | ); 530 | return { success: true, data: stats }; 531 | } 532 | 533 | return { success: true, data: currentStats }; 534 | } catch (err) { 535 | console.error('Failed to update previous stats:', err); 536 | return { success: false, error: err instanceof Error ? err : new Error(String(err)) }; 537 | } 538 | } 539 | } 540 | 541 | StatisticsController.initialize(); -------------------------------------------------------------------------------- /src/controllers/users.controller.ts: -------------------------------------------------------------------------------- 1 | import { Server } from '../models/server.model'; 2 | import { User } from '../models/user.model'; 3 | import { generateUsername } from '../utils/user.util'; 4 | import { StatisticsController } from './statistics.controller'; 5 | 6 | interface ServerUser { 7 | userId: string; 8 | isOnline: boolean; 9 | lastSeen: Date; 10 | bgColor: string; 11 | textColor: string; 12 | } 13 | 14 | class UserController { 15 | private static instance: UserController; 16 | 17 | private constructor() {} 18 | 19 | public static getInstance(): UserController { 20 | if (!UserController.instance) { 21 | UserController.instance = new UserController(); 22 | } 23 | return UserController.instance; 24 | } 25 | 26 | public async addUserToServer(serverId: string, user: Omit): Promise { 27 | const server = await Server.findOne({ serverId }); 28 | if (!server) { 29 | throw new Error(`Server ${serverId} not found`); 30 | } 31 | 32 | let dbUser = await User.findOne({ userId: user.userId }); 33 | if (!dbUser) { 34 | dbUser = await User.create({ 35 | userId: user.userId, 36 | isOnline: true, 37 | currentServer: serverId 38 | }); 39 | 40 | await StatisticsController.incrementTotalUsers(); 41 | await StatisticsController.incrementActiveUsers(); 42 | } else { 43 | await StatisticsController.incrementActiveUsers(); 44 | await User.updateOne( 45 | { userId: user.userId }, 46 | { 47 | isOnline: true, 48 | currentServer: serverId, 49 | lastSeen: new Date() 50 | } 51 | ); 52 | } 53 | 54 | if (!server.approvedUsers.includes(dbUser.userId)) { 55 | server.approvedUsers.push(dbUser.userId); 56 | await server.save(); 57 | } 58 | 59 | const username = generateUsername(`${dbUser.userId}-${serverId}`); 60 | 61 | } 62 | 63 | public async removeUserFromServer(serverId: string, userId: string): Promise { 64 | const server = await Server.findOne({ serverId }); 65 | if (!server) return; 66 | 67 | server.approvedUsers = server.approvedUsers.filter(id => id !== userId); 68 | await server.save(); 69 | 70 | // Update user status 71 | await User.updateOne( 72 | { userId }, 73 | { 74 | isOnline: false, 75 | currentServer: null, 76 | lastSeen: new Date() 77 | } 78 | ); 79 | 80 | await StatisticsController.decrementActiveUsers(); 81 | } 82 | 83 | public async getActiveUsers(serverId: string): Promise { 84 | const server = await Server.findOne({ serverId }); 85 | if (!server) return []; 86 | 87 | const users = await User.find({ userId: { $in: server.allUsers } }); 88 | const approvedUsers = await User.find({ userId: { $in: server.approvedUsers } }); 89 | return users.map(user => { 90 | if(!approvedUsers.find(u => u.userId === user.userId)) return({ 91 | userId: user.userId, 92 | username: generateUsername(`${user.userId}-${serverId}`), 93 | isOnline: false, 94 | lastSeen: user.lastSeen, 95 | bgColor: user.bgColor, 96 | textColor: user.textColor 97 | }); 98 | 99 | return ({ 100 | userId: user.userId, 101 | username: generateUsername(`${user.userId}-${serverId}`), 102 | isOnline: user.isOnline, 103 | lastSeen: user.lastSeen, 104 | bgColor: user.bgColor, 105 | textColor: user.textColor 106 | }) 107 | }); 108 | } 109 | 110 | public async clearServerUsers(serverId: string): Promise { 111 | const server = await Server.findOne({ serverId }); 112 | if (!server) return; 113 | 114 | // Update all users in this server to offline 115 | await User.updateMany( 116 | { userId: { $in: server.approvedUsers } }, 117 | { 118 | isOnline: false, 119 | currentServer: null, 120 | lastSeen: new Date() 121 | } 122 | ); 123 | 124 | await StatisticsController.decrementActiveUsers(server.approvedUsers.length); 125 | 126 | server.approvedUsers = []; 127 | await server.save(); 128 | } 129 | 130 | public async isUserInServer(serverId: string, userId: string): Promise { 131 | const server = await Server.findOne({ serverId }); 132 | if (!server) return false; 133 | 134 | return server.approvedUsers.includes(userId); 135 | } 136 | 137 | public async setUserOnlineStatus(userId: string, isOnline: boolean, serverId?: string): Promise { 138 | await User.updateOne( 139 | { userId }, 140 | { 141 | isOnline, 142 | currentServer: isOnline ? serverId : null, 143 | lastSeen: new Date() 144 | } 145 | ); 146 | } 147 | } 148 | 149 | // Export singleton instance methods 150 | const userController = UserController.getInstance(); 151 | 152 | export const addUserToServer = (serverId: string, user: Omit): Promise => 153 | userController.addUserToServer(serverId, user); 154 | 155 | export const removeUserFromServer = (serverId: string, userId: string): Promise => 156 | userController.removeUserFromServer(serverId, userId); 157 | 158 | export const getActiveUsers = (serverId: string): Promise => 159 | userController.getActiveUsers(serverId); 160 | 161 | export const clearServerUsers = (serverId: string): Promise => 162 | userController.clearServerUsers(serverId); 163 | 164 | export const isUserInServer = (serverId: string, userId: string): Promise => 165 | userController.isUserInServer(serverId, userId); 166 | 167 | export const setUserOnlineStatus = (userId: string, isOnline: boolean, serverId?: string): Promise => 168 | userController.setUserOnlineStatus(userId, isOnline, serverId); 169 | -------------------------------------------------------------------------------- /src/middleware/auth.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | import jwt from "jsonwebtoken"; 3 | import { config } from "../config/dotenv.config"; 4 | import AppError from "../types/error.class"; 5 | import { Server } from "../models/server.model"; 6 | 7 | export interface JWTPayload { 8 | userId: string; 9 | serverId: string; 10 | } 11 | 12 | export interface AuthRequest extends Request { 13 | apiKey?: string; 14 | user?: JWTPayload; 15 | } 16 | 17 | class AuthMiddleware { 18 | private static instance: AuthMiddleware; 19 | private readonly API_KEY_HEADER = "X-API-Key"; 20 | 21 | private constructor() { } 22 | 23 | public static getInstance(): AuthMiddleware { 24 | if (!AuthMiddleware.instance) { 25 | AuthMiddleware.instance = new AuthMiddleware(); 26 | } 27 | return AuthMiddleware.instance; 28 | } 29 | 30 | private validateApiKey(apiKey: string | undefined): string { 31 | if (!apiKey) { 32 | throw new AppError(401, "Access denied. No API key provided."); 33 | } 34 | 35 | if (apiKey !== config.API_KEY) { 36 | throw new AppError(401, "Invalid API key."); 37 | } 38 | 39 | return apiKey; 40 | } 41 | 42 | private extractToken(token: string | undefined): string { 43 | if (!token) { 44 | throw new AppError(401, "Access denied. No token provided."); 45 | } 46 | 47 | return token; 48 | } 49 | 50 | private verifyToken(token: string): JWTPayload { 51 | try { 52 | return jwt.verify(token, config.JWT_SECRET) as JWTPayload; 53 | } catch (error) { 54 | if (error instanceof jwt.TokenExpiredError) { 55 | throw new AppError(401, "Token has expired."); 56 | } 57 | if (error instanceof jwt.JsonWebTokenError) { 58 | throw new AppError(401, "Invalid token."); 59 | } 60 | throw new AppError(401, "Token verification failed."); 61 | } 62 | } 63 | 64 | private async validateServerAccess(payload: JWTPayload): Promise { 65 | const server = await Server.findOne({ serverId: payload.serverId }); 66 | if (!server) { 67 | throw new AppError(404, "Server not found"); 68 | } 69 | 70 | if (server.expiresAt < new Date() && server.type === "Private") { 71 | throw new AppError(410, "Server has expired 01"); 72 | } 73 | } 74 | 75 | public shouldRefreshToken(decoded: JWTPayload): boolean { 76 | const exp = (decoded as any).exp; 77 | if (!exp) return false; 78 | 79 | const currentTime = Math.floor(Date.now() / 1000); 80 | const timeUntilExpiry = exp - currentTime; 81 | 82 | return timeUntilExpiry < 3600; 83 | } 84 | 85 | public authenticate = async (req: AuthRequest, res: Response, next: NextFunction): Promise => { 86 | try { 87 | // Always validate API key 88 | const apiKey = this.validateApiKey(req.header(this.API_KEY_HEADER)); 89 | req.apiKey = apiKey; 90 | next(); 91 | } catch (error) { 92 | if (error instanceof AppError) { 93 | res.status(error.statusCode).json({ message: error.message }); 94 | } else { 95 | res.status(500).json({ message: "Internal server error during authentication." }); 96 | } 97 | } 98 | } 99 | 100 | public authenticateWithJWT = async (req: AuthRequest, res: Response, next: NextFunction): Promise => { 101 | try { 102 | // First validate API key 103 | const apiKey = this.validateApiKey(req.header(this.API_KEY_HEADER)); 104 | 105 | req.apiKey = apiKey; 106 | 107 | // Then validate JWT token 108 | const rawToken = req.header("Authorization"); 109 | const token = this.extractToken(rawToken); 110 | const decoded = this.verifyToken(token); 111 | 112 | 113 | // Validate server access 114 | await this.validateServerAccess(decoded); 115 | 116 | // Check if token needs refresh 117 | if (this.shouldRefreshToken(decoded)) { 118 | const newToken = this.generateToken(decoded); 119 | res.setHeader("X-Refresh-Token", newToken); 120 | } 121 | 122 | req.user = decoded; 123 | 124 | next(); 125 | } catch (error) { 126 | 127 | if (error instanceof AppError) { 128 | res.status(error.statusCode).json({ message: error.message }); 129 | } else { 130 | res.status(500).json({ message: "Internal server error during authentication." }); 131 | } 132 | } 133 | } 134 | 135 | public generateToken(payload: JWTPayload): string { 136 | return jwt.sign(payload, config.JWT_SECRET, { 137 | expiresIn: "24h" // Token expires in 24 hours 138 | }); 139 | } 140 | } 141 | 142 | // Export singleton instance method 143 | export const authMiddleware = AuthMiddleware.getInstance(); 144 | 145 | // Export authenticate middleware 146 | export const authenticateUser = authMiddleware.authenticate; 147 | 148 | // Export authenticateWithJWT middleware 149 | export const authenticateUserWithJWT = authMiddleware.authenticateWithJWT; 150 | 151 | // Export token generator 152 | export const generateToken = (payload: JWTPayload): string => 153 | authMiddleware.generateToken(payload); -------------------------------------------------------------------------------- /src/middleware/errorHandler.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import AppError from '../types/error.class'; 3 | 4 | const errorHandler = (err: AppError, req: Request, res: Response, next: NextFunction) => { 5 | console.error(err.stack); 6 | res.status(err.statusCode).json({ 7 | message: err.message, 8 | statusCode: err.statusCode 9 | }); 10 | }; 11 | 12 | export default errorHandler; -------------------------------------------------------------------------------- /src/models/invitation.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const invitationSchema = new mongoose.Schema({ 4 | inviteCode: { type: String, required: true, unique: true }, 5 | used: { type: Boolean, default: false }, 6 | serverId: { type: String, required: true, ref: "Server" }, 7 | createdAt: { type: Date, default: Date.now }, 8 | expiresAt: { type: Date, required: true } 9 | }); 10 | 11 | const Invitation = mongoose.model("Invitation", invitationSchema); 12 | export default Invitation; -------------------------------------------------------------------------------- /src/models/messages.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import { Server } from "./server.model"; 3 | import { encryptMessage } from "../utils/messages.utli"; 4 | import { generateId } from "../utils/index.util"; 5 | 6 | const messageSchema = new mongoose.Schema({ 7 | serverId: { type: String, required: true, ref: "Server" }, 8 | senderId: { type: String, required: true, ref: "User" }, 9 | receiverId: { type: String, required: true, ref: "User" }, 10 | messageId: { type: String, required: true, unique: true, default: () => `txt-${generateId()}` }, 11 | content: { type: String, required: true }, 12 | attachmentUrl: { type: String, default: null }, 13 | sent: { type: Boolean, default: false }, 14 | createdAt: { type: Date, default: Date.now }, 15 | readBySender: { type: Boolean, default: true }, 16 | readByReceiver: { type: Boolean, default: false } 17 | }); 18 | 19 | // Encrypt message before saving 20 | messageSchema.pre("save", async function (next) { 21 | if (!this.serverId) return next(new Error("Server ID is required")); 22 | if (this.readByReceiver) return next(); 23 | 24 | const message = this; 25 | const server = await Server.findOne({ serverId: message.serverId }); 26 | if (!server) return next(new Error("Server not found")); 27 | 28 | const encryptedContent = encryptMessage(message.content, server.salt); 29 | message.content = encryptedContent; 30 | 31 | if (message.attachmentUrl) { 32 | const encryptedAttachmentUrl = encryptMessage(message.attachmentUrl, server.salt); 33 | message.attachmentUrl = encryptedAttachmentUrl; 34 | } 35 | 36 | next(); 37 | }); 38 | 39 | export const Message = mongoose.model("Message", messageSchema); -------------------------------------------------------------------------------- /src/models/server.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const serverSchema = new mongoose.Schema({ 4 | serverId: { type: String, required: true, unique: true }, 5 | owner: { type: String, required: true }, 6 | serverName: { type: String, required: true }, 7 | salt: { type: String, required: true }, 8 | globalInvitationId: { type: String, required: true, unique: true }, 9 | expiresAt: { type: Date, required: true }, 10 | createdAt: { type: Date, default: Date.now }, 11 | approvedUsers: [{ type: String, ref: 'User' }], 12 | type: { type: String, enum: ['Public', 'Private'], default: 'Public' }, 13 | allUsers: [{ type: String, ref: 'User' }] 14 | }); 15 | 16 | // Add this before the model export 17 | serverSchema.pre('save', function (next) { 18 | // Get all approved users 19 | const approvedUsers = this.approvedUsers || []; 20 | 21 | // Initialize all users if it doesn't exist 22 | if (!this.allUsers) { 23 | this.allUsers = []; 24 | } 25 | 26 | // Add any approved users that aren't already in allUsers 27 | approvedUsers.forEach(userId => { 28 | if (!this.allUsers.includes(userId)) { 29 | this.allUsers.push(userId); 30 | } 31 | }); 32 | 33 | next(); 34 | }); 35 | 36 | export const Server = mongoose.model("Server", serverSchema); -------------------------------------------------------------------------------- /src/models/statistics.model.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model, Document } from 'mongoose'; 2 | import { config } from '../config/dotenv.config'; 3 | 4 | interface previousStats extends Omit { 5 | recordedOn: Date; 6 | } 7 | 8 | export interface IAppStatistics extends Document { 9 | id: string; 10 | totalServers: number; 11 | totalMessages: number; 12 | totalUsers: number; 13 | activeServers: number; 14 | activeUsers: number; 15 | totalFileUploads: number; 16 | deletedServers: number; 17 | expiredServers: number; 18 | fileTypes: { 19 | images: number; 20 | videos: number; 21 | gifs: number; 22 | pdfs: number; 23 | others?: number; 24 | }; 25 | lastUpdated: Date; 26 | previousStats: previousStats[]; 27 | } 28 | 29 | const AppStatisticsSchema = new Schema({ 30 | id: { type: String, default: config.STATS_ID }, 31 | totalServers: { type: Number, default: 0 }, 32 | totalMessages: { type: Number, default: 0 }, 33 | totalUsers: { type: Number, default: 0 }, 34 | activeServers: { type: Number, default: 0 }, 35 | activeUsers: { type: Number, default: 0 }, 36 | totalFileUploads: { type: Number, default: 0 }, 37 | deletedServers: { type: Number, default: 0 }, 38 | expiredServers: { type: Number, default: 0 }, 39 | fileTypes: { 40 | images: { type: Number, default: 0 }, 41 | videos: { type: Number, default: 0 }, 42 | gifs: { type: Number, default: 0 }, 43 | pdfs: { type: Number, default: 0 }, 44 | others: { type: Number, default: 0 }, 45 | }, 46 | lastUpdated: { type: Date, default: Date.now }, 47 | previousStats: [{ 48 | totalServers: { type: Number, default: 0 }, 49 | totalMessages: { type: Number, default: 0 }, 50 | totalUsers: { type: Number, default: 0 }, 51 | activeServers: { type: Number, default: 0 }, 52 | activeUsers: { type: Number, default: 0 }, 53 | totalFileUploads: { type: Number, default: 0 }, 54 | deletedServers: { type: Number, default: 0 }, 55 | expiredServers: { type: Number, default: 0 }, 56 | fileTypes: { 57 | images: { type: Number, default: 0 }, 58 | videos: { type: Number, default: 0 }, 59 | gifs: { type: Number, default: 0 }, 60 | pdfs: { type: Number, default: 0 }, 61 | others: { type: Number, default: 0 }, 62 | }, 63 | recordedOn: { type: Date, default: Date.now } 64 | }] 65 | }); 66 | 67 | export const AppStatistics = model('AppStatistics', AppStatisticsSchema); -------------------------------------------------------------------------------- /src/models/user.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import { generateVibrantBgColorWithTextVisibility } from "../utils/index.util"; 3 | 4 | const userSchema = new mongoose.Schema({ 5 | userId: { type: String, required: true, unique: true, index: true }, 6 | isOnline: { type: Boolean, default: false }, 7 | lastSeen: { type: Date, default: Date.now }, 8 | currentServer: { type: String, default: null, ref: "Server" }, 9 | createdAt: { type: Date, default: Date.now }, 10 | bgColor: { type: String, default: null }, 11 | textColor: { type: String, default: null }, 12 | typing: { type: Boolean, default: false }, 13 | typingTo: { type: String, default: null }, 14 | }); 15 | 16 | // Pre-save hook to set background and text colors if not already set 17 | userSchema.pre('save', function (next) { 18 | // Only set colors for new documents (when they don't have an _id yet) 19 | if (this.isNew && (!this.bgColor || !this.textColor)) { 20 | const { backgroundColor, isWhiteTextVisible } = generateVibrantBgColorWithTextVisibility(); 21 | this.bgColor = backgroundColor; 22 | this.textColor = isWhiteTextVisible ? '#FFFFFF' : '#000000'; 23 | } 24 | next(); 25 | }); 26 | 27 | export const User = mongoose.model("User", userSchema); 28 | -------------------------------------------------------------------------------- /src/routes/auth.routes.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { authenticateUser } from "../middleware/auth"; 3 | import { refreshToken } from "../controllers/auth.controller"; 4 | 5 | const authRouter = express.Router(); 6 | 7 | // Add refresh token endpoint 8 | authRouter.post("/refresh-token", authenticateUser, refreshToken); 9 | 10 | export default authRouter; 11 | -------------------------------------------------------------------------------- /src/routes/messages.route.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import messagesController from "../controllers/messages.controller"; 3 | import { authenticateUserWithJWT } from "../middleware/auth"; 4 | 5 | const messagesRouter = express.Router(); 6 | 7 | // Private route with JWT 8 | messagesRouter.get("/:serverId", authenticateUserWithJWT, messagesController.getMessages); 9 | 10 | export default messagesRouter; 11 | -------------------------------------------------------------------------------- /src/routes/server.routes.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { CreateServer, DeleteServer, GenerateUniqueServerInvitationId, GetServer, GetServerActiveUsers, GlobalInvitation, UniqueInvitation } from "../controllers/server.controller"; 3 | import { authenticateUser, authenticateUserWithJWT } from "../middleware/auth"; 4 | import messagesController from "../controllers/messages.controller"; 5 | 6 | const router = express.Router(); 7 | 8 | // Public route with API key only 9 | router.post("/", authenticateUser, CreateServer); 10 | router.post("/invitation/:globalInvitationId", authenticateUser, GlobalInvitation); 11 | router.post("/unique-invitation/:inviteCode", authenticateUser, UniqueInvitation); 12 | 13 | // Private route with JWT 14 | router.get("/:serverId", authenticateUserWithJWT, GetServer); 15 | router.get("/:serverId/active-users", authenticateUserWithJWT, GetServerActiveUsers); 16 | router.get("/:serverId/generate-unique-server-invitation-id", authenticateUserWithJWT, GenerateUniqueServerInvitationId); 17 | router.get("/:serverId/messages", authenticateUserWithJWT, messagesController.getMessages); 18 | router.delete("/:serverId", authenticateUserWithJWT, DeleteServer); 19 | export default router; -------------------------------------------------------------------------------- /src/sockets/index.socket.ts: -------------------------------------------------------------------------------- 1 | import { Server as SocketServer, Socket } from 'socket.io'; 2 | import { Server } from '../models/server.model'; 3 | import { io } from '../../server'; 4 | import { ServerJoinPayload, ServerMessage, ServerAction, UserStatusUpdate } from '../types/server-socket.interface'; 5 | import { getActiveUsers, setUserOnlineStatus } from '../controllers/users.controller'; 6 | import messagesController from '../controllers/messages.controller'; 7 | import { Message } from '../types/message.interface'; 8 | import { LeaveServer } from '../controllers/server.controller'; 9 | import { User } from '../models/user.model'; 10 | 11 | class ServerSocketService { 12 | private static instance: ServerSocketService; 13 | private io: SocketServer; 14 | private userSocketMap: Map>; // userId -> Set of socket IDs 15 | private serverUsersMap: Map>; // serverId -> Set of userIds 16 | 17 | private constructor() { 18 | this.io = io; 19 | this.userSocketMap = new Map(); 20 | this.serverUsersMap = new Map(); 21 | this.setupSocketHandlers(); 22 | } 23 | 24 | public static getInstance(): ServerSocketService { 25 | if (!ServerSocketService.instance) { 26 | ServerSocketService.instance = new ServerSocketService(); 27 | } 28 | return ServerSocketService.instance; 29 | } 30 | 31 | private async validateServerAccess(serverId: string): Promise { 32 | try { 33 | const server = await Server.findOne({ serverId }); 34 | if (!server) return false; 35 | if (server.expiresAt < new Date() && server.type === "Private") return false; 36 | return true; 37 | } catch (error) { 38 | console.error('Error validating server access:', error); 39 | return false; 40 | } 41 | } 42 | 43 | private async broadcastActiveUsers(serverId: string): Promise { 44 | try { 45 | const activeUsers = await getActiveUsers(serverId); 46 | this.io.to(serverId).emit('active_users_updated', activeUsers); 47 | } catch (error) { 48 | console.error('Error broadcasting active users:', error); 49 | } 50 | } 51 | 52 | private async broadcastUserStatus(update: UserStatusUpdate): Promise { 53 | try { 54 | // First update the user's status in the database 55 | await setUserOnlineStatus(update.userId, update.isOnline, update.serverId); 56 | 57 | // Then broadcast the updated user list to all clients in the server 58 | await this.broadcastActiveUsers(update.serverId); 59 | 60 | // Also emit a specific status update event for real-time UI updates 61 | this.io.to(update.serverId).emit('user_status_changed', update); 62 | 63 | // Emit typing status 64 | await this.broadcastUserTypingStatus(update.serverId, update.userId, false, ""); 65 | } catch (error) { 66 | console.error('Error broadcasting user status:', error); 67 | } 68 | } 69 | 70 | private trackUserSocket(userId: string, socketId: string, serverId: string): void { 71 | // Track socket for user 72 | if (!this.userSocketMap.has(userId)) { 73 | this.userSocketMap.set(userId, new Set()); 74 | } 75 | this.userSocketMap.get(userId)!.add(socketId); 76 | 77 | // Track user for server 78 | if (!this.serverUsersMap.has(serverId)) { 79 | this.serverUsersMap.set(serverId, new Set()); 80 | } 81 | this.serverUsersMap.get(serverId)!.add(userId); 82 | } 83 | 84 | private async untrackUserSocket(userId: string, socketId: string): Promise { 85 | const userSockets = this.userSocketMap.get(userId); 86 | if (userSockets) { 87 | userSockets.delete(socketId); 88 | if (userSockets.size === 0) { 89 | this.userSocketMap.delete(userId); 90 | 91 | // Find which server this user was in 92 | for (const [serverId, users] of this.serverUsersMap) { 93 | if (users.has(userId)) { 94 | users.delete(userId); 95 | // Broadcast status update 96 | await this.broadcastUserStatus({ 97 | userId, 98 | isOnline: false, 99 | serverId 100 | }); 101 | break; 102 | } 103 | } 104 | } 105 | } 106 | } 107 | 108 | private setupSocketHandlers(): void { 109 | this.io.on('connection', (socket: Socket) => { 110 | let currentServerId: string | null = null; 111 | let currentUserId: string | null = null; 112 | 113 | socket.on('join_server', async (payload: ServerJoinPayload) => { 114 | try { 115 | const { serverId, userId } = payload; 116 | currentServerId = serverId; 117 | currentUserId = userId; 118 | 119 | const hasAccess = await this.validateServerAccess(serverId); 120 | if (!hasAccess) { 121 | socket.emit('server_error', { 122 | type: 'error', 123 | content: 'Invalid server credentials or server has expired', 124 | timestamp: Date.now() 125 | }); 126 | return; 127 | } 128 | 129 | // Leave all other rooms first 130 | socket.rooms.forEach(room => { 131 | if (room !== socket.id) { 132 | socket.leave(room); 133 | } 134 | }); 135 | 136 | // Track user and join server room 137 | if (userId) { 138 | this.trackUserSocket(userId, socket.id, serverId); 139 | socket.join(serverId); 140 | 141 | // Broadcast user's online status 142 | await this.broadcastUserStatus({ 143 | userId, 144 | isOnline: true, 145 | serverId 146 | }); 147 | } 148 | 149 | socket.emit('server_joined', { 150 | type: 'status', 151 | content: 'Successfully joined the server', 152 | timestamp: Date.now() 153 | }); 154 | 155 | } catch (error) { 156 | console.error('Error during join process:', error); 157 | socket.emit('server_error', { 158 | type: 'error', 159 | content: 'Failed to join server', 160 | timestamp: Date.now() 161 | }); 162 | } 163 | }); 164 | 165 | socket.on('new-message', async (serverId: string, message: Message) => { 166 | await messagesController.sendMessage(serverId, message.content, message.senderId, message.receiverId, message.attachmentUrl); 167 | }); 168 | 169 | socket.on('mark_message_read', async (messageId: string) => { 170 | await messagesController.markMessageRead(messageId); 171 | }); 172 | 173 | socket.on('leave_server', async (serverId: string) => { 174 | socket.leave(serverId); 175 | try { 176 | if (currentUserId) { 177 | await this.untrackUserSocket(currentUserId, socket.id); 178 | await LeaveServer(serverId, currentUserId); 179 | } 180 | } catch (error) { 181 | console.error('Error during leave process:', error); 182 | } 183 | currentServerId = null; 184 | currentUserId = null; 185 | 186 | // emit typing status 187 | await this.broadcastUserTypingStatus(serverId, serverId, false, ""); 188 | 189 | socket.emit('server_left', { 190 | type: 'status', 191 | content: 'Left the server', 192 | timestamp: Date.now() 193 | }); 194 | }); 195 | 196 | // user is typing 197 | socket.on('typing', async (serverId: string, receiverId: string, userId: string) => { 198 | await User.updateOne({ userId }, { typing: true, typingTo: receiverId }); 199 | await this.broadcastUserTypingStatus(serverId, userId, true, receiverId); 200 | }); 201 | 202 | // user is not typing 203 | socket.on('not_typing', async (serverId: string, userId: string) => { 204 | const user = await User.findOne({ userId }); 205 | if (!user) { 206 | return; 207 | } 208 | 209 | user.typing = false; 210 | const typingTo = user.typingTo; 211 | user.typingTo = ""; 212 | await user.save(); 213 | await this.broadcastUserTypingStatus(serverId, userId, false, typingTo); 214 | }); 215 | 216 | // user disconnect 217 | socket.on('disconnect', async () => { 218 | if (currentUserId) { 219 | await this.untrackUserSocket(currentUserId, socket.id); 220 | if (!currentServerId) return; 221 | await setUserOnlineStatus(currentUserId, false, currentServerId); 222 | } 223 | }); 224 | }); 225 | } 226 | 227 | private async broadcastUserTypingStatus(serverId: string, userId: string, typing: boolean, typingTo: string | null): Promise { 228 | this.io.in(serverId).emit('user_typing', { userId, typing, typingTo }); 229 | } 230 | 231 | public emitToServer(serverId: string, message: ServerMessage): void { 232 | this.io.to(serverId).emit('server_message', message); 233 | } 234 | 235 | // broadcast new message 236 | public broadcastNewMessage(serverId: string, message: Omit): void { 237 | this.io.in(serverId).emit('new-message', message); 238 | } 239 | 240 | // emit server actions 241 | public emitServerActions(serverId: string, action: ServerAction, username?: string): void { 242 | this.io.to(serverId).emit('server_action', { 243 | type: 'status', 244 | content: `${username || 'A user'} ${action === ServerAction.JOIN ? 'joined' : 'left'} the server`, 245 | action, 246 | username, 247 | timestamp: Date.now() 248 | }); 249 | } 250 | 251 | public broadcastServerMessage(serverId: string, message: ServerMessage): void { 252 | this.io.in(serverId).emit('server_message', message); 253 | } 254 | 255 | public broadcastMessageRead(messageId: string, serverId: string): void { 256 | this.io.in(serverId).emit('message_read', messageId); 257 | } 258 | 259 | public broadcastServerDeleted(serverId: string) { 260 | this.io.in(serverId).emit("server_deleted"); 261 | } 262 | } 263 | 264 | export const getSocketService = (): ServerSocketService => { 265 | return ServerSocketService.getInstance(); 266 | }; 267 | -------------------------------------------------------------------------------- /src/types/error.class.ts: -------------------------------------------------------------------------------- 1 | class AppError extends Error { 2 | statusCode: number; 3 | 4 | constructor(statusCode: number = 500, message: string = "Internal Server Error") { 5 | super(message); 6 | this.statusCode = statusCode; 7 | } 8 | } 9 | 10 | export default AppError; -------------------------------------------------------------------------------- /src/types/message.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Message { 2 | serverId: string, 3 | senderId: string, 4 | receiverId: string, 5 | messageId: string, 6 | content: string, 7 | createdAt: Date, 8 | readBySender: boolean, 9 | readByReceiver: boolean, 10 | sent: boolean, 11 | attachmentUrl?: string, 12 | attachments?: string[] 13 | } -------------------------------------------------------------------------------- /src/types/server-socket.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ServerJoinPayload { 2 | serverId: string; 3 | userId: string; 4 | } 5 | 6 | export interface ServerMessage { 7 | type: 'status' | 'error'; 8 | content: string; 9 | timestamp: number; 10 | } 11 | 12 | export interface UserStatusUpdate { 13 | userId: string; 14 | isOnline: boolean; 15 | serverId: string; 16 | } 17 | 18 | export enum ServerAction { 19 | JOIN = 'join', 20 | LEAVE = 'leave' 21 | } -------------------------------------------------------------------------------- /src/types/server.interface.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | 3 | export interface CreateServerRequest { 4 | serverName: string; 5 | encryptionKey: string; 6 | lifeSpan: number; 7 | fingerprint: string; 8 | } 9 | 10 | export interface ServerResponse { 11 | server_name: string; 12 | server_id: string; 13 | expiration: Date; 14 | } 15 | 16 | export interface ServerData { 17 | serverId: string; 18 | serverName: string; 19 | salt: string; 20 | expiresAt: Date; 21 | isActive?: boolean; 22 | } 23 | 24 | export interface IServerController { 25 | createServer(req: Request, res: Response, next: NextFunction): Promise; 26 | validateRequest(body: any): body is CreateServerRequest; 27 | generateServerData(request: CreateServerRequest): ServerData; 28 | formatResponse(server: ServerData): ServerResponse; 29 | } -------------------------------------------------------------------------------- /src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Initial statistics values used for new instances and resets 3 | * These values match the structure of the IAppStatistics interface 4 | */ 5 | export const initialStats = { 6 | totalServers: 0, 7 | totalMessages: 0, 8 | totalUsers: 0, 9 | activeServers: 0, 10 | activeUsers: 0, 11 | totalFileUploads: 0, 12 | fileTypes: { 13 | images: 0, 14 | videos: 0, 15 | gifs: 0, 16 | pdfs: 0, 17 | others: 0, 18 | }, 19 | }; 20 | 21 | /** 22 | * Statistics keys for type safety throughout the application 23 | */ 24 | export const STAT_KEYS = { 25 | // Top-level stats 26 | TOTAL_SERVERS: 'totalServers' as const, 27 | TOTAL_MESSAGES: 'totalMessages' as const, 28 | TOTAL_USERS: 'totalUsers' as const, 29 | ACTIVE_SERVERS: 'activeServers' as const, 30 | ACTIVE_USERS: 'activeUsers' as const, 31 | TOTAL_FILE_UPLOADS: 'totalFileUploads' as const, 32 | 33 | // File type stats 34 | FILE_TYPES: { 35 | IMAGES: 'images' as const, 36 | VIDEOS: 'videos' as const, 37 | GIFS: 'gifs' as const, 38 | PDFS: 'pdfs' as const, 39 | OTHERS: 'others' as const 40 | } 41 | }; -------------------------------------------------------------------------------- /src/utils/index.util.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from "uuid"; 2 | 3 | export function generateId() { 4 | return uuidv4(); 5 | } 6 | 7 | export function generateVibrantBgColorWithTextVisibility(): { 8 | backgroundColor: string; 9 | isWhiteTextVisible: boolean 10 | } { 11 | const hue = 220 + Math.floor(Math.random() * 60); 12 | const saturation = 70 + Math.floor(Math.random() * 30); 13 | const lightness = 40 + Math.floor(Math.random() * 30); 14 | 15 | const h = hue / 360; 16 | const s = saturation / 100; 17 | const l = lightness / 100; 18 | 19 | let r, g, b; 20 | 21 | if (s === 0) { 22 | r = g = b = l; 23 | } else { 24 | const hue2rgb = (p: number, q: number, t: number) => { 25 | if (t < 0) t += 1; 26 | if (t > 1) t -= 1; 27 | if (t < 1 / 6) return p + (q - p) * 6 * t; 28 | if (t < 1 / 2) return q; 29 | if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; 30 | return p; 31 | }; 32 | 33 | const q = l < 0.5 ? l * (1 + s) : l + s - l * s; 34 | const p = 2 * l - q; 35 | 36 | r = hue2rgb(p, q, h + 1 / 3); 37 | g = hue2rgb(p, q, h); 38 | b = hue2rgb(p, q, h - 1 / 3); 39 | } 40 | 41 | 42 | const rInt = Math.round(r * 255); 43 | const gInt = Math.round(g * 255); 44 | const bInt = Math.round(b * 255); 45 | 46 | 47 | const toHex = (c: number) => { 48 | const hex = c.toString(16); 49 | return hex.length === 1 ? '0' + hex : hex; 50 | }; 51 | 52 | const backgroundColor = `#${toHex(rInt)}${toHex(gInt)}${toHex(bInt)}`; 53 | 54 | 55 | const sRGB = [r, g, b].map(val => { 56 | return val <= 0.03928 ? val / 12.92 : Math.pow((val + 0.055) / 1.055, 2.4); 57 | }); 58 | 59 | const luminance = 0.2126 * sRGB[0] + 0.7152 * sRGB[1] + 0.0722 * sRGB[2]; 60 | 61 | 62 | 63 | const contrastRatio = (1.0 + 0.05) / (luminance + 0.05); 64 | 65 | 66 | const isWhiteTextVisible = contrastRatio >= 3; 67 | 68 | return { backgroundColor, isWhiteTextVisible }; 69 | } -------------------------------------------------------------------------------- /src/utils/messages.utli.ts: -------------------------------------------------------------------------------- 1 | import crypto from "crypto"; 2 | 3 | // Function to encrypt messages 4 | function encryptMessage(text: string, salt: string): string { 5 | const iv = crypto.randomBytes(16); 6 | 7 | const key = crypto.pbkdf2Sync(salt, 'fixed-salt', 100000, 32, 'sha256'); 8 | 9 | const cipher = crypto.createCipheriv('aes-256-ctr', key, iv); 10 | 11 | let encrypted = cipher.update(text, 'utf8', 'hex'); 12 | encrypted += cipher.final('hex'); 13 | 14 | return iv.toString('hex') + encrypted; 15 | } 16 | 17 | // Function to decrypt messages 18 | function decryptMessage(encryptedText: string, salt: string): string { 19 | const iv = Buffer.from(encryptedText.slice(0, 32), 'hex'); 20 | const encryptedContent = encryptedText.slice(32); 21 | 22 | const key = crypto.pbkdf2Sync(salt, 'fixed-salt', 100000, 32, 'sha256'); 23 | 24 | const decipher = crypto.createDecipheriv('aes-256-ctr', key, iv); 25 | 26 | let decrypted = decipher.update(encryptedContent, 'hex', 'utf8'); 27 | decrypted += decipher.final('utf8'); 28 | 29 | return decrypted; 30 | } 31 | 32 | export { decryptMessage, encryptMessage } -------------------------------------------------------------------------------- /src/utils/server.util.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabiconcept/secret-room-backend/b781f85e513b4da479d98fbd90697ddc36ffc82e/src/utils/server.util.ts -------------------------------------------------------------------------------- /src/utils/user.util.ts: -------------------------------------------------------------------------------- 1 | import { adjectives, nouns } from "../../constants/user.constant"; 2 | 3 | const patterns = [ 4 | // Pattern types 5 | { type: 'alpha', generate: () => generateAlphaPattern() }, 6 | { type: 'alphaNum', generate: () => generateAlphaNumPattern() } 7 | ]; 8 | 9 | function generateAlphaPattern(): string { 10 | const pattern = Math.random() < 0.5 11 | ? adjectives[Math.floor(Math.random() * adjectives.length)] 12 | : nouns[Math.floor(Math.random() * nouns.length)]; 13 | 14 | // Capitalize first letter sometimes 15 | return Math.random() < 0.3 ? capitalize(pattern) : pattern; 16 | } 17 | 18 | function generateAlphaNumPattern(): string { 19 | const word = Math.random() < 0.5 20 | ? adjectives[Math.floor(Math.random() * adjectives.length)] 21 | : nouns[Math.floor(Math.random() * nouns.length)]; 22 | 23 | const number = Math.floor(Math.random() * 99) + 1; 24 | return `${word}${number}`; 25 | } 26 | 27 | function capitalize(str: string): string { 28 | return str.charAt(0).toUpperCase() + str.slice(1); 29 | } 30 | 31 | export function generateUsername(salt: string): string { 32 | if (!salt || typeof salt !== 'string' || salt.length === 0) { 33 | throw new Error('A non-empty salt string must be provided'); 34 | } 35 | 36 | // Use the salt to create a deterministic random value 37 | const hashCode = (str: string): number => { 38 | let hash = 0; 39 | for (let i = 0; i < str.length; i++) { 40 | const char = str.charCodeAt(i); 41 | hash = ((hash << 5) - hash) + char; 42 | hash = hash & hash; // Convert to 32bit integer 43 | } 44 | return Math.abs(hash); 45 | }; 46 | 47 | const hash = hashCode(salt); 48 | 49 | // Define patterns for username generation 50 | const patterns = [ 51 | // Pattern for non-alphanumeric (using letters only) 52 | { 53 | generate: () => { 54 | const consonants = 'bcdfghjklmnpqrstvwxyz'; 55 | const vowels = 'aeiou'; 56 | let result = ''; 57 | 58 | // Use the hash to deterministically generate a username 59 | const seed = hash; 60 | const length = (seed % 6) + 3; // Length between 3 and 8 61 | 62 | for (let i = 0; i < length; i++) { 63 | // Alternate between consonants and vowels 64 | const charSet = i % 2 === 0 ? consonants : vowels; 65 | const charIndex = Math.floor((seed / (i + 1)) % charSet.length); 66 | result += charSet[charIndex]; 67 | } 68 | 69 | return result; 70 | } 71 | }, 72 | // Pattern for alphanumeric 73 | { 74 | generate: () => { 75 | const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; 76 | let result = ''; 77 | 78 | const seed = hash; 79 | const length = (seed % 6) + 3; // Length between 3 and 8 80 | 81 | for (let i = 0; i < length; i++) { 82 | const charIndex = Math.floor((seed / (i + 1)) % chars.length); 83 | result += chars[charIndex]; 84 | } 85 | 86 | return result; 87 | } 88 | } 89 | ]; 90 | 91 | // Use the first digit of the hash to determine whether to use alphanumeric 92 | const alphanumeric = (hash % 10) < 3; // 30% chance, just like the original 93 | 94 | // Generate username based on the selected pattern 95 | const pattern = alphanumeric ? patterns[1] : patterns[0]; 96 | const username = pattern.generate(); 97 | 98 | return username; 99 | } 100 | 101 | export function validateUsername(username: string): boolean { 102 | if (username.length < 3 || username.length > 8) return false; 103 | return /^[a-zA-Z0-9]+$/.test(username); 104 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | /* Projects */ 5 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 6 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 7 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 8 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 9 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 10 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 11 | /* Language and Environment */ 12 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 13 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 14 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 15 | // "libReplacement": true, /* Enable lib replacement. */ 16 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 17 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 18 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 19 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 20 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 21 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 22 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 23 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 24 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 25 | /* Modules */ 26 | "module": "commonjs", /* Specify what module code is generated. */ 27 | "rootDir": "./", /* Specify the root folder within your source files. */ 28 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 29 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 30 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 31 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 32 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 33 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 34 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 35 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 36 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 37 | // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ 38 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 39 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 40 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 41 | // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ 42 | // "resolveJsonModule": true, /* Enable importing .json files. */ 43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 45 | /* JavaScript Support */ 46 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 47 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 48 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 49 | /* Emit */ 50 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 51 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 52 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 53 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 54 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 55 | // "noEmit": true, /* Disable emitting files from a compilation. */ 56 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 57 | "outDir": "./dist", /* Specify an output folder for all emitted files. */ 58 | // "removeComments": true, /* Disable emitting comments. */ 59 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 60 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 61 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 62 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 63 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 64 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 65 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 66 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 67 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 68 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 69 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 70 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 71 | /* Interop Constraints */ 72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 73 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 74 | // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ 75 | // "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */ 76 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 77 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 78 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 79 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 80 | /* Type Checking */ 81 | "strict": true, /* Enable all strict type-checking options. */ 82 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 83 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 84 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 85 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 86 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 87 | // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ 88 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 89 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 90 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 91 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 92 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 93 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 94 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 95 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 96 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 97 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 98 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 99 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 100 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 101 | /* Completeness */ 102 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 103 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 104 | } 105 | } --------------------------------------------------------------------------------