├── .dockerignore ├── .env.example ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .prettierrc ├── .vscode ├── launch.json └── settings.json ├── Dockerfile ├── README.md ├── docker-compose.yml ├── docs ├── comment.excalidraw ├── images │ ├── swagger.png │ └── thanks-for-watching.jpg ├── post.excalidraw ├── script.sql ├── service.excalidraw ├── social-network-SNet.pdf └── system.excalidraw ├── fastapi-ai ├── .env.example ├── Dockerfile ├── README.md ├── api │ ├── api_posts.py │ ├── api_router.py │ └── api_users.py ├── core │ └── settings.py ├── database │ ├── base.py │ └── chromadb.py ├── helpers │ ├── embeddinh_type.py │ ├── gender_type.py │ ├── privacy_type.py │ └── role_type.py ├── main.py ├── medias │ ├── 1.jpg │ └── swagger.png ├── middlewares │ └── auth_middleware.py ├── model-ai │ └── yolov8n.pt ├── models │ ├── device_session.py │ ├── post.py │ └── user.py ├── requirements.txt ├── schemas │ ├── device_session.py │ ├── embedding_search_query.py │ ├── id_request.py │ ├── metadata.py │ ├── paginated_query.py │ ├── posts.py │ └── user_interface.py └── services │ ├── ai.py │ ├── auth.py │ ├── device_session.py │ ├── embeddings.py │ ├── post.py │ └── user.py ├── filebeat ├── Dockerfile └── filebeat.yml ├── nest-cli.json ├── nestjs-logs ├── app-2025-05-08.log ├── app-2025-05-08.log.1 ├── app-2025-05-09.log ├── app-2025-05-13.log ├── app-2025-05-19.log └── app-2025-06-02.log ├── package.json ├── public ├── avatar-chat-room │ └── swagger-1746522435932.png ├── avatar-user │ ├── 1-1746519005059.jpg │ └── swagger-1746520908677.png ├── images │ ├── avatar-user │ │ ├── cat-1741019175932.png │ │ ├── cat-1741019236812.png │ │ └── cat-1741019243790.png │ └── default │ │ └── avatar-conversation.jpg └── medias-posts │ ├── 1-1744524025645.jpg │ ├── Screenshot 2024-10-11 080038-1744557016878.png │ └── cat-1744524025645.png ├── src ├── admins │ ├── admin.guard.ts │ ├── admin.interface.ts │ ├── admins.controller.ts │ ├── admins.module.ts │ ├── admins.service.ts │ └── dto │ │ └── add-admin.dto.ts ├── app.controller.ts ├── app.module.ts ├── app.service.ts ├── auth │ ├── auth.module.ts │ ├── auth.service.ts │ ├── google-auth.guard.ts │ ├── google.strategy.ts │ ├── jwt-auth.guard.ts │ ├── jwt.strategy.ts │ └── payload.interface.ts ├── bullmq │ ├── bullmg.module.ts │ ├── bullmq.controller.ts │ ├── bullmq.service.ts │ ├── medias-post.processor.ts │ ├── noti-birthday.processor.ts │ ├── noti-system.processor.ts │ └── send-email.processor.ts ├── chat-members │ ├── chat-members.controller.ts │ ├── chat-members.module.ts │ ├── chat-members.service.ts │ ├── dto │ │ └── request-join-chat-room.dto.ts │ └── entities │ │ ├── chat-member.entity.ts │ │ └── waiting-members.entity.ts ├── chat-messages │ ├── chat-messages.controller.ts │ ├── chat-messages.module.ts │ ├── chat-messages.service.ts │ ├── dto │ │ ├── create-chat-message.dto.ts │ │ └── update-chat-message.dto.ts │ └── entities │ │ └── chat-message.entity.ts ├── chat-rooms │ ├── chat-rooms.controller.ts │ ├── chat-rooms.module.ts │ ├── chat-rooms.service.ts │ ├── dto │ │ ├── create-chat-room.dto.ts │ │ ├── update-chat-room.dto.ts │ │ └── update-permission-add-member.dto.ts │ └── entities │ │ └── chat-room.entity.ts ├── comments │ ├── comments.controller.ts │ ├── comments.module.ts │ ├── comments.service.ts │ ├── dto │ │ ├── create-comment.dto.ts │ │ └── update-comment.dto.ts │ └── entities │ │ └── comment.entity.ts ├── core │ ├── multer.config.ts │ └── transform.interceptor.ts ├── database │ ├── database.module.ts │ └── database.providers.ts ├── decorator │ └── customize.ts ├── device-sessions │ ├── activate.gateway.ts │ ├── device-sessions.controller.ts │ ├── device-sessions.module.ts │ ├── device-sessions.service.ts │ └── entities │ │ └── device-session.entity.ts ├── gateway │ ├── gategate.gateway.ts │ ├── gateway.module.ts │ ├── gateway.service.ts │ └── ws-auth.middleware.ts ├── helper │ ├── activate.enum.ts │ ├── addDay.ts │ ├── deleteFile.ts │ ├── gender.enum.ts │ ├── id.dto.ts │ ├── member.enum.ts │ ├── message-status.enum.ts │ ├── notification.enum.ts │ ├── pagination.dto.ts │ ├── privacy.enum.ts │ ├── reaction.enum.ts │ ├── relation.enum.ts │ ├── role.enum.ts │ └── user-category.enum.ts ├── logger │ ├── log-nest.service.ts │ └── logger.module.ts ├── main.ts ├── notification-users │ ├── dto │ │ └── delete-noti-user.dto.ts │ ├── entities │ │ └── notification-user.entity.ts │ ├── notification-users.controller.ts │ ├── notification-users.module.ts │ └── notification-users.service.ts ├── notifications │ ├── dto │ │ └── create-noti-system.dto.ts │ ├── entities │ │ └── notification.entity.ts │ ├── notification.interface.ts │ ├── notifications.controller.ts │ ├── notifications.module.ts │ └── notifications.service.ts ├── parent-child-comments │ ├── dto │ │ ├── create-parent-child-comment.dto.ts │ │ └── update-parent-child-comment.dto.ts │ ├── entities │ │ └── parent-child-comment.entity.ts │ ├── parent-child-comments.controller.ts │ ├── parent-child-comments.module.ts │ └── parent-child-comments.service.ts ├── pin-chats │ ├── entities │ │ └── pin-chat.entity.ts │ ├── pin-chats.controller.ts │ ├── pin-chats.module.ts │ └── pin-chats.service.ts ├── pin-messages │ ├── entities │ │ └── pin-messages.entity.ts │ ├── pin-messages.controller.ts │ ├── pin-messages.module.ts │ └── pin-messages.service.ts ├── posts │ ├── dto │ │ ├── create-post.dto.ts │ │ ├── update-post.dto.ts │ │ └── update-privacy-post.dto.ts │ ├── entities │ │ └── post.entity.ts │ ├── posts.controller.ts │ ├── posts.module.ts │ └── posts.service.ts ├── reactions │ ├── dto │ │ ├── create-reaction.dto.ts │ │ └── update-reaction.dto.ts │ ├── entities │ │ └── reaction.entity.ts │ ├── reactions.controller.ts │ ├── reactions.module.ts │ └── reactions.service.ts ├── redis │ ├── redis.module.ts │ └── redis.service.ts ├── relations │ ├── dto │ │ ├── relation.dto.ts │ │ └── update-relation.dto.ts │ ├── entities │ │ └── relation.entity.ts │ ├── relations.controller.ts │ ├── relations.module.ts │ └── relations.service.ts ├── save-lists │ ├── dto │ │ ├── create-save-list.dto.ts │ │ └── update-save-list.dto.ts │ ├── entities │ │ └── save-list.entity.ts │ ├── save-lists.controller.ts │ ├── save-lists.module.ts │ └── save-lists.service.ts ├── save-posts │ ├── dto │ │ ├── create-save-post.dto.ts │ │ └── update-save-post.dto.ts │ ├── entities │ │ └── save-post.entity.ts │ ├── save-posts.controller.ts │ ├── save-posts.module.ts │ └── save-posts.service.ts ├── search-engine │ ├── dto │ │ ├── user-search-body.interface.ts │ │ └── user-search.dto.ts │ ├── post-search.service.ts │ ├── search-engine.controller.ts │ ├── search-engine.module.ts │ └── user-search.service.ts ├── templates │ └── email │ │ ├── otp-delete-account.hbs │ │ ├── otp-forgot-password-account.hbs │ │ ├── otp-login-account.hbs │ │ ├── otp-signup-account.hbs │ │ └── signup-success.hbs └── users │ ├── birthday.job.ts │ ├── dto │ ├── after-delete.dto.ts │ ├── after-forgot-password.ts │ ├── after-login.dto.ts │ ├── after-signup.dto.ts │ ├── before-login.dto.ts │ ├── create-account-with-google.dto.ts │ ├── refresh-token.dto.ts │ ├── send-otp.dto.ts │ └── update-user.dto.ts │ ├── entities │ └── user.entity.ts │ ├── users.controller.ts │ ├── users.interface.ts │ ├── users.module.ts │ └── users.service.ts ├── test ├── app.e2e-spec.ts ├── jest-e2e.json └── socket.js ├── tsconfig.build.json └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | yarn-error.log 4 | dist 5 | .DS_Store 6 | .env 7 | .env.local 8 | .env.development 9 | .env.production 10 | .env.test 11 | .cache 12 | coverage 13 | .git 14 | .gitignore 15 | .vscode 16 | .idea 17 | logs 18 | *.log 19 | *.tsbuildinfo -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # # Database 2 | # DATABASE_HOST=postgres 3 | # DATABASE_PORT=5432 4 | # DATABASE_USERNAME=tuanthanh 5 | # DATABASE_PASSWORD=123456 6 | # DATABASE_NAME=social-network-SNet 7 | 8 | # # Access token 9 | # JWT_ACCESS_EXPIRE=432000000 10 | # # Refresh token 11 | # JWT_REFRESH_EXPIRE=1296000000 12 | # JWT_REFRESH_EXPIRE_DAY=15 13 | 14 | # # Rate limit 15 | # THROTTLE_TTL=60000 16 | # THROTTLE_LIMIT=100 17 | 18 | # # Redis 19 | # REDIS_HOST=redis 20 | # REDIS_PORT=6379 21 | # REDIS_PASSWORD=123456 22 | # REDIS_DB=0 23 | 24 | # # Redis 25 | # BULLMQ_HOST=redis 26 | # BULLMQ_PORT=6379 27 | # BULLMQ_PASSWORD=123456 28 | # BULLMQ_DB=1 29 | # TIME_OTP=120 30 | 31 | # # Mail 32 | # MAIL_HOST=smtp.gmail.com 33 | # MAIL_USER=tuanthanh2kk4@gmail.com 34 | # MAIL_PASSWORD='gwti fkzn bfng ngho' 35 | # MAIL_FORM=${MAIL_USER} 36 | # MAIL_TRANSPORT=smtp://${MAIL_USER}:${MAIL_PASSWORD}@${MAIL_HOST} 37 | 38 | # # Elasticsearch 39 | # ELASTICSEARCH_HOSTS=http://elasticsearch:9200 40 | # ELASTICSEARCH_USERNAME=elastic 41 | # ELASTICSEARCH_PASSWORD=tuanthanh 42 | 43 | # # Logstack 44 | # LOGSTASH_HOST=logstash 45 | # LOGSTASH_PORT=5044 46 | 47 | # # Gemini 48 | # GEMINI_API_KEY='AIzaSyAEJLCwGcI5i2u8s80q1QESMoTulcuQt1w' 49 | 50 | # # Node 51 | # NODE_ENV=development 52 | # PORT=3000 53 | # HOST=http://localhost:3000/ 54 | 55 | 56 | # # FastAPI 57 | # FASTAPI_HOST=localhost 58 | # FASTAPI_PORT=8000 59 | # API_PREFIX='' 60 | 61 | 62 | # Run when backend not in docker-compose 63 | # Database 64 | DATABASE_HOST=localhost 65 | DATABASE_PORT=5432 66 | DATABASE_USERNAME=tuanthanh 67 | DATABASE_PASSWORD=123456 68 | DATABASE_NAME=social-network-SNet 69 | 70 | # Access token 71 | JWT_ACCESS_EXPIRE=432000000 72 | # Refresh token 73 | JWT_REFRESH_EXPIRE=1296000000 74 | JWT_REFRESH_EXPIRE_DAY=15 75 | 76 | # Rate limit 77 | THROTTLE_TTL=60000 78 | THROTTLE_LIMIT=100 79 | 80 | # Redis 81 | REDIS_HOST=localhost 82 | REDIS_PORT=6379 83 | REDIS_PASSWORD=123456 84 | REDIS_DB=0 85 | 86 | # Redis 87 | BULLMQ_HOST=localhost 88 | BULLMQ_PORT=6379 89 | BULLMQ_PASSWORD=123456 90 | BULLMQ_DB=1 91 | TIME_OTP=120 92 | 93 | # Mail 94 | MAIL_HOST=smtp.gmail.com 95 | MAIL_USER=tuanthanh2kk4@gmail.com 96 | MAIL_PASSWORD='gwti fkzn bfng ngho' 97 | MAIL_FORM=${MAIL_USER} 98 | MAIL_TRANSPORT=smtp://${MAIL_USER}:${MAIL_PASSWORD}@${MAIL_HOST} 99 | 100 | # Elasticsearch 101 | ELASTICSEARCH_HOSTS=http://elasticsearch:9200 102 | ELASTICSEARCH_USERNAME=elastic 103 | ELASTICSEARCH_PASSWORD=tuanthanh 104 | 105 | # Logstack 106 | LOGSTASH_HOST=logstash 107 | LOGSTASH_PORT=5044 108 | 109 | # Gemini 110 | GEMINI_API_KEY='AIzaSyAEJLCwGcI5i2u8s80q1QESMoTulcuQt1w' 111 | 112 | # Node 113 | NODE_ENV=development 114 | PORT=3000 115 | HOST=http://localhost:3000/ 116 | 117 | 118 | # Google 119 | GOOGLE_CLIENT_ID=your_google_client_id 120 | GOOGLE_CLIENT_SECRET=your_google_client_secret 121 | GOOGLE_CALLBACK_URL=http://localhost:3000/api/auth/google/callback 122 | 123 | 124 | # FastAPI 125 | FASTAPI_HOST=localhost 126 | FASTAPI_PORT=8000 127 | API_PREFIX='' 128 | 129 | 130 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-username-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | 'prettier/prettier': ['error', { endOfLine: 'auto' }], 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntthanh2603/SNet-Backend/3316950608a69517b1e6ea685ea5041ef9b56d2f/.gitattributes -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | dist/ 3 | node_modules/ 4 | documentation/ 5 | .idea/ 6 | .tmp.driveupload/ 7 | fastapi-ai/venv/ 8 | __pycache__ 9 | package-lock.json 10 | nestjs-log/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "endOfLine": "auto" 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug NestJS", 6 | "type": "node", 7 | "request": "launch", 8 | "args": ["${workspaceFolder}/src/main.ts"], 9 | "runtimeArgs": ["-r", "ts-node/register"], 10 | "sourceMaps": true, 11 | "resolveSourceMapLocations": [ 12 | "${workspaceFolder}/**", 13 | "!**/node_modules/**" 14 | ], 15 | "cwd": "${workspaceFolder}", 16 | "outFiles": ["${workspaceFolder}/dist/**/*.js"], 17 | "console": "integratedTerminal", 18 | "autoAttachChildProcesses": true, 19 | "internalConsoleOptions": "neverOpen" 20 | }, 21 | { 22 | "name": "Attach Debug NestJS", 23 | "type": "node", 24 | "request": "attach", 25 | "port": 9229, 26 | "restart": true, 27 | "timeout": 10000, 28 | "sourceMaps": true, 29 | "outFiles": ["${workspaceFolder}/dist/**/*.js"] 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true, 4 | "editor.formatOnPaste": true, 5 | "editor.formatOnType": true, 6 | "editor.tabSize": 2, 7 | "editor.insertSpaces": true, 8 | "files.eol": "\n", 9 | "python.formatting.provider": "black", 10 | "python.formatting.blackArgs": ["--line-length", "88"] 11 | } 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY package*.json ./ 6 | 7 | RUN npm install --legacy-peer-deps 8 | 9 | RUN npm install -g @nestjs/cli --legacy-peer-deps 10 | 11 | CMD ["npm", "run", "start:dev"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SNet - Modern Social Network Platform 2 | 3 |

4 | NPM Version 5 | Package License 6 | NPM Downloads 7 | CircleCI 8 | Discord 9 |

10 | 11 | ## 🚀 Overview 12 | 13 | SNet is a powerful social networking platform built on modern technologies. The project leverages NestJS as its primary framework for building a scalable and efficient backend, with FastAPI serving as a secondary framework for AI-powered features and API processing. 14 | 15 | ## 🔧 Tech Stack 16 | 17 | ### Core Technologies 18 | 19 | - **[NestJS](https://nestjs.com/)**: Primary backend framework 20 | - **[FastAPI](https://fastapi.tiangolo.com/)**: Secondary framework for AI models and API processing 21 | - **[PostgreSQL](https://www.postgresql.org/)**: Main database 22 | - **[Redis](https://redis.io/)**: In-memory database for caching and real-time features 23 | - **[BullMQ](https://docs.bullmq.io/)**: Queue system for background job processing 24 | - **[Elasticsearch](https://www.elastic.co/)**: Search engine for content indexing 25 | - **[FAISS](https://github.com/facebookresearch/faiss)**: Vector similarity search for AI features 26 | 27 | ## 🗂️ Project Structure 28 | 29 | ``` 30 | ├── src/ # Backend system 31 | ├── model-ai/ # Models AI use FastAPI, Huggingface, Faiss... 32 | ├── docs/ # Documents for project and images for README.md 33 | ├── public/ # Data images/videos of app 34 | ├── test/ # Files test app 35 | ├── docker-compose.yaml # Manager containers 36 | ├── README.md # Project documentation (this file) 37 | └── .gitignore # Block files when push github 38 | ``` 39 | 40 | #### Model detect image 16+ [**click here**](https://github.com/ntthanh2603/ImageGuard.git) 41 | 42 | #### Model AI segmentation analysis [**click here**](https://github.com/ntthanh2603/segmentation-analysis.git) 43 | 44 | #### Database [**click here**](https://drive.google.com/file/d/1ZPQa1NhCKHPOJKAGEMPWNWbutVYLUWsU/view?usp=sharing) 45 | 46 | ## ⚙️ Installation and Setup 47 | 48 | ### Using Docker (Recommended) 49 | 50 | ```bash 51 | # Create and run all services 52 | docker-compose up 53 | 54 | # To shut down and remove volumes 55 | docker-compose down -v 56 | ``` 57 | 58 | ### Manual Setup 59 | 60 | ```bash 61 | # Install dependencies 62 | npm install --legacy-peer-deps 63 | 64 | # Development mode with hot-reload 65 | npm run dev 66 | ``` 67 | 68 | ## 🔄 Architecture 69 | 70 | SNet follows a microservices architecture: 71 | 72 | 1. **NestJS Backend**: Handles core application logic, authentication, and main API endpoints 73 | 2. **FastAPI Services**: Process AI-related tasks and specialized API endpoints 74 | 3. **Database Layer**: PostgreSQL for persistent storage, Redis for caching 75 | 4. **Search Services**: Elasticsearch for content search, FAISS for AI vector similarity search 76 | 5. **Background Processing**: BullMQ for handling asynchronous tasks 77 | 78 | ## 🤝 Contributing 79 | 80 | Contributions are welcome! Please feel free to submit a Pull Request. 81 | 82 | ## 💬 Support 83 | 84 | If you encounter any issues or need support, please reach out: 85 | 86 | - [Facebook](https://www.facebook.com/ntthanh2603) 87 | 88 | ## 📜 License 89 | 90 | SNet is [MIT licensed](LICENSE). 91 | 92 |

93 | Thanks for watching 94 |

95 | -------------------------------------------------------------------------------- /docs/comment.excalidraw: -------------------------------------------------------------------------------- 1 | { 2 | "type": "excalidraw", 3 | "version": 2, 4 | "source": "https://excalidraw.com", 5 | "elements": [], 6 | "appState": { 7 | "gridSize": 20, 8 | "gridStep": 5, 9 | "gridModeEnabled": false, 10 | "viewBackgroundColor": "#f5faff" 11 | }, 12 | "files": {} 13 | } -------------------------------------------------------------------------------- /docs/images/swagger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntthanh2603/SNet-Backend/3316950608a69517b1e6ea685ea5041ef9b56d2f/docs/images/swagger.png -------------------------------------------------------------------------------- /docs/images/thanks-for-watching.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntthanh2603/SNet-Backend/3316950608a69517b1e6ea685ea5041ef9b56d2f/docs/images/thanks-for-watching.jpg -------------------------------------------------------------------------------- /docs/script.sql: -------------------------------------------------------------------------------- 1 | -- Tạo 200 user 2 | INSERT INTO "user" (email, password, username, avatar, bio, website, birthday, gender, address, privacy, last_active, user_category, company, education, role, created_at) 3 | SELECT 4 | 'user' || i || '@gmail.com' AS email, 5 | '$2b$10$c3fD0cw/Mi4/xBLvXIOEB.rWSskSxUGxx5AYW8kkGVrz99Q35F/7y' AS password, 6 | 'user' || i AS username, 7 | 'avatar' || i || '.png' AS avatar, 8 | CASE WHEN i % 2 = 0 THEN 'I am user ' || i ELSE NULL END AS bio, 9 | 'https://github.com/ntthanh2603' AS website, 10 | ('1990-01-01'::DATE + (i * INTERVAL '1 month'))::DATE AS birthday, 11 | CASE 12 | WHEN i % 3 = 0 THEN 'male'::user_gender_enum 13 | WHEN i % 3 = 1 THEN 'female'::user_gender_enum 14 | ELSE 'other'::user_gender_enum 15 | END AS gender, 16 | 'Address ' || i AS address, 17 | CASE 18 | WHEN i % 3 = 0 THEN 'public'::user_privacy_enum 19 | WHEN i % 3 = 1 THEN 'private'::user_privacy_enum 20 | ELSE 'friend'::user_privacy_enum 21 | END AS privacy, 22 | ('2025-03-01'::TIMESTAMP + (i * INTERVAL '1 hour')) AS last_active, 23 | 'casualuser'::user_user_category_enum AS user_category, 24 | CASE WHEN i % 3 = 0 THEN ARRAY['Company ' || i]::TEXT[] ELSE ARRAY[]::TEXT[] END AS company, 25 | CASE WHEN i % 2 = 0 THEN ARRAY['Education ' || i]::TEXT[] ELSE ARRAY[]::TEXT[] END AS education, 26 | 'user'::user_role_enum AS role, 27 | CURRENT_TIMESTAMP AS created_at 28 | FROM generate_series(1, 5) AS i; 29 | 30 | 31 | 32 | -- Tạo 5 admin 33 | INSERT INTO "user" (email, password, username, avatar, bio, website, birthday, gender, address, privacy, last_active, user_category, company, education, role, created_at) 34 | SELECT 35 | 'admin' || i || '@gmail.com' AS email, 36 | '$2b$10$c3fD0cw/Mi4/xBLvXIOEB.rWSskSxUGxx5AYW8kkGVrz99Q35F/7y' AS password, 37 | 'admin' || i AS username, 38 | 'avatar' || i || '.png' AS avatar, 39 | CASE WHEN i % 2 = 0 THEN 'I am admin ' || i ELSE NULL END AS bio, 40 | 'https://github.com/ntthanh2603' AS website, 41 | ('1990-01-01'::DATE + (i * INTERVAL '1 month'))::DATE AS birthday, 42 | CASE 43 | WHEN i % 3 = 0 THEN 'male'::user_gender_enum 44 | WHEN i % 3 = 1 THEN 'female'::user_gender_enum 45 | ELSE 'other'::user_gender_enum 46 | END AS gender, 47 | 'Address ' || i AS address, 48 | CASE 49 | WHEN i % 3 = 0 THEN 'public'::user_privacy_enum 50 | WHEN i % 3 = 1 THEN 'private'::user_privacy_enum 51 | ELSE 'friend'::user_privacy_enum 52 | END AS privacy, 53 | ('2025-03-01'::TIMESTAMP + (i * INTERVAL '1 hour')) AS last_active, 54 | 'casualuser'::user_user_category_enum AS user_category, 55 | CASE WHEN i % 3 = 0 THEN ARRAY['Company ' || i]::TEXT[] ELSE ARRAY[]::TEXT[] END AS company, 56 | CASE WHEN i % 2 = 0 THEN ARRAY['Education ' || i]::TEXT[] ELSE ARRAY[]::TEXT[] END AS education, 57 | 'admin'::user_role_enum AS role, 58 | CURRENT_TIMESTAMP AS created_at 59 | FROM generate_series(1, 5) AS i; -------------------------------------------------------------------------------- /docs/service.excalidraw: -------------------------------------------------------------------------------- 1 | { 2 | "type": "excalidraw", 3 | "version": 2, 4 | "source": "https://excalidraw.com", 5 | "elements": [ 6 | { 7 | "id": "TsYOkpVH_BvWzK_0llQOe", 8 | "type": "rectangle", 9 | "x": 810, 10 | "y": 550, 11 | "width": 472, 12 | "height": 202, 13 | "angle": 0, 14 | "strokeColor": "#1971c2", 15 | "backgroundColor": "#e9ecef", 16 | "fillStyle": "solid", 17 | "strokeWidth": 2, 18 | "strokeStyle": "solid", 19 | "roughness": 1, 20 | "opacity": 100, 21 | "groupIds": [], 22 | "frameId": null, 23 | "index": "a0", 24 | "roundness": { 25 | "type": 3 26 | }, 27 | "seed": 130047025, 28 | "version": 50, 29 | "versionNonce": 1949764369, 30 | "isDeleted": false, 31 | "boundElements": null, 32 | "updated": 1732004459487, 33 | "link": null, 34 | "locked": false 35 | } 36 | ], 37 | "appState": { 38 | "gridSize": 20, 39 | "gridStep": 5, 40 | "gridModeEnabled": false, 41 | "viewBackgroundColor": "#f5faff" 42 | }, 43 | "files": {} 44 | } -------------------------------------------------------------------------------- /docs/social-network-SNet.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntthanh2603/SNet-Backend/3316950608a69517b1e6ea685ea5041ef9b56d2f/docs/social-network-SNet.pdf -------------------------------------------------------------------------------- /fastapi-ai/.env.example: -------------------------------------------------------------------------------- 1 | # Database Postgres 2 | DATABASE_HOST=localhost 3 | DATABASE_PORT=5432 4 | DATABASE_USERNAME=tuanthanh 5 | DATABASE_PASSWORD=123456 6 | DATABASE_NAME=social-network-SNet 7 | 8 | # JWT 9 | JWT_ALGORITHM=HS256 10 | UPLOAD_DIR=medias 11 | 12 | # DatabaseChromaDB 13 | CHROMA_DB_HOST=localhost 14 | CHROMA_DB_PORT=8001 15 | 16 | 17 | # Backend NestJS 18 | PORT_NEST=3000 19 | HOST_NEST=localhost 20 | KEY_AUTH=key_auth 21 | 22 | # Backend FastAPI 23 | PORT_FASTAPI=8000 24 | HOST_FASTAPI=localhost 25 | API_PREFIX="" -------------------------------------------------------------------------------- /fastapi-ai/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10 2 | 3 | WORKDIR /app 4 | 5 | COPY requirements.txt . 6 | RUN pip install -r requirements.txt 7 | 8 | COPY . . 9 | 10 | CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] 11 | -------------------------------------------------------------------------------- /fastapi-ai/README.md: -------------------------------------------------------------------------------- 1 | ### Tạo môi trường: 2 | 3 | ```bash 4 | python3 -m venv venv 5 | ``` 6 | 7 | ### Kích hoạt môi trường: 8 | 9 | ```bash 10 | source venv/bin/activate 11 | ``` 12 | 13 | ### Cài đặt các thư viện cần thiết: 14 | 15 | ```bash 16 | pip install -r requirements.txt 17 | ``` 18 | 19 | ### Chaỵ dự án: 20 | 21 | ```bash 22 | uvicorn main:app --reload 23 | ``` 24 | -------------------------------------------------------------------------------- /fastapi-ai/api/api_posts.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from services.post import PostService 3 | from schemas.id_request import IDRequest 4 | 5 | router = APIRouter() 6 | 7 | postService = PostService() 8 | 9 | @router.post("/check-policy-for-post") 10 | async def check_policy_for_post(dto: IDRequest): 11 | return await postService.check_policy_for_posts(dto.id) 12 | 13 | 14 | -------------------------------------------------------------------------------- /fastapi-ai/api/api_router.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from api import api_posts, api_users 3 | 4 | router = APIRouter() 5 | 6 | router.include_router(api_posts.router, tags=["posts"], prefix="/posts") 7 | router.include_router(api_users.router, tags=["users"], prefix="/users") -------------------------------------------------------------------------------- /fastapi-ai/api/api_users.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Request 2 | from services.user import UserService 3 | 4 | 5 | router = APIRouter() 6 | 7 | # Embedding user by text when create user 8 | # @router.post("/create") 9 | # def create_user(dto: CreateUserDto, request: Request): 10 | # return UserService.create(dto, request.state.user.id) 11 | -------------------------------------------------------------------------------- /fastapi-ai/core/settings.py: -------------------------------------------------------------------------------- 1 | from pydantic_settings import BaseSettings 2 | 3 | 4 | class Settings(BaseSettings): 5 | # Database Postgres 6 | DATABASE_HOST: str 7 | DATABASE_PORT: int 8 | DATABASE_USERNAME: str 9 | DATABASE_PASSWORD: str 10 | DATABASE_NAME: str 11 | 12 | # JWT 13 | JWT_ALGORITHM: str 14 | UPLOAD_DIR: str 15 | 16 | # ChromaDB 17 | CHROMA_DB_HOST: str 18 | CHROMA_DB_PORT: int 19 | 20 | # Backend NestJS 21 | PORT_NEST: int 22 | HOST_NEST: str 23 | KEY_AUTH: str 24 | 25 | # FastAPI 26 | PORT_FASTAPI: int 27 | HOST_FASTAPI: str 28 | API_PREFIX: str 29 | 30 | class Config: 31 | env_file = ".env" 32 | 33 | 34 | settings = Settings() 35 | -------------------------------------------------------------------------------- /fastapi-ai/database/base.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine 2 | from sqlalchemy.orm import sessionmaker 3 | from core.settings import settings 4 | 5 | # Get info database from settings 6 | DATABASE_HOST = settings.DATABASE_HOST 7 | DATABASE_PORT = settings.DATABASE_PORT 8 | DATABASE_USERNAME = settings.DATABASE_USERNAME 9 | DATABASE_PASSWORD = settings.DATABASE_PASSWORD 10 | DATABASE_NAME = settings.DATABASE_NAME 11 | 12 | DATABASE_URL = f"postgresql+asyncpg://{DATABASE_USERNAME}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}" 13 | 14 | engine = create_async_engine(DATABASE_URL, echo=True) 15 | 16 | AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) 17 | 18 | async def get_db(): 19 | async with AsyncSessionLocal() as session: 20 | yield session 21 | -------------------------------------------------------------------------------- /fastapi-ai/helpers/embeddinh_type.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | class EmbeddingType(enum.Enum): 4 | USER = "user" 5 | POST = "post" -------------------------------------------------------------------------------- /fastapi-ai/helpers/gender_type.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | class Gender(enum.Enum): 4 | MALE = 'male' 5 | FEMALE = 'female' 6 | OTHER = 'other' -------------------------------------------------------------------------------- /fastapi-ai/helpers/privacy_type.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | class PrivacyType(enum.Enum): 4 | PUBLIC = "public" 5 | PRIVATE = "private" 6 | FRIEND = "friend" -------------------------------------------------------------------------------- /fastapi-ai/helpers/role_type.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | class RoleType(enum.Enum): 4 | ADMIN = "admin" 5 | USER = "user" 6 | -------------------------------------------------------------------------------- /fastapi-ai/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from starlette.middleware.cors import CORSMiddleware 3 | from middlewares.auth_middleware import auth_middleware 4 | from api.api_router import router 5 | import uvicorn 6 | from database.base import get_db 7 | import asyncio 8 | from core.settings import settings 9 | 10 | def create_application() -> FastAPI: 11 | # Create the FastAPI application 12 | application = FastAPI( 13 | title='Social network SNet', docs_url="/docs", redoc_url='/re-docs', 14 | openapi_url=f"{settings.API_PREFIX}/openapi.json", 15 | description=''' 16 | API use FastAPI docs for Social network SNet: 17 | - Posts. 18 | - Recomendations. 19 | - Search vector. 20 | - Suggest user follow. 21 | ''' 22 | ) 23 | 24 | application.middleware("http")(auth_middleware) 25 | 26 | application.add_middleware( 27 | CORSMiddleware, 28 | allow_origins=["*"], 29 | allow_credentials=True, 30 | allow_methods=["*"], 31 | allow_headers=["*"], 32 | ) 33 | application.include_router(router, prefix=settings.API_PREFIX) 34 | return application 35 | 36 | app = create_application() 37 | 38 | def main(): 39 | config = uvicorn.Config( 40 | app=app, 41 | host=settings.HOST_FASTAPI, 42 | port=int(settings.PORT_FASTAPI), 43 | reload=True 44 | ) 45 | 46 | server = uvicorn.Server(config) 47 | asyncio.run(server.serve()) 48 | 49 | if __name__ == '__main__': 50 | main() 51 | 52 | 53 | # CLI run server: uvicorn main:app --reload -------------------------------------------------------------------------------- /fastapi-ai/medias/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntthanh2603/SNet-Backend/3316950608a69517b1e6ea685ea5041ef9b56d2f/fastapi-ai/medias/1.jpg -------------------------------------------------------------------------------- /fastapi-ai/medias/swagger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntthanh2603/SNet-Backend/3316950608a69517b1e6ea685ea5041ef9b56d2f/fastapi-ai/medias/swagger.png -------------------------------------------------------------------------------- /fastapi-ai/middlewares/auth_middleware.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from typing import Optional 3 | from fastapi import Request, HTTPException, status 4 | from fastapi.responses import JSONResponse 5 | import jwt 6 | from schemas.user_interface import IUser 7 | from services.auth import AuthService 8 | from core.settings import settings 9 | 10 | # API public 11 | public_routes = {"/docs", "/openapi.json", "/token"} 12 | # API backend nestjs call 13 | system_routes = {"/posts/check-policy-for-post"} 14 | 15 | # Middleware authorization 16 | async def auth_middleware(request: Request, call_next): 17 | 18 | path = request.url.path 19 | key_auth = request.headers.get("key_auth") 20 | valid_key = settings.KEY_AUTH 21 | 22 | # Check if the request is for a public route 23 | if path in public_routes: 24 | return await call_next(request) 25 | 26 | # Check if the request is for a system route 27 | if path in system_routes: 28 | if key_auth == valid_key: 29 | return await call_next(request) 30 | else: 31 | return JSONResponse( 32 | status_code=status.HTTP_403_FORBIDDEN, 33 | content={"detail": "Your request is not access"} 34 | ) 35 | 36 | # Get token from header 37 | token = request.headers.get("authorization").split(" ")[1] 38 | 39 | if not token: 40 | return JSONResponse( 41 | status_code=status.HTTP_401_UNAUTHORIZED, 42 | content={"detail": "Authorization header missing or invalid"} 43 | ) 44 | 45 | try: 46 | auth_service = AuthService() 47 | payload: IUser = await auth_service.verify_token(token) 48 | 49 | request.state.user = payload 50 | 51 | # Continue process request 52 | response = await call_next(request) 53 | 54 | return response 55 | 56 | except HTTPException as e: 57 | return JSONResponse( 58 | status_code=e.status_code, 59 | content={"detail": e.detail} 60 | ) -------------------------------------------------------------------------------- /fastapi-ai/model-ai/yolov8n.pt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntthanh2603/SNet-Backend/3316950608a69517b1e6ea685ea5041ef9b56d2f/fastapi-ai/model-ai/yolov8n.pt -------------------------------------------------------------------------------- /fastapi-ai/models/device_session.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, String, DateTime 2 | from sqlalchemy.ext.declarative import declarative_base 3 | import datetime 4 | import uuid 5 | 6 | Base = declarative_base() 7 | 8 | class DeviceSession(Base): 9 | __tablename__ = "device_session" 10 | 11 | id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) 12 | user_id = Column(String, nullable=False) 13 | device_id = Column(String, nullable=False) 14 | ip_address = Column(String, nullable=False) 15 | secret_key = Column(String, nullable=False) 16 | refresh_token = Column(String, nullable=False) 17 | expired_at = Column(DateTime, nullable=False) 18 | created_at = Column(DateTime, default=datetime.datetime.utcnow) 19 | updated_at = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow) 20 | -------------------------------------------------------------------------------- /fastapi-ai/models/post.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, String, DateTime, Enum 2 | from sqlalchemy.ext.declarative import declarative_base 3 | import datetime 4 | import uuid 5 | import enum 6 | from helpers.privacy_type import PrivacyType 7 | 8 | Base = declarative_base() 9 | 10 | 11 | 12 | class Post(Base): 13 | __tablename__ = "post" 14 | 15 | id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) 16 | user_id = Column(String, nullable=False) 17 | content = Column(String, nullable=False) 18 | hashtags = Column(String, nullable=True) 19 | medias = Column(String, nullable=True) 20 | privacy = Column(Enum(PrivacyType), nullable=False, default=PrivacyType.PUBLIC) 21 | created_at = Column(DateTime, default=datetime.datetime.utcnow) 22 | -------------------------------------------------------------------------------- /fastapi-ai/models/user.py: -------------------------------------------------------------------------------- 1 | from helpers.gender_type import GenderType 2 | from helpers.role_type import RoleType 3 | from helpers.privacy_type import PrivacyType 4 | from sqlalchemy import Column, String, DateTime, Integer, Enum 5 | from sqlalchemy.ext.declarative import declarative_base 6 | import datetime 7 | import uuid 8 | 9 | Base = declarative_base() 10 | 11 | class User(Base): 12 | __tablename__ = "user" 13 | 14 | id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) 15 | email = Column(String, nullable=False, unique=True) 16 | password = Column(String, nullable=False) 17 | avatar = Column(String, nullable=True) 18 | username = Column(String, nullable=False, unique=True) 19 | bio = Column(String, nullable=True) 20 | birthday = Column(DateTime, nullable=True) 21 | website = Column(String, nullable=True) 22 | gender = Column(Enum(GenderType), nullable=True) 23 | address = Column(String, nullable=True) 24 | company = Column(String, nullable=True) 25 | education = Column(String, nullable=True) 26 | last_active = Column(DateTime, nullable=True) 27 | user_category = Column(Integer, nullable=True) 28 | role = Column(Enum(RoleType), nullable=False, default=RoleType.USER) 29 | privacy = Column(Enum(PrivacyType), nullable=False, default=PrivacyType.PUBLIC) 30 | created_at = Column(DateTime, default=datetime.datetime.utcnow, oncreate=datetime.datetime.utcnow) 31 | updated_at = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow) -------------------------------------------------------------------------------- /fastapi-ai/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | chromadb 4 | sentence-transformers 5 | numpy 6 | pydantic 7 | pyjwt 8 | python-jose[cryptography] 9 | passlib 10 | sqlalchemy 11 | load_dotenv 12 | asyncpg 13 | black 14 | ultralytics 15 | opencv-python 16 | opencv-python 17 | pydantic-settings 18 | aiohttp 19 | pillow -------------------------------------------------------------------------------- /fastapi-ai/schemas/device_session.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from datetime import datetime 3 | 4 | class DeviceSessionBase(BaseModel): 5 | user_id: str 6 | device_id: str 7 | ip_address: str 8 | secret_key: str 9 | refresh_token: str 10 | expired_at: datetime 11 | 12 | 13 | 14 | class DeviceSessionResponse(DeviceSessionBase): 15 | id: str 16 | created_at: datetime 17 | updated_at: datetime 18 | 19 | class Config: 20 | from_attributes = True 21 | -------------------------------------------------------------------------------- /fastapi-ai/schemas/embedding_search_query.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from typing import Optional, List 3 | from helpers.embeddinh_type import EmbeddingType 4 | 5 | 6 | # Search by embedding 7 | class EmbeddingSearchQuery(BaseModel): 8 | embedding: List[float] 9 | page: int = 1 10 | page_size: int = 10 11 | type: Optional[EmbeddingType] = None 12 | -------------------------------------------------------------------------------- /fastapi-ai/schemas/id_request.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class IDRequest(BaseModel): 5 | id: str 6 | -------------------------------------------------------------------------------- /fastapi-ai/schemas/metadata.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from helpers.embeddinh_type import EmbeddingType 3 | from typing import Optional, List 4 | 5 | class Metadata(BaseModel): 6 | id: str 7 | text: str 8 | type: Optional[EmbeddingType] = None 9 | Embedding: Optional[List[float]] = None 10 | -------------------------------------------------------------------------------- /fastapi-ai/schemas/paginated_query.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from typing import Optional 3 | from helpers.embeddinh_type import EmbeddingType 4 | 5 | 6 | class PaginatedQuery(BaseModel): 7 | query: str 8 | page: int = 1 9 | page_size: int = 10 10 | type: Optional[EmbeddingType] = None -------------------------------------------------------------------------------- /fastapi-ai/schemas/posts.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from typing import Optional 3 | from datetime import datetime 4 | from helpers.privacy_type import PrivacyType 5 | 6 | 7 | class PostsBase(BaseModel): 8 | id: str 9 | user_id: str 10 | content: str 11 | hashtags: Optional[str] = None 12 | medias: Optional[str] = None 13 | privacy: PrivacyType = PrivacyType.PUBLIC 14 | created_at: datetime 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /fastapi-ai/schemas/user_interface.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from typing import Optional 3 | 4 | class IUser(BaseModel): 5 | id: str 6 | deviceSessionId: Optional[str] = None 7 | role: str 8 | iat: int 9 | exp: int 10 | 11 | -------------------------------------------------------------------------------- /fastapi-ai/services/ai.py: -------------------------------------------------------------------------------- 1 | from ultralytics import YOLO 2 | import cv2 3 | from typing import Dict, Any 4 | import numpy as np 5 | 6 | PATH_MEDIAS_POSTS = '/home/asus/Code/SNet-Backend/public/medias-posts/' 7 | CLASS_NAME = ["Normal", "Violent", "Adult"] 8 | class AIService: 9 | def __init__(self): 10 | # Load YOLOv8n model 11 | self.model = YOLO('model-ai/yolov8n.pt') 12 | 13 | async def classify_image(self, image_path: str) -> Dict[str, Any]: 14 | try: 15 | # Read image 16 | image = cv2.imread(PATH_MEDIAS_POSTS + image_path) 17 | 18 | if image is None: 19 | raise ValueError("Could not read image") 20 | 21 | # Run inference 22 | results = self.model(image) 23 | 24 | detections = {} 25 | 26 | for result in results: 27 | probs = result.probs 28 | for class_id, prob in enumerate(probs.data): 29 | class_name = CLASS_NAME[class_id] 30 | detections[class_name] = round(float(prob), 2) 31 | 32 | return { 33 | 'success': True, 34 | "image_name": image_path, 35 | 'detections': detections, 36 | } 37 | 38 | except Exception as e: 39 | return { 40 | 'success': False, 41 | 'error': str(e) 42 | } 43 | 44 | -------------------------------------------------------------------------------- /fastapi-ai/services/auth.py: -------------------------------------------------------------------------------- 1 | import jwt 2 | from schemas.user_interface import IUser 3 | from fastapi import Request, HTTPException, status 4 | from fastapi.responses import JSONResponse 5 | from services.device_session import DeviceSessionService 6 | import os 7 | 8 | class AuthService: 9 | # Decode token 10 | def decode_token(self, token: str) -> IUser: 11 | try: 12 | payload = jwt.decode(token, options={"verify_signature": False}) 13 | 14 | return payload 15 | except jwt.PyJWTError: 16 | raise HTTPException( 17 | status_code=status.HTTP_401_UNAUTHORIZED, 18 | detail="Invalid token", 19 | headers={"WWW-Authenticate": "Bearer"}, 20 | ) 21 | 22 | # Verify token 23 | async def verify_token(self, token: str) -> IUser: 24 | try: 25 | payload = self.decode_token(token) 26 | 27 | secret_key: str = await DeviceSessionService.get_secret_key( 28 | device_session_id=payload['deviceSecssionId'], 29 | user_id=payload['id']) 30 | 31 | result = jwt.decode(token, secret_key, algorithms=[os.getenv('JWT_ALGORITHM')]) 32 | 33 | return result 34 | 35 | except jwt.PyJWTError: 36 | raise HTTPException( 37 | status_code=status.HTTP_401_UNAUTHORIZED, 38 | detail="Invalid token", 39 | headers={"WWW-Authenticate": "Bearer"}, 40 | ) -------------------------------------------------------------------------------- /fastapi-ai/services/device_session.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.future import select 2 | from sqlalchemy import text 3 | from database.base import get_db 4 | from fastapi import HTTPException 5 | import datetime 6 | 7 | class DeviceSessionService: 8 | 9 | # Get secret key by device_session_id and user_id 10 | async def get_secret_key(device_session_id: str, user_id: str) -> str: 11 | async for db in get_db(): 12 | try: 13 | query = text( 14 | "SELECT * FROM device_session WHERE id = :device_session_id AND user_id = :user_id" 15 | ) 16 | result = await db.execute(query, {"device_session_id": device_session_id, "user_id": user_id}) 17 | device_session = result.fetchone() 18 | 19 | # Check device_session valid 20 | if not device_session or device_session.expired_at < datetime.datetime.utcnow(): 21 | raise HTTPException( 22 | status_code=404, 23 | detail="Not found session login user" 24 | ) 25 | 26 | 27 | # return secret_key 28 | return device_session.secret_key 29 | 30 | except HTTPException as http_exc: 31 | raise http_exc 32 | except Exception as e: 33 | raise HTTPException( 34 | status_code=500, 35 | detail="Error authorization" 36 | ) -------------------------------------------------------------------------------- /fastapi-ai/services/embeddings.py: -------------------------------------------------------------------------------- 1 | from sentence_transformers import SentenceTransformer 2 | from PIL import Image 3 | 4 | class Embeddings: 5 | 6 | def __init__(self, model_name: str = 'clip-ViT-B-32'): 7 | self.model = SentenceTransformer(model_name) 8 | 9 | # Embedding text 10 | def get_embedding_text(self, text: str) -> list: 11 | return self.model.encode(text).tolist() 12 | 13 | # Embedding image 14 | def get_embedding_image(self, image: Image.Image) -> list: 15 | return self.model.encode(image).tolist() -------------------------------------------------------------------------------- /fastapi-ai/services/post.py: -------------------------------------------------------------------------------- 1 | from database.chromadb import ChromaDb 2 | from database.base import get_db 3 | from sqlalchemy import text 4 | from fastapi import HTTPException 5 | from schemas.posts import PostsBase 6 | from services.ai import AIService 7 | 8 | 9 | class PostService: 10 | def __init__(self): 11 | self.chromadb = ChromaDb() 12 | self.model_ai = AIService() 13 | 14 | async def find_post_by_id(self, posts_id: str): 15 | 16 | async for db in get_db(): 17 | try: 18 | query = text( 19 | "SELECT * FROM post WHERE id = :post_id" 20 | ) 21 | result = await db.execute(query, {"post_id": posts_id}) 22 | post = result.mappings().first() 23 | 24 | if not post: 25 | raise HTTPException( 26 | status_code=404, 27 | detail="Post not found" 28 | ) 29 | 30 | return post 31 | 32 | except HTTPException as http_exc: 33 | raise http_exc 34 | except Exception as e: 35 | raise HTTPException( 36 | status_code=500, 37 | detail=f"Database error: {str(e)}" 38 | ) 39 | 40 | # Check policy for post 41 | async def check_policy_for_posts(self, posts_id: str): 42 | posts: PostsBase = await self.find_post_by_id(posts_id) 43 | # Get medias 44 | medias = posts["medias"] 45 | # Check medias empty 46 | if not medias: 47 | return {"message": "Post is'n media"} 48 | 49 | result = [] 50 | 51 | # Loop medias and detect image 52 | for media in medias: 53 | detect = await self.model_ai.classify_image(media) 54 | result.append(detect) 55 | 56 | return result 57 | 58 | -------------------------------------------------------------------------------- /fastapi-ai/services/user.py: -------------------------------------------------------------------------------- 1 | from services.embeddings import Embeddings 2 | 3 | 4 | 5 | class UserService: 6 | def __init__(self, embeddings: Embeddings): 7 | self.embeddings = embeddings 8 | 9 | # async def create(self,dto: CreateUserDto, user_id: str): 10 | # embedding_text = await self.embeddings.get_embedding_text(dto.text) 11 | 12 | 13 | -------------------------------------------------------------------------------- /filebeat/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.elastic.co/beats/filebeat-oss:7.15.0 2 | 3 | COPY filebeat.yml /usr/share/filebeat/filebeat.yml 4 | USER root 5 | RUN chown root:root /usr/share/filebeat/filebeat.yml 6 | RUN chmod go-w /usr/share/filebeat/filebeat.yml -------------------------------------------------------------------------------- /filebeat/filebeat.yml: -------------------------------------------------------------------------------- 1 | name: 'nest-app-filebeat' 2 | logging.metrics.enabled: false 3 | xpack.monitoring.enabled: false 4 | setup.template.enabled: false 5 | 6 | filebeat.inputs: 7 | - type: log 8 | scan_frequency: 1s 9 | enabled: true 10 | paths: 11 | - /nestjs-logs/app*.log 12 | fields: 13 | service: nestjs-app 14 | fields_under_root: true 15 | json: 16 | keys_under_root: true 17 | overwrite_keys: true 18 | message_key: 'message' 19 | 20 | output.elasticsearch: 21 | hosts: ['elasticsearch:9200'] 22 | index: 'nest-app' 23 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "assets": [{ "include": "templates/**/*", "outDir": "dist/src" }], 7 | "deleteOutDir": true, 8 | "plugins": ["@nestjs/swagger"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /nestjs-logs/app-2025-05-08.log: -------------------------------------------------------------------------------- 1 | {"level":"info","message":"Home page accessed","timestamp":"2025-05-08T14:14:21.045Z"} 2 | {"level":"info","message":"Home page accessed","timestamp":"2025-05-08T14:17:07.837Z"} 3 | {"level":"warn","message":"Home page accessed","timestamp":"2025-05-08T14:17:07.838Z"} 4 | {"level":"error","message":"Home page accessed","timestamp":"2025-05-08T14:17:07.838Z"} 5 | {"level":"info","message":"Home page accessed","timestamp":"2025-05-08T14:23:34.893Z"} 6 | {"level":"warn","message":"Home page accessed","timestamp":"2025-05-08T14:23:34.894Z"} 7 | {"level":"error","message":"Home page accessed","timestamp":"2025-05-08T14:23:34.894Z"} 8 | {"level":"info","message":"Home page accessed","timestamp":"2025-05-08T14:23:43.103Z"} 9 | {"level":"warn","message":"Home page accessed","timestamp":"2025-05-08T14:23:43.103Z"} 10 | {"level":"error","message":"Home page accessed","timestamp":"2025-05-08T14:23:43.103Z"} 11 | {"level":"info","message":"Home page accessed","timestamp":"2025-05-08T14:34:20.156Z"} 12 | {"level":"warn","message":"Home page accessed","timestamp":"2025-05-08T14:34:20.157Z"} 13 | {"level":"error","message":"Home page accessed","timestamp":"2025-05-08T14:34:20.157Z"} 14 | -------------------------------------------------------------------------------- /nestjs-logs/app-2025-05-08.log.1: -------------------------------------------------------------------------------- 1 | {"level":"info","message":"Home page accessed","timestamp":"2025-05-08T14:35:47.843Z"} 2 | {"level":"warn","message":"Home page accessed","timestamp":"2025-05-08T14:35:47.844Z"} 3 | {"level":"error","message":"Home page accessed","timestamp":"2025-05-08T14:35:47.844Z"} 4 | -------------------------------------------------------------------------------- /nestjs-logs/app-2025-05-09.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntthanh2603/SNet-Backend/3316950608a69517b1e6ea685ea5041ef9b56d2f/nestjs-logs/app-2025-05-09.log -------------------------------------------------------------------------------- /nestjs-logs/app-2025-05-13.log: -------------------------------------------------------------------------------- 1 | {"level":"info","message":"Home page accessed","timestamp":"2025-05-13T06:39:59.279Z"} 2 | {"level":"warn","message":"Home page accessed","timestamp":"2025-05-13T06:39:59.285Z"} 3 | {"level":"error","message":"Home page accessed","timestamp":"2025-05-13T06:39:59.288Z"} 4 | -------------------------------------------------------------------------------- /nestjs-logs/app-2025-05-19.log: -------------------------------------------------------------------------------- 1 | {"level":"info","message":"Home page accessed","timestamp":"2025-05-19T06:47:41.505Z"} 2 | {"level":"warn","message":"Home page accessed","timestamp":"2025-05-19T06:47:41.514Z"} 3 | {"level":"error","message":"Home page accessed","timestamp":"2025-05-19T06:47:41.524Z"} 4 | {"level":"info","message":"Home page accessed","timestamp":"2025-05-19T06:47:41.596Z"} 5 | {"level":"warn","message":"Home page accessed","timestamp":"2025-05-19T06:47:41.605Z"} 6 | {"level":"error","message":"Home page accessed","timestamp":"2025-05-19T06:47:41.607Z"} 7 | {"level":"info","message":"Home page accessed","timestamp":"2025-05-19T06:48:06.672Z"} 8 | {"level":"warn","message":"Home page accessed","timestamp":"2025-05-19T06:48:06.673Z"} 9 | {"level":"error","message":"Home page accessed","timestamp":"2025-05-19T06:48:06.675Z"} 10 | {"level":"info","message":"Home page accessed","timestamp":"2025-05-19T06:48:06.727Z"} 11 | {"level":"warn","message":"Home page accessed","timestamp":"2025-05-19T06:48:06.727Z"} 12 | {"level":"error","message":"Home page accessed","timestamp":"2025-05-19T06:48:06.727Z"} 13 | -------------------------------------------------------------------------------- /nestjs-logs/app-2025-06-02.log: -------------------------------------------------------------------------------- 1 | [Nest] 4020 - 02/06/2025, 4:17:58 PM INFO [AppController] Home endpoint accessed 2 | [Nest] 4020 - 02/06/2025, 4:17:58 PM DEBUG [AppController] Debugging home endpoint 3 | [Nest] 4020 - 02/06/2025, 4:17:58 PM WARN [AppController] Warning: Home endpoint accessed 4 | [Nest] 4020 - 02/06/2025, 4:17:58 PM ERROR [AppController] Error: Home endpoint accessed 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "username": "SNet-Backend", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start:dev": "nest start --watch", 12 | "start:debug": "nest start --debug --watch", 13 | "start:prod": "node dist/main", 14 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 15 | "test": "jest", 16 | "test:watch": "jest --watch", 17 | "test:cov": "jest --coverage", 18 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 19 | "test:e2e": "jest --config ./test/jest-e2e.json" 20 | }, 21 | "dependencies": { 22 | "@elastic/ecs-winston-format": "^1.5.3", 23 | "@elastic/elasticsearch": "^8.17.1", 24 | "@google/generative-ai": "^0.21.0", 25 | "@nest-modules/mailer": "^1.3.22", 26 | "@nestjs/bull": "^11.0.2", 27 | "@nestjs/bullmq": "^11.0.2", 28 | "@nestjs/common": "^10.4.15", 29 | "@nestjs/config": "^3.2.3", 30 | "@nestjs/core": "^10.0.0", 31 | "@nestjs/elasticsearch": "^11.0.0", 32 | "@nestjs/jwt": "^10.2.0", 33 | "@nestjs/mapped-types": "*", 34 | "@nestjs/passport": "^10.0.3", 35 | "@nestjs/platform-express": "^10.0.0", 36 | "@nestjs/platform-socket.io": "^9.1.6", 37 | "@nestjs/schedule": "^5.0.1", 38 | "@nestjs/swagger": "^7.4.2", 39 | "@nestjs/throttler": "^6.4.0", 40 | "@nestjs/typeorm": "^10.0.2", 41 | "@nestjs/websockets": "^9.1.6", 42 | "@xenova/transformers": "^2.17.2", 43 | "axios": "^1.7.9", 44 | "bcrypt": "^5.1.1", 45 | "bull": "^4.16.5", 46 | "bullmq": "^5.41.5", 47 | "class-transformer": "^0.5.1", 48 | "class-validator": "^0.14.1", 49 | "cookie-parser": "^1.4.7", 50 | "date-fns": "^4.1.0", 51 | "elasticsearch": "^16.7.3", 52 | "google-auth-library": "^9.15.1", 53 | "hbs": "^4.2.0", 54 | "helmet": "^8.0.0", 55 | "ioredis": "^5.4.2", 56 | "logform": "^2.7.0", 57 | "nest-winston": "^1.10.2", 58 | "nestjs-fingerprint": "^1.0.4", 59 | "nodemailer": "^6.10.0", 60 | "passport": "^0.7.0", 61 | "passport-google-oauth20": "^2.0.0", 62 | "passport-jwt": "^4.0.1", 63 | "passport-local": "^1.0.0", 64 | "pg": "^8.13.0", 65 | "randomatic": "^3.1.1", 66 | "reflect-metadata": "^0.2.0", 67 | "rxjs": "^7.8.1", 68 | "socket.io": "^4.8.1", 69 | "socket.io-client": "^4.8.1", 70 | "typeorm": "^0.3.20", 71 | "uuid": "^11.1.0", 72 | "winston": "^3.17.0", 73 | "winston-daily-rotate-file": "^5.0.0", 74 | "winston-elasticsearch": "^0.19.0", 75 | "winston-logstash-transport": "^2.0.0" 76 | }, 77 | "devDependencies": { 78 | "@compodoc/compodoc": "^1.1.26", 79 | "@nestjs/cli": "^10.0.0", 80 | "@nestjs/schematics": "^10.0.0", 81 | "@nestjs/testing": "^10.0.0", 82 | "@types/bcrypt": "^5.0.2", 83 | "@types/cookie-parser": "^1.4.8", 84 | "@types/express": "^4.17.17", 85 | "@types/jest": "^29.5.2", 86 | "@types/ms": "^0.7.34", 87 | "@types/multer": "^1.4.12", 88 | "@types/node": "^20.3.1", 89 | "@types/nodemailer": "^6.4.17", 90 | "@types/passport-local": "^1.0.38", 91 | "@types/supertest": "^6.0.0", 92 | "@types/uuid": "^10.0.0", 93 | "@typescript-eslint/eslint-plugin": "^8.0.0", 94 | "@typescript-eslint/parser": "^8.0.0", 95 | "eslint": "^8.57.1", 96 | "eslint-config-prettier": "^9.1.0", 97 | "eslint-plugin-prettier": "^5.2.1", 98 | "jest": "^29.5.0", 99 | "ms": "^2.1.3", 100 | "prettier": "^3.3.3", 101 | "source-map-support": "^0.5.21", 102 | "supertest": "^7.0.0", 103 | "ts-jest": "^29.1.0", 104 | "ts-loader": "^9.4.3", 105 | "ts-node": "^10.9.1", 106 | "tsconfig-paths": "^4.2.0", 107 | "typescript": "^5.1.3" 108 | }, 109 | "jest": { 110 | "moduleFileExtensions": [ 111 | "js", 112 | "json", 113 | "ts" 114 | ], 115 | "rootDir": "src", 116 | "testRegex": ".*\\.spec\\.ts$", 117 | "transform": { 118 | "^.+\\.(t|j)s$": "ts-jest" 119 | }, 120 | "collectCoverageFrom": [ 121 | "**/*.(t|j)s" 122 | ], 123 | "coverageDirectory": "../coverage", 124 | "testEnvironment": "node" 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /public/avatar-chat-room/swagger-1746522435932.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntthanh2603/SNet-Backend/3316950608a69517b1e6ea685ea5041ef9b56d2f/public/avatar-chat-room/swagger-1746522435932.png -------------------------------------------------------------------------------- /public/avatar-user/1-1746519005059.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntthanh2603/SNet-Backend/3316950608a69517b1e6ea685ea5041ef9b56d2f/public/avatar-user/1-1746519005059.jpg -------------------------------------------------------------------------------- /public/avatar-user/swagger-1746520908677.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntthanh2603/SNet-Backend/3316950608a69517b1e6ea685ea5041ef9b56d2f/public/avatar-user/swagger-1746520908677.png -------------------------------------------------------------------------------- /public/images/avatar-user/cat-1741019175932.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntthanh2603/SNet-Backend/3316950608a69517b1e6ea685ea5041ef9b56d2f/public/images/avatar-user/cat-1741019175932.png -------------------------------------------------------------------------------- /public/images/avatar-user/cat-1741019236812.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntthanh2603/SNet-Backend/3316950608a69517b1e6ea685ea5041ef9b56d2f/public/images/avatar-user/cat-1741019236812.png -------------------------------------------------------------------------------- /public/images/avatar-user/cat-1741019243790.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntthanh2603/SNet-Backend/3316950608a69517b1e6ea685ea5041ef9b56d2f/public/images/avatar-user/cat-1741019243790.png -------------------------------------------------------------------------------- /public/images/default/avatar-conversation.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntthanh2603/SNet-Backend/3316950608a69517b1e6ea685ea5041ef9b56d2f/public/images/default/avatar-conversation.jpg -------------------------------------------------------------------------------- /public/medias-posts/1-1744524025645.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntthanh2603/SNet-Backend/3316950608a69517b1e6ea685ea5041ef9b56d2f/public/medias-posts/1-1744524025645.jpg -------------------------------------------------------------------------------- /public/medias-posts/Screenshot 2024-10-11 080038-1744557016878.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntthanh2603/SNet-Backend/3316950608a69517b1e6ea685ea5041ef9b56d2f/public/medias-posts/Screenshot 2024-10-11 080038-1744557016878.png -------------------------------------------------------------------------------- /public/medias-posts/cat-1744524025645.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntthanh2603/SNet-Backend/3316950608a69517b1e6ea685ea5041ef9b56d2f/public/medias-posts/cat-1744524025645.png -------------------------------------------------------------------------------- /src/admins/admin.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; 2 | import { Observable } from 'rxjs'; 3 | import { RoleType } from 'src/helper/role.enum'; 4 | 5 | @Injectable() 6 | export class AdminGuard implements CanActivate { 7 | canActivate( 8 | context: ExecutionContext, 9 | ): boolean | Promise | Observable { 10 | const request = context.switchToHttp().getRequest(); 11 | return request.user.role === RoleType.ADMIN ? true : true; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/admins/admin.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IAdmin { 2 | id: string; 3 | deviceId: string; 4 | role: string; 5 | iat: number; 6 | exp: number; 7 | } 8 | -------------------------------------------------------------------------------- /src/admins/admins.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Post, UseGuards } from '@nestjs/common'; 2 | import { AdminsService } from './admins.service'; 3 | import { ApiOperation, ApiTags } from '@nestjs/swagger'; 4 | import { AdminGuard } from './admin.guard'; 5 | import { Admin } from 'src/decorator/customize'; 6 | import { IAdmin } from './admin.interface'; 7 | import { AddAdminDto } from './dto/add-admin.dto'; 8 | 9 | @Controller('admins') 10 | @ApiTags('Admins') 11 | @UseGuards(AdminGuard) 12 | export class AdminsController { 13 | constructor(private readonly adminsService: AdminsService) {} 14 | @Post('add-admin') 15 | @ApiOperation({ summary: 'Admin: Add Admin' }) 16 | addAdmin(@Admin() admin: IAdmin, @Body() dto: AddAdminDto) { 17 | return this.adminsService.addAdmin(admin, dto); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/admins/admins.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AdminsService } from './admins.service'; 3 | import { AdminsController } from './admins.controller'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { User } from 'src/users/entities/user.entity'; 6 | import { AdminGuard } from './admin.guard'; 7 | import { UsersModule } from 'src/users/users.module'; 8 | 9 | @Module({ 10 | imports: [TypeOrmModule.forFeature([User]), UsersModule], 11 | controllers: [AdminsController], 12 | providers: [AdminsService, AdminGuard], 13 | exports: [AdminGuard], 14 | }) 15 | export class AdminsModule {} 16 | -------------------------------------------------------------------------------- /src/admins/admins.service.ts: -------------------------------------------------------------------------------- 1 | import { UsersService } from './../users/users.service'; 2 | import { 3 | BadRequestException, 4 | Injectable, 5 | InternalServerErrorException, 6 | } from '@nestjs/common'; 7 | import { InjectRepository } from '@nestjs/typeorm'; 8 | import { User } from 'src/users/entities/user.entity'; 9 | import { Repository } from 'typeorm'; 10 | import { IAdmin } from './admin.interface'; 11 | import { AddAdminDto } from './dto/add-admin.dto'; 12 | import { RoleType } from 'src/helper/role.enum'; 13 | 14 | @Injectable() 15 | export class AdminsService { 16 | constructor( 17 | @InjectRepository(User) 18 | private readonly adminRepository: Repository, 19 | private readonly usersService: UsersService, 20 | ) {} 21 | 22 | /** 23 | * Promotes a user to an admin role. 24 | * 25 | * @param admin - The admin performing the action. 26 | * @param dto - Data transfer object containing the ID of the user to be promoted. 27 | * @returns An object containing a success message if the user is successfully promoted. 28 | * @throws BadRequestException - If the user is not found or is not currently a regular user. 29 | * @throws InternalServerErrorException - If an error occurs during the promotion process. 30 | */ 31 | async addAdmin(admin: IAdmin, dto: AddAdminDto) { 32 | try { 33 | const user = await this.usersService.findUserById(dto.user_id); 34 | 35 | if (user && user.role === RoleType.USER) { 36 | user.role = RoleType.ADMIN; 37 | await this.adminRepository.save(user); 38 | 39 | return { 40 | message: 'Admin added successfully', 41 | }; 42 | } 43 | throw new BadRequestException('User not found or role user is not user'); 44 | } catch (error) { 45 | if (error instanceof BadRequestException) { 46 | throw error; 47 | } 48 | throw new InternalServerErrorException(); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/admins/dto/add-admin.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsUUID } from 'class-validator'; 2 | 3 | export class AddAdminDto { 4 | @IsUUID() 5 | user_id: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Post } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | import { Public, ResponseMessage } from './decorator/customize'; 4 | import { ApiOperation, ApiTags } from '@nestjs/swagger'; 5 | import { LogNestService } from './logger/log-nest.service'; 6 | 7 | @ApiTags('App') 8 | @Controller() 9 | export class AppController { 10 | constructor( 11 | private readonly appService: AppService, 12 | private logNestService: LogNestService, 13 | ) {} 14 | 15 | @Get() 16 | @Public() 17 | @ResponseMessage('Trang chủ') 18 | @ApiOperation({ summary: 'Trang chủ' }) 19 | home() { 20 | this.logNestService.log('Home endpoint accessed', 'AppController'); 21 | this.logNestService.debug('Debugging home endpoint', 'AppController'); 22 | this.logNestService.warn( 23 | 'Warning: Home endpoint accessed', 24 | 'AppController', 25 | ); 26 | this.logNestService.error('Error: Home endpoint accessed', 'AppController'); 27 | 28 | return this.appService.home(); 29 | } 30 | 31 | @Post('/chatbot/new') 32 | @ResponseMessage('Tạo prompt thành công') 33 | @ApiOperation({ summary: 'Tạo prompt với Chatbot AI' }) 34 | chatbot(@Body() body: { prompt: string }) { 35 | return this.appService.chatbot(body.prompt); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { GoogleGenerativeAI } from '@google/generative-ai'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { ConfigService } from '@nestjs/config'; 4 | 5 | @Injectable() 6 | export class AppService { 7 | constructor(private readonly configService: ConfigService) {} 8 | 9 | /** 10 | * Returns a string indicating the home page message. 11 | * 12 | * @returns {string} - The message for the home page. 13 | */ 14 | 15 | /****** 8418b6ea-591b-4cd6-b4f7-fd0c61a2c1e2 *******/ 16 | home() { 17 | return 'Đây là trang chủ'; 18 | } 19 | 20 | /** 21 | * Generates a response from a generative AI model based on a provided prompt. 22 | * 23 | * This function initializes a generative AI model using a specified API key 24 | * and configuration settings. It starts a chat session with the model, 25 | * appending a base prompt that describes the features of the SNet social 26 | * networking application. The model is then prompted with additional user 27 | * input to generate a contextual response. 28 | * 29 | * @param prompt - The user-provided string to be appended to the base prompt 30 | * for generating the AI's response. 31 | * @returns An object containing the AI-generated response text. 32 | */ 33 | 34 | /****** 9d761ba5-64f2-46e3-b78d-44707ad3fa4b *******/ 35 | async chatbot(prompt: string) { 36 | const genAI = new GoogleGenerativeAI( 37 | this.configService.get('GEMINI_API_KEY'), 38 | ); 39 | const model = genAI.getGenerativeModel({ model: 'gemini-2.0-flash' }); 40 | 41 | const generationConfig = { 42 | temperature: 1, 43 | topP: 0.95, 44 | topK: 40, 45 | maxOutputTokens: 8192, 46 | responseMimeType: 'text/plain', 47 | }; 48 | 49 | const chatSession = model.startChat({ 50 | generationConfig, 51 | history: [], 52 | }); 53 | 54 | const promptBase = `Trong dự án ứng dụng mạng xã hội tên SNet, SNet có các chức 55 | năng tạo đoạn chat,nhắn tin, theo dõi người dùng khác, hủy theo dõi người dùng 56 | khác, thông báo, đăng bài, thả cảm xúc, bình luận. Bạn hãy trả lời câu hỏi sau:`; 57 | 58 | const result = await chatSession.sendMessage(promptBase + prompt); 59 | 60 | return { result: result.response.text() }; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Module } from '@nestjs/common'; 2 | import { UsersModule } from 'src/users/users.module'; 3 | import { DeviceSessionsModule } from 'src/device-sessions/device-sessions.module'; 4 | import { AuthService } from './auth.service'; 5 | import { JwtModule } from '@nestjs/jwt'; 6 | import { ConfigModule, ConfigService } from '@nestjs/config'; 7 | import { PassportModule } from '@nestjs/passport'; 8 | import { JwtStrategy } from './jwt.strategy'; 9 | import { GoogleStrategy } from './google.strategy'; 10 | import { TypeOrmModule } from '@nestjs/typeorm'; 11 | import { User } from 'src/users/entities/user.entity'; 12 | import { GoogleAuthGuard } from './google-auth.guard'; 13 | 14 | @Module({ 15 | imports: [ 16 | TypeOrmModule.forFeature([User]), 17 | forwardRef(() => UsersModule), 18 | forwardRef(() => DeviceSessionsModule), 19 | PassportModule.register({ defaultStrategy: 'jwt' }), 20 | JwtModule.registerAsync({ 21 | imports: [ConfigModule], 22 | useFactory: async (configService: ConfigService) => ({ 23 | // secret: configService.get('JWT_ACCESS_TOKEN_SECRET'), 24 | signOptions: { 25 | expiresIn: configService.get('JWT_ACCESS_EXPIRE'), 26 | }, 27 | }), 28 | inject: [ConfigService], 29 | }), 30 | ], 31 | controllers: [], 32 | providers: [AuthService, JwtStrategy, GoogleStrategy, GoogleAuthGuard], 33 | exports: [AuthService, GoogleAuthGuard, JwtStrategy], 34 | }) 35 | export class AuthModule {} 36 | -------------------------------------------------------------------------------- /src/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { JwtService } from '@nestjs/jwt'; 2 | import { 3 | forwardRef, 4 | Inject, 5 | Injectable, 6 | UnauthorizedException, 7 | } from '@nestjs/common'; 8 | import { ConfigService } from '@nestjs/config'; 9 | import { DeviceSessionsService } from 'src/device-sessions/device-sessions.service'; 10 | import { OAuth2Client } from 'google-auth-library'; 11 | import { IPayload } from './payload.interface'; 12 | import { UsersService } from 'src/users/users.service'; 13 | import { InjectRepository } from '@nestjs/typeorm'; 14 | import { User } from 'src/users/entities/user.entity'; 15 | import { Repository } from 'typeorm'; 16 | import { CreateAccountWithGoogleDto } from 'src/users/dto/create-account-with-google.dto'; 17 | 18 | @Injectable() 19 | export class AuthService { 20 | private googleClient: OAuth2Client; 21 | 22 | constructor( 23 | private jwtService: JwtService, 24 | private configService: ConfigService, 25 | @Inject(forwardRef(() => DeviceSessionsService)) 26 | private readonly deviceSessionsService: DeviceSessionsService, 27 | @Inject(forwardRef(() => UsersService)) 28 | private readonly usersService: UsersService, 29 | @InjectRepository(User) 30 | private readonly userRepository: Repository, 31 | ) { 32 | this.googleClient = new OAuth2Client( 33 | this.configService.get('GOOGLE_CLIENT_ID'), 34 | ); 35 | } 36 | 37 | async validateTokenGoogle(googleUser: CreateAccountWithGoogleDto) { 38 | try { 39 | const user = await this.userRepository.findOne({ 40 | where: { email: googleUser.email }, 41 | }); 42 | if (user) return user; 43 | return await this.userRepository.save(googleUser); 44 | } catch { 45 | throw new UnauthorizedException('Invalid token'); 46 | } 47 | } 48 | 49 | async verify(token: string) { 50 | try { 51 | // Decode token 52 | const decoded = this.jwtService.decode(token); 53 | 54 | // Get secret key from device session 55 | const secret = await this.deviceSessionsService.getSecret( 56 | decoded.id, 57 | decoded.deviceSecssionid, 58 | ); 59 | 60 | // Return payload if valid token 61 | return this.jwtService.verify(token, { 62 | secret: secret, 63 | }); 64 | } catch (error) { 65 | throw new Error(`Failed to verify token: ${error.message}`); 66 | } 67 | } 68 | 69 | decode(token: string) { 70 | try { 71 | const decoded = this.jwtService.decode(token); 72 | if (!decoded) { 73 | throw new Error('Invalid token'); 74 | } 75 | return decoded; 76 | } catch (error) { 77 | throw new Error(`Failed to decode token: ${error.message}`); 78 | } 79 | } 80 | 81 | generateAccessToken(payload: IPayload, secretKey: string) { 82 | const accessToken = this.jwtService.sign(payload, { 83 | secret: secretKey, 84 | expiresIn: this.configService.get('JWT_ACCESS_EXPIRE'), 85 | }); 86 | 87 | return accessToken; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/auth/google-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export class GoogleAuthGuard extends AuthGuard('google') {} 6 | -------------------------------------------------------------------------------- /src/auth/google.strategy.ts: -------------------------------------------------------------------------------- 1 | import { PassportStrategy } from '@nestjs/passport'; 2 | import { Strategy, VerifyCallback } from 'passport-google-oauth20'; 3 | import { Injectable } from '@nestjs/common'; 4 | import { ConfigService } from '@nestjs/config'; 5 | import { AuthService } from './auth.service'; 6 | import { CreateAccountWithGoogleDto } from 'src/users/dto/create-account-with-google.dto'; 7 | 8 | @Injectable() 9 | export class GoogleStrategy extends PassportStrategy(Strategy) { 10 | constructor( 11 | private configService: ConfigService, 12 | private authService: AuthService, 13 | ) { 14 | super({ 15 | clientID: configService.get('GOOGLE_CLIENT_ID'), 16 | clientSecret: configService.get('GOOGLE_CLIENT_SECRET'), 17 | callbackURL: configService.get('GOOGLE_CALLBACK_URL'), 18 | scope: ['email', 'profile'], 19 | }); 20 | } 21 | 22 | async validate( 23 | accessToken: string, 24 | refreshToken: string, 25 | profile: any, 26 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 27 | done: VerifyCallback, 28 | ): Promise { 29 | const user = new CreateAccountWithGoogleDto(); 30 | user.email = profile.emails[0].value; 31 | user.username = profile.displayName; 32 | user.avatar = profile.photos[0].value; 33 | user.password = ''; 34 | 35 | return await this.authService.validateTokenGoogle(user); 36 | } 37 | } 38 | 39 | // accessToken ya29.a0AW4XtxhHXhWgEVE2VNiTetMB19o2G2jfKQ2GFkgnOn8Ui6zRSFs1Cxhy7639_TlRY8ajTFVqChbrOOQgkcfho-AcXdLsffs_Zdp21h9UVcqrGDTXSWFTHNYuKt6QsslRScpVADG5IlMev-XqRniyHubPygsHdyHp2BnUGBq-aCgYKAZUSARYSFQHGX2MiGDAKhXVywGjMCYOJGwsojQ0175 40 | // refreshToken undefined 41 | // profile { 42 | // id: '101044400483186205867', 43 | // displayName: 'Thành Tuấn', 44 | // name: { familyName: 'Tuấn', givenName: 'Thành' }, 45 | // emails: [ { value: 'tuanthanh2kk4@gmail.com', verified: 46 | // true } ], 47 | // photos: [ 48 | // { 49 | // value: 'https://lh3.googleusercontent.com/a/ACg8ocJIJ43GB6PDJpco42Kdp__TcUrIfbwcWiY_xlMugg2BvcORFBo=s96-c' 50 | // } 51 | // ], 52 | // provider: 'google', 53 | // _raw: '{\n' + 54 | // ' "sub": "101044400483186205867",\n' + 55 | // ' "name": "Thành Tuấn",\n' + 56 | // ' "given_name": "Thành",\n' + 57 | // ' "family_name": "Tuấn",\n' + 58 | // ' "picture": "https://lh3.googleusercontent.com/a/ACg8ocJIJ43GB6PDJpco42Kdp__TcUrIfbwcWiY_xlMugg2BvcORFBo\\u003ds96-c",\n' + 59 | // ' "email": "tuanthanh2kk4@gmail.com",\n' + 60 | // ' "email_verified": true\n' + 61 | // '}', 62 | // _json: { 63 | // sub: '101044400483186205867', 64 | // name: 'Thành Tuấn', 65 | // given_name: 'Thành', 66 | // family_name: 'Tuấn', 67 | // picture: 'https://lh3.googleusercontent.com/a/ACg8ocJIJ43GB6PDJpco42Kdp__TcUrIfbwcWiY_xlMugg2BvcORFBo=s96-c', 68 | // email: 'tuanthanh2kk4@gmail.com', 69 | // email_verified: true 70 | // } 71 | // } 72 | -------------------------------------------------------------------------------- /src/auth/jwt-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExecutionContext, 3 | Injectable, 4 | UnauthorizedException, 5 | } from '@nestjs/common'; 6 | import { Reflector } from '@nestjs/core'; 7 | import { AuthGuard } from '@nestjs/passport'; 8 | import { IS_PUBLIC_KEY } from 'src/decorator/customize'; 9 | 10 | /* 11 | - Bảo vệ các route yêu cầu xác thực JWT, cho phép bỏ qua các route được đánh dấu là public. 12 | */ 13 | @Injectable() 14 | export class JwtAuthGuard extends AuthGuard('jwt') { 15 | constructor(private reflector: Reflector) { 16 | super(); 17 | } 18 | 19 | canActivate(context: ExecutionContext) { 20 | const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ 21 | context.getHandler(), 22 | context.getClass(), 23 | ]); 24 | 25 | if (isPublic) { 26 | return true; 27 | } 28 | return super.canActivate(context); 29 | } 30 | 31 | handleRequest(err, user) { 32 | if (err || !user) { 33 | throw err || new UnauthorizedException('Token invalid'); 34 | } 35 | return user; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/auth/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { ExtractJwt, Strategy } from 'passport-jwt'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { 4 | forwardRef, 5 | Inject, 6 | Injectable, 7 | UnauthorizedException, 8 | } from '@nestjs/common'; 9 | import { IUser } from 'src/users/users.interface'; 10 | import { DeviceSessionsService } from 'src/device-sessions/device-sessions.service'; 11 | 12 | @Injectable() 13 | export class JwtStrategy extends PassportStrategy(Strategy) { 14 | constructor( 15 | @Inject(forwardRef(() => DeviceSessionsService)) 16 | private readonly deviceSessionsService: DeviceSessionsService, 17 | ) { 18 | super({ 19 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 20 | ignoreExpiration: false, 21 | secretOrKeyProvider: async (request, rawJwtToken, done) => { 22 | try { 23 | // Decode token and get `deviceSessionId` 24 | const payload: IUser = JSON.parse( 25 | Buffer.from(rawJwtToken.split('.')[1], 'base64').toString(), 26 | ); 27 | 28 | if (!payload.deviceSecssionId) { 29 | return done( 30 | new UnauthorizedException('Invalid token payload'), 31 | null, 32 | ); 33 | } 34 | 35 | // Find device session 36 | const deviceSession = await this.deviceSessionsService.findOne( 37 | payload.deviceSecssionId, 38 | ); 39 | 40 | if (!deviceSession || !deviceSession.secret_key) { 41 | return done( 42 | new UnauthorizedException('Invalid device session'), 43 | null, 44 | ); 45 | } 46 | 47 | // Return secret key 48 | return done(null, deviceSession.secret_key); 49 | } catch { 50 | return done(new UnauthorizedException('Invalid token format'), null); 51 | } 52 | }, 53 | }); 54 | } 55 | 56 | async validate(payload: IUser) { 57 | return payload; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/auth/payload.interface.ts: -------------------------------------------------------------------------------- 1 | import { RoleType } from 'src/helper/role.enum'; 2 | 3 | export interface IPayload { 4 | id: string; 5 | deviceSecssionId: string; 6 | role: RoleType; 7 | sub?: number; 8 | iat?: number; 9 | exp?: number; 10 | } 11 | -------------------------------------------------------------------------------- /src/bullmq/bullmg.module.ts: -------------------------------------------------------------------------------- 1 | import { RedisModule } from './../redis/redis.module'; 2 | import { Module, Global } from '@nestjs/common'; 3 | import { ConfigModule } from '@nestjs/config'; 4 | import { BullMQController } from './bullmq.controller'; 5 | import { BullModule } from '@nestjs/bullmq'; 6 | import { BullMQService } from './bullmq.service'; 7 | import { SendEmailProcessor } from './send-email.processor'; 8 | import { NotiSystemProcessor } from './noti-system.processor'; 9 | import { TypeOrmModule } from '@nestjs/typeorm'; 10 | import { User } from 'src/users/entities/user.entity'; 11 | import { NotificationUser } from 'src/notification-users/entities/notification-user.entity'; 12 | import { GatewayModule } from 'src/gateway/gateway.module'; 13 | import { MediasPostsProcessor } from './medias-post.processor'; 14 | 15 | @Global() 16 | @Module({ 17 | imports: [ 18 | ConfigModule, 19 | RedisModule, 20 | BullModule.registerQueue({ name: 'send-email' }), 21 | BullModule.registerQueue({ name: 'noti-birthday' }), 22 | BullModule.registerQueue({ name: 'noti-system' }), 23 | BullModule.registerQueue({ name: 'create-posts' }), 24 | TypeOrmModule.forFeature([User]), 25 | TypeOrmModule.forFeature([NotificationUser]), 26 | GatewayModule, 27 | ], 28 | controllers: [BullMQController], 29 | providers: [ 30 | BullMQService, 31 | SendEmailProcessor, 32 | NotiSystemProcessor, 33 | MediasPostsProcessor, 34 | ], 35 | exports: [SendEmailProcessor, NotiSystemProcessor], 36 | }) 37 | export class BullMQModule {} 38 | -------------------------------------------------------------------------------- /src/bullmq/bullmq.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common'; 2 | import { ApiTags } from '@nestjs/swagger'; 3 | import { BullMQService } from './bullmq.service'; 4 | 5 | @ApiTags('job') 6 | @Controller('jobs') 7 | export class BullMQController { 8 | constructor(private bullMQService: BullMQService) {} 9 | } 10 | -------------------------------------------------------------------------------- /src/bullmq/bullmq.service.ts: -------------------------------------------------------------------------------- 1 | import { InjectQueue } from '@nestjs/bullmq'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { Queue } from 'bullmq'; 4 | 5 | @Injectable() 6 | export class BullMQService { 7 | constructor(@InjectQueue('send-email') private readonly queue: Queue) {} 8 | 9 | async push(name: string, data: any, opts?: any): Promise { 10 | await this.queue.add(name, data, opts); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/bullmq/medias-post.processor.ts: -------------------------------------------------------------------------------- 1 | import { Processor, WorkerHost } from '@nestjs/bullmq'; 2 | import { Job } from 'bullmq'; 3 | 4 | @Processor('create-posts') 5 | export class MediasPostsProcessor extends WorkerHost { 6 | constructor() { 7 | super(); 8 | } 9 | 10 | async process(job: Job): Promise { 11 | try { 12 | console.log('data create post', job.data); 13 | return job.data; 14 | } catch (error) { 15 | console.error('Error sending email:', error); 16 | throw error; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/bullmq/noti-birthday.processor.ts: -------------------------------------------------------------------------------- 1 | import { Processor, WorkerHost } from '@nestjs/bullmq'; 2 | import { Job } from 'bullmq'; 3 | 4 | @Processor('noti-birthday') 5 | export class NotiBirthdayProcessor extends WorkerHost { 6 | constructor() { 7 | super(); 8 | } 9 | 10 | async process(job: Job): Promise { 11 | try { 12 | console.log('data', job.data); 13 | return job.data; 14 | } catch (error) { 15 | console.error('Error sending email:', error); 16 | throw error; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/bullmq/noti-system.processor.ts: -------------------------------------------------------------------------------- 1 | import { Processor, WorkerHost } from '@nestjs/bullmq'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Job } from 'bullmq'; 4 | import { GatewayGateway } from 'src/gateway/gategate.gateway'; 5 | import { NotificationUser } from 'src/notification-users/entities/notification-user.entity'; 6 | import { RedisService } from 'src/redis/redis.service'; 7 | import { User } from 'src/users/entities/user.entity'; 8 | import { Repository } from 'typeorm'; 9 | import { v4 as uuidv4 } from 'uuid'; 10 | 11 | @Processor('noti-system') 12 | export class NotiSystemProcessor extends WorkerHost { 13 | constructor( 14 | @InjectRepository(User) 15 | private readonly usersRepository: Repository, 16 | @InjectRepository(NotificationUser) 17 | private readonly notiUserRepository: Repository, 18 | private readonly gatewayGateway: GatewayGateway, 19 | private readonly redisService: RedisService, 20 | ) { 21 | super(); 22 | } 23 | 24 | /** 25 | * Processes a job to send system notifications to all users. 26 | * 27 | * This function retrieves all users and creates a new `NotificationUser` entry 28 | * for each user with the provided notification data. After saving the notification 29 | * entry, it sends the notification to the user using the `GatewayGateway`. 30 | * 31 | * @param job - The job containing data attributes: 32 | * - `notification_id`: The ID of the notification to be sent. 33 | * - `title`: The title of the notification message. 34 | * - `message`: The content of the notification message. 35 | * 36 | * @throws Will log and rethrow any errors encountered during processing. 37 | */ 38 | async process(job: Job): Promise { 39 | try { 40 | const notification_id: string = job.data['notification_id']; 41 | 42 | const users = await this.usersRepository.find({ 43 | select: ['id'], 44 | }); 45 | users.map(async (user) => { 46 | const notificationUser = new NotificationUser(); 47 | notificationUser.id = uuidv4(); 48 | notificationUser.notification_id = notification_id; 49 | notificationUser.user_id = user.id; 50 | notificationUser.is_sent = false; 51 | 52 | // Check user status 53 | const userStatus = await this.redisService.get( 54 | `connection_number:${user.id}`, 55 | ); 56 | 57 | if (!userStatus || parseInt(userStatus) <= 0) { 58 | notificationUser.is_sent = false; 59 | } else { 60 | notificationUser.is_sent = true; 61 | 62 | // Send notification use socket 63 | this.gatewayGateway.sendNotification({ 64 | user_id: user.id, 65 | noti_user_id: notificationUser.id, 66 | notification_type: 'system', 67 | title: job.data['title'], 68 | message: job.data['message'], 69 | }); 70 | } 71 | // Save notification in database 72 | await this.notiUserRepository.save(notificationUser); 73 | }); 74 | 75 | return; 76 | } catch (error) { 77 | throw error; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/bullmq/send-email.processor.ts: -------------------------------------------------------------------------------- 1 | import { MailerService } from '@nest-modules/mailer'; 2 | import { Processor, WorkerHost } from '@nestjs/bullmq'; 3 | import { Job } from 'bullmq'; 4 | 5 | @Processor('send-email') 6 | export class SendEmailProcessor extends WorkerHost { 7 | constructor(private readonly mailerService: MailerService) { 8 | super(); 9 | } 10 | 11 | async process(job: Job): Promise { 12 | try { 13 | await this.mailerService.sendMail({ 14 | to: job.data['email'], 15 | subject: 'Mạng xã hội SNet', 16 | template: `./${job.data['template']}`, 17 | context: { 18 | username: job.data['username'], 19 | otp: job.data['otp'], 20 | }, 21 | }); 22 | } catch (error) { 23 | console.error('Error sending email:', error); 24 | throw error; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/chat-members/chat-members.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Post } from '@nestjs/common'; 2 | import { ChatMembersService } from './chat-members.service'; 3 | import { ApiOperation, ApiTags } from '@nestjs/swagger'; 4 | import { ResponseMessage, User } from 'src/decorator/customize'; 5 | import { IUser } from 'src/users/users.interface'; 6 | import { RequestJoinChatRoomDto } from './dto/request-join-chat-room.dto'; 7 | 8 | @ApiTags('Chat Members') 9 | @Controller('chat-members') 10 | export class ChatMembersController { 11 | constructor(private readonly chatMembersService: ChatMembersService) {} 12 | 13 | @Post() 14 | @ResponseMessage('Request join chat room successfully') 15 | @ApiOperation({ 16 | summary: 'Request join chat room for user not exits in chat room', 17 | }) 18 | requestJoinChatRoom( 19 | @Body() dto: RequestJoinChatRoomDto, 20 | @User() user: IUser, 21 | ) { 22 | return this.chatMembersService.requestJoinChatRoom(dto, user); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/chat-members/chat-members.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Module } from '@nestjs/common'; 2 | import { ChatMembersService } from './chat-members.service'; 3 | import { ChatMembersController } from './chat-members.controller'; 4 | import { ChatMember } from './entities/chat-member.entity'; 5 | import { TypeOrmModule } from '@nestjs/typeorm'; 6 | import { RedisModule } from 'src/redis/redis.module'; 7 | import { MulterModule } from '@nestjs/platform-express'; 8 | import { MulterConfigService } from 'src/core/multer.config'; 9 | import { ChatRoomsModule } from 'src/chat-rooms/chat-rooms.module'; 10 | import { UsersModule } from 'src/users/users.module'; 11 | import { WaitingMembers } from './entities/waiting-members.entity'; 12 | 13 | @Module({ 14 | imports: [ 15 | TypeOrmModule.forFeature([WaitingMembers]), 16 | TypeOrmModule.forFeature([ChatMember]), 17 | RedisModule, 18 | MulterModule.registerAsync({ 19 | useClass: MulterConfigService, 20 | }), 21 | forwardRef(() => ChatRoomsModule), 22 | UsersModule, 23 | ], 24 | controllers: [ChatMembersController], 25 | providers: [ChatMembersService], 26 | exports: [ChatMembersService], 27 | }) 28 | export class ChatMembersModule {} 29 | -------------------------------------------------------------------------------- /src/chat-members/dto/request-join-chat-room.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsString, IsUUID } from 'class-validator'; 2 | 3 | export class RequestJoinChatRoomDto { 4 | @IsNotEmpty() 5 | @IsUUID() 6 | @IsString() 7 | chat_room_id: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/chat-members/entities/chat-member.entity.ts: -------------------------------------------------------------------------------- 1 | import { ChatRoom } from 'src/chat-rooms/entities/chat-room.entity'; 2 | import { MemberType } from 'src/helper/member.enum'; 3 | import { User } from 'src/users/entities/user.entity'; 4 | import { 5 | Entity, 6 | Column, 7 | PrimaryGeneratedColumn, 8 | ManyToOne, 9 | JoinColumn, 10 | CreateDateColumn, 11 | Index, 12 | } from 'typeorm'; 13 | 14 | @Entity() 15 | export class ChatMember { 16 | @PrimaryGeneratedColumn('uuid') 17 | id: string; 18 | 19 | @Index() 20 | @Column() 21 | chat_room_id: string; 22 | 23 | @Index() 24 | @Column() 25 | user_id: string; 26 | 27 | @Index() 28 | @Column({ 29 | type: 'enum', 30 | enum: MemberType, 31 | default: MemberType.MEMBER, 32 | }) 33 | member_type: MemberType; 34 | 35 | @Column({ default: '' }) 36 | nickname: string; 37 | 38 | @Column({ default: true }) 39 | allow_notification: boolean; 40 | 41 | @CreateDateColumn() 42 | created_at: Date; 43 | 44 | @ManyToOne(() => User, (user) => user.chat_members) 45 | @JoinColumn({ name: 'user_id' }) 46 | user: User; 47 | 48 | @ManyToOne(() => ChatRoom, (chatRoom) => chatRoom.chat_members) 49 | @JoinColumn({ name: 'chat_room_id' }) 50 | chat_room: ChatRoom; 51 | } 52 | -------------------------------------------------------------------------------- /src/chat-members/entities/waiting-members.entity.ts: -------------------------------------------------------------------------------- 1 | import { ChatRoom } from 'src/chat-rooms/entities/chat-room.entity'; 2 | import { User } from 'src/users/entities/user.entity'; 3 | import { 4 | Column, 5 | Entity, 6 | Index, 7 | JoinColumn, 8 | ManyToOne, 9 | PrimaryGeneratedColumn, 10 | } from 'typeorm'; 11 | 12 | @Entity() 13 | export class WaitingMembers { 14 | @PrimaryGeneratedColumn('uuid') 15 | id: string; 16 | 17 | @Index() 18 | @Column() 19 | chat_room_id: string; 20 | 21 | @Index() 22 | @Column() 23 | user_id: string; 24 | 25 | @ManyToOne(() => ChatRoom, (chatRoom) => chatRoom.waiting_members) 26 | @JoinColumn({ name: 'chat_room_id' }) 27 | chat_room: ChatRoom; 28 | 29 | @ManyToOne(() => User, (user) => user.waiting_members) 30 | @JoinColumn({ name: 'user_id' }) 31 | user: User; 32 | } 33 | -------------------------------------------------------------------------------- /src/chat-messages/chat-messages.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Post, 5 | UploadedFiles, 6 | UseInterceptors, 7 | } from '@nestjs/common'; 8 | import { ChatMessagesService } from './chat-messages.service'; 9 | import { ResponseMessage, User } from 'src/decorator/customize'; 10 | import { ApiOperation, ApiTags } from '@nestjs/swagger'; 11 | import { IUser } from 'src/users/users.interface'; 12 | import { CreateChatMessageDto } from './dto/create-chat-message.dto'; 13 | import { FilesInterceptor } from '@nestjs/platform-express'; 14 | 15 | @ApiTags('Chat Messages') 16 | @Controller('chat-messages') 17 | export class ChatMessagesController { 18 | constructor(private readonly chatMessagesService: ChatMessagesService) {} 19 | 20 | @Post() 21 | @ResponseMessage('Create message to chat successfully') 22 | @ApiOperation({ summary: 'Create message to chat' }) 23 | @UseInterceptors(FilesInterceptor('medias-messages')) 24 | createMessage( 25 | @Body() dto: CreateChatMessageDto, 26 | @User() user: IUser, 27 | @UploadedFiles() 28 | file: Express.Multer.File, 29 | ) { 30 | return this.chatMessagesService.createMessage(dto, user, file); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/chat-messages/chat-messages.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ChatMessagesService } from './chat-messages.service'; 3 | import { ChatMessagesController } from './chat-messages.controller'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { ChatMessage } from './entities/chat-message.entity'; 6 | import { MulterModule } from '@nestjs/platform-express'; 7 | import { MulterConfigService } from 'src/core/multer.config'; 8 | 9 | @Module({ 10 | imports: [ 11 | TypeOrmModule.forFeature([ChatMessage]), 12 | MulterModule.registerAsync({ 13 | useClass: MulterConfigService, 14 | }), 15 | ], 16 | controllers: [ChatMessagesController], 17 | providers: [ChatMessagesService], 18 | }) 19 | export class ChatMessagesModule {} 20 | -------------------------------------------------------------------------------- /src/chat-messages/chat-messages.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ChatMessage } from './entities/chat-message.entity'; 3 | import { InjectRepository } from '@nestjs/typeorm'; 4 | import { Repository } from 'typeorm'; 5 | import { CreateChatMessageDto } from './dto/create-chat-message.dto'; 6 | import { IUser } from 'src/users/users.interface'; 7 | 8 | @Injectable() 9 | export class ChatMessagesService { 10 | constructor( 11 | @InjectRepository(ChatMessage) 12 | private readonly chatMessagesRepository: Repository, 13 | ) {} 14 | async createMessage( 15 | dto: CreateChatMessageDto, 16 | user: IUser, 17 | file: Express.Multer.File, 18 | ) { 19 | console.log(dto, user, file); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/chat-messages/dto/create-chat-message.dto.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IsNotEmpty, 3 | IsOptional, 4 | IsString, 5 | IsUUID, 6 | MaxLength, 7 | } from 'class-validator'; 8 | 9 | export class CreateChatMessageDto { 10 | @IsNotEmpty() 11 | @IsUUID() 12 | @IsString() 13 | chat_room_id: string; 14 | 15 | @IsOptional() 16 | @IsString() 17 | @MaxLength(1000) 18 | message: string; 19 | 20 | @IsOptional() 21 | @IsString() 22 | @IsNotEmpty() 23 | medias: string; 24 | } 25 | -------------------------------------------------------------------------------- /src/chat-messages/dto/update-chat-message.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/swagger'; 2 | import { CreateChatMessageDto } from './create-chat-message.dto'; 3 | 4 | export class UpdateChatMessageDto extends PartialType(CreateChatMessageDto) {} 5 | -------------------------------------------------------------------------------- /src/chat-messages/entities/chat-message.entity.ts: -------------------------------------------------------------------------------- 1 | import { ChatRoom } from 'src/chat-rooms/entities/chat-room.entity'; 2 | import { MessageStatusType } from 'src/helper/message-status.enum'; 3 | import { PinMessage } from 'src/pin-messages/entities/pin-messages.entity'; 4 | import { User } from 'src/users/entities/user.entity'; 5 | import { 6 | Column, 7 | CreateDateColumn, 8 | Entity, 9 | Index, 10 | JoinColumn, 11 | ManyToOne, 12 | OneToOne, 13 | PrimaryGeneratedColumn, 14 | } from 'typeorm'; 15 | 16 | @Entity() 17 | export class ChatMessage { 18 | @PrimaryGeneratedColumn('uuid') 19 | id: string; 20 | 21 | @Index() 22 | @Column() 23 | chat_room_id: string; 24 | 25 | @Column() 26 | created_by: string; 27 | 28 | @Column() 29 | @Index() 30 | message: string; 31 | 32 | @Index() 33 | @Column('text', { array: true, default: null }) 34 | medias: string[]; 35 | 36 | @Index() 37 | @Column({ 38 | type: 'enum', 39 | enum: MessageStatusType, 40 | default: MessageStatusType.NORMAL, 41 | }) 42 | message_status: MessageStatusType; 43 | 44 | @ManyToOne(() => ChatRoom, (chatRoom) => chatRoom.chat_messages) 45 | @JoinColumn({ name: 'chat_room_id' }) 46 | chat_room: ChatRoom; 47 | 48 | @ManyToOne(() => User, (user) => user.chat_messages) 49 | @JoinColumn({ name: 'created_by' }) 50 | user: User; 51 | 52 | @CreateDateColumn() 53 | created_at: Date; 54 | 55 | @OneToOne(() => PinMessage, (pinMessage) => pinMessage.chat_message) 56 | pin_messages: PinMessage; 57 | } 58 | -------------------------------------------------------------------------------- /src/chat-rooms/chat-rooms.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Patch, 5 | Post, 6 | Get, 7 | Param, 8 | NotFoundException, 9 | Delete, 10 | UseInterceptors, 11 | UploadedFile, 12 | Query, 13 | } from '@nestjs/common'; 14 | import { ChatRoomsService } from './chat-rooms.service'; 15 | import { ApiOperation, ApiTags } from '@nestjs/swagger'; 16 | import { CreateChatRoomDto } from './dto/create-chat-room.dto'; 17 | import { IUser } from 'src/users/users.interface'; 18 | import { ResponseMessage, User } from 'src/decorator/customize'; 19 | import { UpdateChatRoomDto } from './dto/update-chat-room.dto'; 20 | import { isUUID } from 'class-validator'; 21 | import { FileInterceptor } from '@nestjs/platform-express'; 22 | import IdDto from 'src/helper/id.dto'; 23 | import { PaginationDto } from 'src/helper/pagination.dto'; 24 | import { UpdatePermissionAddMemberDto } from './dto/update-permission-add-member.dto'; 25 | 26 | @ApiTags('Chat Rooms') 27 | @Controller('chat-rooms') 28 | export class ChatRoomsController { 29 | constructor(private readonly chatRoomsService: ChatRoomsService) {} 30 | 31 | @Get(':id') 32 | @ResponseMessage('Find chat room success') 33 | @ApiOperation({ summary: 'Find chat room' }) 34 | findChatRoomById(@Param('id') id: string) { 35 | if (!isUUID(id)) throw new NotFoundException('Id does not type uuid'); 36 | const room = this.chatRoomsService.findChatRoomByID(id); 37 | if (!room) throw new NotFoundException('Not found chat room'); 38 | 39 | return room; 40 | } 41 | 42 | @Get('list-chat-room') 43 | @ResponseMessage('Get list chat room success') 44 | @ApiOperation({ summary: 'Get list chat room' }) 45 | getListChatRoom(@User() user: IUser, @Query() query: PaginationDto) { 46 | return this.chatRoomsService.getListChatRoom(user, query); 47 | } 48 | 49 | @Post() 50 | @ResponseMessage('Create chat room success') 51 | @ApiOperation({ summary: 'Create chat room' }) 52 | createChatRoom(@Body() dto: CreateChatRoomDto, @User() user: IUser) { 53 | return this.chatRoomsService.createChatRoom(dto, user); 54 | } 55 | 56 | @Patch() 57 | @ResponseMessage('Update name or avatar chat room success') 58 | @ApiOperation({ summary: 'Update name or avatar chat room' }) 59 | @UseInterceptors(FileInterceptor('avatar-chat-room')) 60 | updateNameOrAvatar( 61 | @Body() dto: UpdateChatRoomDto, 62 | @User() user: IUser, 63 | @UploadedFile() 64 | file: Express.Multer.File, 65 | ) { 66 | return this.chatRoomsService.updateNameOrAvatar(dto, user, file); 67 | } 68 | 69 | @Patch('permission-add-member') 70 | @ResponseMessage('Update permission add member success') 71 | @ApiOperation({ summary: 'Update permission add member' }) 72 | updatePermissionAddMember( 73 | @Body() dto: UpdatePermissionAddMemberDto, 74 | @User() user: IUser, 75 | ) { 76 | return this.chatRoomsService.updatePermissionAddMember(dto, user); 77 | } 78 | 79 | @Delete() 80 | @ResponseMessage('Delete chat room success') 81 | @ApiOperation({ summary: 'Delete chat room' }) 82 | deleteChatRoom(@Body() dto: IdDto, @User() user: IUser) { 83 | return this.chatRoomsService.deleteChatRoom(dto, user); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/chat-rooms/chat-rooms.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Module } from '@nestjs/common'; 2 | import { ChatRoomsService } from './chat-rooms.service'; 3 | import { ChatRoomsController } from './chat-rooms.controller'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { ChatRoom } from './entities/chat-room.entity'; 6 | import { NotificationModule } from 'src/notifications/notifications.module'; 7 | import { RedisModule } from 'src/redis/redis.module'; 8 | import { MulterModule } from '@nestjs/platform-express'; 9 | import { MulterConfigService } from 'src/core/multer.config'; 10 | import { ChatMembersModule } from 'src/chat-members/chat-members.module'; 11 | import { ChatMember } from 'src/chat-members/entities/chat-member.entity'; 12 | 13 | @Module({ 14 | imports: [ 15 | TypeOrmModule.forFeature([ChatRoom]), 16 | TypeOrmModule.forFeature([ChatMember]), 17 | NotificationModule, 18 | RedisModule, 19 | MulterModule.registerAsync({ 20 | useClass: MulterConfigService, 21 | }), 22 | forwardRef(() => ChatMembersModule), 23 | ], 24 | controllers: [ChatRoomsController], 25 | providers: [ChatRoomsService], 26 | exports: [ChatRoomsService], 27 | }) 28 | export class ChatRoomsModule {} 29 | -------------------------------------------------------------------------------- /src/chat-rooms/dto/create-chat-room.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator'; 3 | 4 | export class CreateChatRoomDto { 5 | @IsString() 6 | @MaxLength(30) 7 | @MinLength(3) 8 | @IsNotEmpty() 9 | @ApiProperty({ example: 'Group Chat' }) 10 | name: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/chat-rooms/dto/update-chat-room.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { 3 | IsNotEmpty, 4 | IsString, 5 | IsUUID, 6 | MaxLength, 7 | MinLength, 8 | } from 'class-validator'; 9 | 10 | export class UpdateChatRoomDto { 11 | @IsUUID() 12 | @IsNotEmpty() 13 | id: string; 14 | 15 | @IsString() 16 | @MaxLength(30) 17 | @MinLength(3) 18 | @IsNotEmpty() 19 | @ApiProperty({ example: 'Group Chat' }) 20 | name: string; 21 | } 22 | -------------------------------------------------------------------------------- /src/chat-rooms/dto/update-permission-add-member.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsEnum, IsNotEmpty, IsUUID } from 'class-validator'; 3 | import { MemberType } from 'src/helper/member.enum'; 4 | 5 | export class UpdatePermissionAddMemberDto { 6 | @IsUUID() 7 | @IsNotEmpty() 8 | id: string; 9 | 10 | @IsEnum(MemberType) 11 | @ApiProperty({ 12 | example: 'member', 13 | }) 14 | new_permission_add_member: MemberType; 15 | } 16 | -------------------------------------------------------------------------------- /src/chat-rooms/entities/chat-room.entity.ts: -------------------------------------------------------------------------------- 1 | import { ChatMember } from 'src/chat-members/entities/chat-member.entity'; 2 | import { WaitingMembers } from 'src/chat-members/entities/waiting-members.entity'; 3 | import { ChatMessage } from 'src/chat-messages/entities/chat-message.entity'; 4 | import { MemberType } from 'src/helper/member.enum'; 5 | import { ReactionType } from 'src/helper/reaction.enum'; 6 | import { PinChat } from 'src/pin-chats/entities/pin-chat.entity'; 7 | import { PinMessage } from 'src/pin-messages/entities/pin-messages.entity'; 8 | import { User } from 'src/users/entities/user.entity'; 9 | 10 | import { 11 | Column, 12 | CreateDateColumn, 13 | Entity, 14 | Index, 15 | JoinColumn, 16 | ManyToOne, 17 | OneToMany, 18 | PrimaryGeneratedColumn, 19 | } from 'typeorm'; 20 | 21 | @Entity() 22 | export class ChatRoom { 23 | @PrimaryGeneratedColumn('uuid') 24 | id: string; 25 | 26 | @Column() 27 | @Index() 28 | name: string; 29 | 30 | @Index() 31 | @Column({ default: 'chat-room.png' }) 32 | avatar: string; 33 | 34 | @Column() 35 | created_by: string; 36 | 37 | @Column({ type: 'enum', enum: MemberType, default: MemberType.MEMBER }) 38 | permission_add_member: MemberType; 39 | 40 | @CreateDateColumn() 41 | created_at: Date; 42 | 43 | @Index() 44 | @Column({ type: 'enum', enum: ReactionType, default: ReactionType.LIKE }) 45 | reaction_default: ReactionType; 46 | 47 | @Column({ default: false }) 48 | end_to_end_encryption: boolean; 49 | 50 | @ManyToOne(() => User, (user) => user.chat_rooms) 51 | @JoinColumn({ name: 'created_by' }) 52 | user: User; 53 | 54 | @OneToMany(() => ChatMember, (chatMember) => chatMember.chat_room) 55 | chat_members: ChatMember[]; 56 | 57 | @OneToMany(() => ChatMessage, (chatMessage) => chatMessage.chat_room) 58 | chat_messages: ChatMessage[]; 59 | 60 | @ManyToOne(() => PinMessage, (pinMessage) => pinMessage.chat_message) 61 | pin_messages: PinMessage[]; 62 | 63 | @OneToMany(() => PinChat, (pinChat) => pinChat.chat_room) 64 | pin_chats: PinChat[]; 65 | 66 | @OneToMany(() => WaitingMembers, (waitingMembers) => waitingMembers.chat_room) 67 | waiting_members: WaitingMembers[]; 68 | } 69 | -------------------------------------------------------------------------------- /src/comments/comments.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common'; 2 | import { CommentsService } from './comments.service'; 3 | 4 | @Controller('comments') 5 | export class CommentsController { 6 | constructor(private readonly commentsService: CommentsService) {} 7 | } 8 | -------------------------------------------------------------------------------- /src/comments/comments.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { CommentsService } from './comments.service'; 3 | import { CommentsController } from './comments.controller'; 4 | 5 | @Module({ 6 | controllers: [CommentsController], 7 | providers: [CommentsService], 8 | }) 9 | export class CommentsModule {} 10 | -------------------------------------------------------------------------------- /src/comments/comments.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class CommentsService {} 5 | -------------------------------------------------------------------------------- /src/comments/dto/create-comment.dto.ts: -------------------------------------------------------------------------------- 1 | export class CreateCommentDto {} 2 | -------------------------------------------------------------------------------- /src/comments/dto/update-comment.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/swagger'; 2 | import { CreateCommentDto } from './create-comment.dto'; 3 | 4 | export class UpdateCommentDto extends PartialType(CreateCommentDto) {} 5 | -------------------------------------------------------------------------------- /src/comments/entities/comment.entity.ts: -------------------------------------------------------------------------------- 1 | import { ParentChildComment } from 'src/parent-child-comments/entities/parent-child-comment.entity'; 2 | import { Post } from 'src/posts/entities/post.entity'; 3 | import { User } from 'src/users/entities/user.entity'; 4 | import { 5 | Column, 6 | CreateDateColumn, 7 | Entity, 8 | Index, 9 | JoinColumn, 10 | ManyToOne, 11 | OneToMany, 12 | PrimaryGeneratedColumn, 13 | } from 'typeorm'; 14 | 15 | @Entity() 16 | export class Comment { 17 | @PrimaryGeneratedColumn('uuid') 18 | id: string; 19 | 20 | @Index() 21 | @Column() 22 | user_id: string; 23 | 24 | @Column() 25 | @Index() 26 | post_id: string; 27 | 28 | @Index() 29 | @Column() 30 | content: string; 31 | 32 | @Index() 33 | @Column('text', { array: true, default: null }) 34 | medias: string[]; 35 | 36 | @CreateDateColumn() 37 | created_at: Date; 38 | 39 | @ManyToOne(() => User, (user) => user.comments) 40 | @JoinColumn({ name: 'user_id' }) 41 | user: User; 42 | 43 | @ManyToOne(() => Post, (post) => post.comments) 44 | @JoinColumn({ name: 'post_id' }) 45 | post: Post; 46 | 47 | @OneToMany( 48 | () => ParentChildComment, 49 | (parentChildComment) => parentChildComment.parentComment, 50 | ) 51 | childComments: ParentChildComment[]; 52 | 53 | @OneToMany( 54 | () => ParentChildComment, 55 | (parentChildComment) => parentChildComment.childComment, 56 | ) 57 | parentComments: ParentChildComment[]; 58 | } 59 | -------------------------------------------------------------------------------- /src/core/multer.config.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { 3 | MulterModuleOptions, 4 | MulterOptionsFactory, 5 | } from '@nestjs/platform-express'; 6 | import { diskStorage } from 'multer'; 7 | import * as fs from 'fs'; 8 | import * as path from 'path'; 9 | 10 | @Injectable() 11 | export class MulterConfigService implements MulterOptionsFactory { 12 | getRootPath = () => { 13 | return process.cwd(); 14 | }; 15 | ensureExists(targetDirectory: string) { 16 | fs.mkdir(targetDirectory, { recursive: true }, (error) => { 17 | if (!error) { 18 | console.log('Directory successfully created, or it already exists.'); 19 | return; 20 | } else { 21 | } 22 | }); 23 | } 24 | 25 | createMulterOptions(): MulterModuleOptions { 26 | return { 27 | storage: diskStorage({ 28 | destination: (req, file, cb) => { 29 | const folder = req?.headers?.folder_type ?? 'default'; 30 | this.ensureExists(`public/${folder}`); 31 | cb(null, path.join(this.getRootPath(), `public/${folder}`)); 32 | }, 33 | filename: (req, file, cb) => { 34 | //get image extension 35 | const extName = path.extname(file.originalname); 36 | //get image's name (without extension) 37 | const baseName = path.basename(file.originalname, extName); 38 | const finalName = `${baseName}-${Date.now()}${extName}`; 39 | cb(null, finalName); 40 | }, 41 | }), 42 | }; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/core/transform.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | NestInterceptor, 4 | ExecutionContext, 5 | CallHandler, 6 | } from '@nestjs/common'; 7 | import { Reflector } from '@nestjs/core'; 8 | import { Observable } from 'rxjs'; 9 | import { map } from 'rxjs/operators'; 10 | import { RESPONSE_MESSAGE } from 'src/decorator/customize'; 11 | 12 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 13 | export interface Response { 14 | statusCode: number; 15 | // message: string; 16 | data: any; 17 | } 18 | 19 | @Injectable() 20 | export class TransformInterceptor 21 | implements NestInterceptor> 22 | { 23 | constructor(private reflector: Reflector) {} 24 | 25 | intercept( 26 | context: ExecutionContext, 27 | next: CallHandler, 28 | ): Observable> { 29 | return next.handle().pipe( 30 | map((data) => ({ 31 | statusCode: context.switchToHttp().getResponse().statusCode, 32 | message: 33 | this.reflector.get(RESPONSE_MESSAGE, context.getHandler()) || 34 | '', 35 | data: data, 36 | })), 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/database/database.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { DatabaseProviders } from './database.providers'; 4 | import { ConfigModule, ConfigService } from '@nestjs/config'; 5 | 6 | @Module({ 7 | imports: [ 8 | TypeOrmModule.forRootAsync({ 9 | imports: [ConfigModule], 10 | inject: [ConfigService], 11 | useFactory: async (configService: ConfigService) => ({ 12 | type: 'postgres', 13 | host: configService.get('DATABASE_HOST'), 14 | port: configService.get('DATABASE_PORT'), 15 | username: configService.get('DATABASE_USERNAME'), 16 | password: configService.get('DATABASE_PASSWORD'), 17 | database: configService.get('DATABASE_NAME'), 18 | entities: [__dirname + '/../**/*.entity{.ts,.js}'], 19 | synchronize: true, 20 | }), 21 | }), 22 | ], 23 | providers: [...DatabaseProviders], 24 | exports: [...DatabaseProviders, 'DATA_SOURCE'], 25 | }) 26 | export class DatabaseModule {} 27 | -------------------------------------------------------------------------------- /src/database/database.providers.ts: -------------------------------------------------------------------------------- 1 | import { DataSource } from 'typeorm'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { Provider } from '@nestjs/common'; 4 | 5 | export const DatabaseProviders: Provider[] = [ 6 | { 7 | provide: 'DATA_SOURCE', 8 | useFactory: async (configService: ConfigService) => { 9 | const dataSource = new DataSource({ 10 | type: 'postgres', 11 | host: configService.get('DATABASE_HOST'), 12 | port: configService.get('DATABASE_PORT'), 13 | username: configService.get('DATABASE_USERNAME'), 14 | password: configService.get('DATABASE_PASSWORD'), 15 | database: configService.get('DATABASE_NAME'), 16 | entities: [__dirname + '/../**/*.entity{.ts,.js}'], 17 | synchronize: true, 18 | }); 19 | return dataSource.initialize(); 20 | }, 21 | inject: [ConfigService], 22 | }, 23 | ]; 24 | -------------------------------------------------------------------------------- /src/decorator/customize.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createParamDecorator, 3 | ExecutionContext, 4 | SetMetadata, 5 | } from '@nestjs/common'; 6 | 7 | export const IS_PUBLIC_KEY = 'isPublic'; 8 | export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); 9 | 10 | // Interceptor 11 | export const RESPONSE_MESSAGE = 'response_message'; 12 | export const ResponseMessage = (message: string) => 13 | SetMetadata(RESPONSE_MESSAGE, message); 14 | 15 | export const User = createParamDecorator( 16 | (data: unknown, ctx: ExecutionContext) => { 17 | const request = ctx.switchToHttp().getRequest(); 18 | return request.user; 19 | }, 20 | ); 21 | 22 | export const Admin = createParamDecorator( 23 | (data: unknown, ctx: ExecutionContext) => { 24 | const request = ctx.switchToHttp().getRequest(); 25 | return request.user; 26 | }, 27 | ); 28 | -------------------------------------------------------------------------------- /src/device-sessions/activate.gateway.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ntthanh2603/SNet-Backend/3316950608a69517b1e6ea685ea5041ef9b56d2f/src/device-sessions/activate.gateway.ts -------------------------------------------------------------------------------- /src/device-sessions/device-sessions.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Post, Req } from '@nestjs/common'; 2 | import { DeviceSessionsService } from './device-sessions.service'; 3 | import { Public, ResponseMessage, User } from 'src/decorator/customize'; 4 | import { IUser } from 'src/users/users.interface'; 5 | import { ApiOperation, ApiTags } from '@nestjs/swagger'; 6 | import { Fingerprint, IFingerprint } from 'nestjs-fingerprint'; 7 | import { Request } from 'express'; 8 | 9 | @ApiTags('DeviceSessions') 10 | @Controller('device-sessions') 11 | export class DeviceSessionsController { 12 | constructor(private readonly deviceSessionsService: DeviceSessionsService) {} 13 | 14 | @Post('/logout') 15 | @ApiOperation({ summary: 'Đăng xuất tài khoản' }) 16 | @ResponseMessage('Đăng xuất tài khoản thành công') 17 | hendleLogout(@Fingerprint() fp: IFingerprint, @User() user: IUser) { 18 | return this.deviceSessionsService.logout(user, fp['id']); 19 | } 20 | 21 | @Public() 22 | @ResponseMessage('Cấp lại access token thành công') 23 | @ApiOperation({ summary: 'Cấp lại access token' }) 24 | @Get('/refresh-token') 25 | reAuth(@Fingerprint() fp: IFingerprint, @Req() request: Request) { 26 | const refreshToken = request.cookies['refreshToken']; 27 | return this.deviceSessionsService.reAuth(refreshToken, fp['id']); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/device-sessions/device-sessions.module.ts: -------------------------------------------------------------------------------- 1 | import { RedisModule } from './../redis/redis.module'; 2 | import { forwardRef, Module } from '@nestjs/common'; 3 | import { DeviceSessionsService } from './device-sessions.service'; 4 | import { DeviceSessionsController } from './device-sessions.controller'; 5 | import { TypeOrmModule } from '@nestjs/typeorm'; 6 | import { DeviceSession } from './entities/device-session.entity'; 7 | import { UsersModule } from 'src/users/users.module'; 8 | import { AuthModule } from 'src/auth/auth.module'; 9 | 10 | @Module({ 11 | imports: [ 12 | TypeOrmModule.forFeature([DeviceSession]), 13 | forwardRef(() => UsersModule), 14 | forwardRef(() => AuthModule), 15 | RedisModule, 16 | ], 17 | controllers: [DeviceSessionsController], 18 | providers: [DeviceSessionsService], 19 | exports: [DeviceSessionsService], 20 | }) 21 | export class DeviceSessionsModule {} 22 | -------------------------------------------------------------------------------- /src/device-sessions/entities/device-session.entity.ts: -------------------------------------------------------------------------------- 1 | import { User } from 'src/users/entities/user.entity'; 2 | import { 3 | Column, 4 | Entity, 5 | Index, 6 | JoinColumn, 7 | ManyToOne, 8 | PrimaryGeneratedColumn, 9 | UpdateDateColumn, 10 | } from 'typeorm'; 11 | 12 | @Entity() 13 | export class DeviceSession { 14 | @PrimaryGeneratedColumn('uuid') 15 | id: string; 16 | 17 | @Index() 18 | @Column() 19 | device_id: string; 20 | 21 | @Index() 22 | @Column() 23 | ip_address: string; 24 | 25 | @Index() 26 | @Column() 27 | secret_key: string; 28 | 29 | @Index() 30 | @Column({ default: null }) 31 | refresh_token: string; 32 | 33 | @Index() 34 | @Column() 35 | expired_at: Date; 36 | 37 | @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) 38 | created_at: Date; 39 | 40 | @UpdateDateColumn() 41 | updated_at: Date; 42 | 43 | @Index() 44 | @Column() 45 | user_id: string; 46 | 47 | @ManyToOne(() => User, (user) => user.device_sessions) 48 | @JoinColumn({ name: 'user_id' }) 49 | user: User; 50 | } 51 | -------------------------------------------------------------------------------- /src/gateway/gategate.gateway.ts: -------------------------------------------------------------------------------- 1 | import { NotificationUsersService } from './../notification-users/notification-users.service'; 2 | import { 3 | WebSocketGateway, 4 | WebSocketServer, 5 | OnGatewayConnection, 6 | OnGatewayDisconnect, 7 | OnGatewayInit, 8 | ConnectedSocket, 9 | SubscribeMessage, 10 | MessageBody, 11 | } from '@nestjs/websockets'; 12 | import { Server, Socket } from 'socket.io'; 13 | import { RedisService } from 'src/redis/redis.service'; 14 | import { WsAuthMiddleware } from './ws-auth.middleware'; 15 | import { INotiUser } from 'src/notifications/notification.interface'; 16 | 17 | /* 18 | Client -->|Gửi tin| Redis; 19 | Redis -->|Phản hồi nhanh| Client; 20 | Redis -->|Lưu vào hàng đợi| BullMQ; 21 | BullMQ -->|Ghi tin nhắn vào DB| PostgreSQL; 22 | Client -->|Yêu cầu tin nhắn cũ| PostgreSQL; 23 | */ 24 | 25 | @WebSocketGateway({ 26 | cors: true, 27 | pingInterval: 10000, 28 | pingTimeout: 20000, 29 | }) 30 | export class GatewayGateway 31 | implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit 32 | { 33 | @WebSocketServer() 34 | server: Server; 35 | 36 | constructor( 37 | private readonly redisService: RedisService, 38 | private readonly wsAuthMiddleware: WsAuthMiddleware, 39 | private readonly notificationUsersService: NotificationUsersService, 40 | ) {} 41 | 42 | // Initialize WebSocket 43 | afterInit() { 44 | console.log('WebSocket initialized'); 45 | this.server.use((socket: Socket, next) => 46 | this.wsAuthMiddleware.use(socket, next), 47 | ); 48 | } 49 | 50 | // Handle connection 51 | async handleConnection(socket: Socket) { 52 | // Increase connection number 53 | socket.join(socket.data.user.id); 54 | await this.redisService.incr(`connection_number:${socket.data.user.id}`); 55 | } 56 | 57 | // Handle disconnection 58 | async handleDisconnect(@ConnectedSocket() socket: Socket) { 59 | // get connection number 60 | const connectionNumber = await this.redisService.get( 61 | `connection_number:${socket.data.user.id}`, 62 | ); 63 | 64 | // Decrease connection number 65 | if (parseInt(connectionNumber) === 1) { 66 | await this.redisService.del(`connection_number:${socket.data.user.id}`); 67 | } else { 68 | await this.redisService.decr(`connection_number:${socket.data.user.id}`); 69 | } 70 | } 71 | 72 | // Send notification 73 | async sendNotification(noti: INotiUser) { 74 | this.server.to(noti.user_id).emit('notification', noti); 75 | } 76 | 77 | // User read notification 78 | @SubscribeMessage('readNotification') 79 | handleReadNotification( 80 | @ConnectedSocket() socket: Socket, 81 | @MessageBody() body: object, 82 | ) { 83 | this.notificationUsersService.readNoti( 84 | socket.data.user.id, 85 | body['noti_user_id'], 86 | ); 87 | } 88 | 89 | // Join a chat room 90 | @SubscribeMessage('joinRoom') 91 | async handleJoinRoom( 92 | @ConnectedSocket() socket: Socket, 93 | @MessageBody() roomId: string, 94 | ) { 95 | socket.join(roomId); 96 | console.log(`User ${socket.data.user.id} joined room ${roomId}`); 97 | 98 | // Get message history 99 | const messages = await this.redisService.lRange(`chat:${roomId}`, 0, -1); 100 | 101 | // Send message history to client 102 | socket.emit( 103 | 'messageHistory', 104 | messages.map((msg) => JSON.parse(msg)), 105 | ); 106 | } 107 | 108 | // Leave a chat room 109 | @SubscribeMessage('leaveRoom') 110 | async handleLeaveRoom( 111 | @ConnectedSocket() socket: Socket, 112 | @MessageBody() roomId: string, 113 | ) { 114 | socket.leave(roomId); 115 | console.log(`User ${socket.data.user.id} left room ${roomId}`); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/gateway/gateway.module.ts: -------------------------------------------------------------------------------- 1 | import { GatewayGateway } from './gategate.gateway'; 2 | import { Module } from '@nestjs/common'; 3 | import { GatewayService } from './gateway.service'; 4 | import { AuthModule } from 'src/auth/auth.module'; 5 | import { WsAuthMiddleware } from './ws-auth.middleware'; 6 | import { NotificationUsersModule } from 'src/notification-users/notification-users.module'; 7 | 8 | @Module({ 9 | imports: [AuthModule, NotificationUsersModule], 10 | providers: [GatewayService, GatewayGateway, WsAuthMiddleware], 11 | exports: [GatewayGateway], 12 | }) 13 | export class GatewayModule {} 14 | -------------------------------------------------------------------------------- /src/gateway/gateway.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class GatewayService {} 5 | -------------------------------------------------------------------------------- /src/gateway/ws-auth.middleware.ts: -------------------------------------------------------------------------------- 1 | // middleware/auth.middleware.ts 2 | import { Injectable, NestMiddleware } from '@nestjs/common'; 3 | import { Socket } from 'socket.io'; 4 | import { AuthService } from 'src/auth/auth.service'; 5 | 6 | @Injectable() 7 | export class WsAuthMiddleware implements NestMiddleware { 8 | constructor(private readonly authService: AuthService) {} 9 | async use(socket: Socket, next: (err?: any) => void) { 10 | const authToken: any = socket.handshake.headers.authorization; 11 | 12 | if (!authToken) { 13 | return next(new Error('Authentication error: No token provided')); 14 | } 15 | 16 | try { 17 | // Verify token 18 | const user = await this.authService.verify(authToken); 19 | // Attach user to socket 20 | socket.data.user = user; 21 | 22 | next(); 23 | } catch { 24 | next(new Error('Authentication error: Invalid token')); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/helper/activate.enum.ts: -------------------------------------------------------------------------------- 1 | export enum ActivateType { 2 | ONLINE = 'online', 3 | OFFLINE = 'offline', 4 | } 5 | -------------------------------------------------------------------------------- /src/helper/addDay.ts: -------------------------------------------------------------------------------- 1 | export default function addDay(days: number) { 2 | const d = new Date(); 3 | d.setDate(d.getDate() + days); 4 | 5 | return d; 6 | } 7 | -------------------------------------------------------------------------------- /src/helper/deleteFile.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException } from '@nestjs/common'; 2 | import * as fs from 'fs'; 3 | 4 | export default function deleteFile(filePath: string): void { 5 | try { 6 | if (fs.existsSync(filePath)) { 7 | fs.unlinkSync(filePath); 8 | } 9 | } catch { 10 | throw new BadRequestException('Error deleting file'); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/helper/gender.enum.ts: -------------------------------------------------------------------------------- 1 | export enum GenderType { 2 | MALE = 'male', 3 | FEMALE = 'female', 4 | OTHER = 'other', 5 | } 6 | -------------------------------------------------------------------------------- /src/helper/id.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsUUID } from 'class-validator'; 2 | 3 | export default class IdDto { 4 | @IsNotEmpty() 5 | @IsUUID() 6 | id: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/helper/member.enum.ts: -------------------------------------------------------------------------------- 1 | export enum MemberType { 2 | ADMIN = 'admin', 3 | MEMBER = 'member', 4 | } 5 | -------------------------------------------------------------------------------- /src/helper/message-status.enum.ts: -------------------------------------------------------------------------------- 1 | export enum MessageStatusType { 2 | NORMAL = 'normal', 3 | EDITED = 'edited', 4 | DELETED = 'deleted', 5 | } 6 | -------------------------------------------------------------------------------- /src/helper/notification.enum.ts: -------------------------------------------------------------------------------- 1 | export enum NotificationType { 2 | LIKE = 'like', 3 | COMMENT = 'comment', 4 | FOLLOW = 'follow', 5 | REACTION = 'reaction', 6 | SYSTEM = 'system', 7 | } 8 | -------------------------------------------------------------------------------- /src/helper/pagination.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiPropertyOptional } from '@nestjs/swagger'; 2 | import { IsOptional, IsNumber, Min } from 'class-validator'; 3 | import { Type } from 'class-transformer'; 4 | 5 | export class PaginationDto { 6 | @ApiPropertyOptional({ default: 1 }) 7 | @IsNumber() 8 | @IsOptional() 9 | @Type(() => Number) 10 | @Min(1) 11 | page?: number = 1; 12 | 13 | @ApiPropertyOptional({ default: 10 }) 14 | @IsNumber() 15 | @IsOptional() 16 | @Type(() => Number) 17 | @Min(1) 18 | limit?: number = 10; 19 | } 20 | -------------------------------------------------------------------------------- /src/helper/privacy.enum.ts: -------------------------------------------------------------------------------- 1 | export enum PrivacyType { 2 | PUBLIC = 'public', 3 | FRIEND = 'friend', 4 | PRIVATE = 'private', 5 | } 6 | -------------------------------------------------------------------------------- /src/helper/reaction.enum.ts: -------------------------------------------------------------------------------- 1 | export enum ReactionType { 2 | LIKE = 'like', 3 | LOVE = 'love', 4 | HAHA = 'haha', 5 | WOW = 'wow', 6 | SAD = 'sad', 7 | ANGRY = 'angry', 8 | } 9 | -------------------------------------------------------------------------------- /src/helper/relation.enum.ts: -------------------------------------------------------------------------------- 1 | export enum RelationType { 2 | FRIEND = 'friend', 3 | FOLLOWING = 'following', 4 | BLOCK = 'block', 5 | NONE = 'none', 6 | } 7 | -------------------------------------------------------------------------------- /src/helper/role.enum.ts: -------------------------------------------------------------------------------- 1 | export enum RoleType { 2 | USER = 'user', 3 | ADMIN = 'admin', 4 | } 5 | -------------------------------------------------------------------------------- /src/helper/user-category.enum.ts: -------------------------------------------------------------------------------- 1 | export enum UserCategoryType { 2 | IDOL = 'idol', 3 | CASUALUSER = 'casualuser', 4 | BLOCK = 'block', 5 | } 6 | -------------------------------------------------------------------------------- /src/logger/log-nest.service.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/restrict-template-expressions */ 2 | import { Injectable, LoggerService } from '@nestjs/common'; 3 | import { createLogger, Logger, format, transports } from 'winston'; 4 | import * as dayjs from 'dayjs'; 5 | import * as chalk from 'chalk'; 6 | import * as path from 'path'; 7 | import 'winston-daily-rotate-file'; 8 | 9 | /** 10 | error: 0 11 | warn: 1 12 | info: 2 13 | http: 3 14 | verbose: 4 15 | debug: 5 16 | silly: 6 17 | */ 18 | 19 | @Injectable() 20 | export class LogNestService implements LoggerService { 21 | private logger: Logger; 22 | 23 | constructor() { 24 | this.logger = createLogger({ 25 | level: 'debug', 26 | transports: [ 27 | new transports.Console({ 28 | format: format.combine( 29 | format.printf(({ context, message, level, time }) => { 30 | const pid = process.pid; 31 | const strApp = chalk.green('[Nest]'); 32 | const strPid = chalk.green(`${pid}`); 33 | const strContext = chalk.yellow(`[${context}]`); 34 | 35 | let formattedLevel = ''; 36 | switch (level) { 37 | case 'info': 38 | formattedLevel = chalk.green('LOG'); 39 | break; 40 | case 'error': 41 | formattedLevel = chalk.red('ERROR'); 42 | break; 43 | case 'warn': 44 | formattedLevel = chalk.yellow('WARN'); 45 | break; 46 | case 'debug': 47 | formattedLevel = chalk.magenta('DEBUG'); 48 | break; 49 | default: 50 | formattedLevel = level.toUpperCase(); 51 | } 52 | 53 | return `${strApp} ${strPid} - ${time} ${formattedLevel} ${strContext} ${message}`; 54 | }), 55 | ), 56 | }), 57 | new transports.DailyRotateFile({ 58 | level: 'debug', 59 | dirname: path.join(__dirname, '../../nestjs-logs/'), 60 | filename: 'app-%DATE%.log', 61 | datePattern: 'YYYY-MM-DD', 62 | maxSize: '100m', 63 | format: format.combine( 64 | format.printf(({ context, message, level, time }) => { 65 | const pid = process.pid; 66 | const formattedLevel = level.toUpperCase(); 67 | return `[Nest] ${pid} - ${time} ${formattedLevel} [${context}] ${message}`; 68 | }), 69 | ), 70 | }), 71 | ], 72 | }); 73 | } 74 | 75 | log(message: string, context: string) { 76 | const time = dayjs().format('DD/MM/YYYY, h:mm:ss A'); 77 | this.logger.log('info', message, { context, time }); 78 | } 79 | 80 | error(message: string, context: string) { 81 | const time = dayjs().format('DD/MM/YYYY, h:mm:ss A'); 82 | this.logger.error(message, { context, time }); 83 | } 84 | 85 | warn(message: string, context: string) { 86 | const time = dayjs().format('DD/MM/YYYY, h:mm:ss A'); 87 | this.logger.warn(message, { context, time }); 88 | } 89 | 90 | debug(message: string, context: string) { 91 | const time = dayjs().format('DD/MM/YYYY, h:mm:ss A'); 92 | this.logger.debug(message, { context, time }); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/logger/logger.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | import { LogNestService } from './log-nest.service'; 3 | 4 | @Global() 5 | @Module({ 6 | providers: [LogNestService], 7 | exports: [LogNestService], 8 | }) 9 | export class LoggerModule {} 10 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory, Reflector } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import { ConfigService } from '@nestjs/config'; 4 | import { Logger, ValidationPipe } from '@nestjs/common'; 5 | import { TransformInterceptor } from './core/transform.interceptor'; 6 | import { JwtAuthGuard } from './auth/jwt-auth.guard'; 7 | import * as cookieParser from 'cookie-parser'; 8 | import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; 9 | import helmet from 'helmet'; 10 | import { IoAdapter } from '@nestjs/platform-socket.io'; 11 | 12 | async function bootstrap() { 13 | const app = await NestFactory.create(AppModule); 14 | 15 | const configService = app.get(ConfigService); 16 | 17 | app.useWebSocketAdapter(new IoAdapter(app)); 18 | 19 | app.use( 20 | helmet({ 21 | contentSecurityPolicy: { 22 | directives: { 23 | defaultSrc: ["'self'"], 24 | scriptSrc: ["'self'", "'unsafe-inline'", 'https://trusted.cdn.com'], 25 | styleSrc: [ 26 | "'self'", 27 | "'unsafe-inline'", 28 | 'https://fonts.googleapis.com', 29 | ], 30 | imgSrc: ["'self'", 'data:', 'https://images.trusted.com'], 31 | fontSrc: ["'self'", 'https://fonts.gstatic.com'], 32 | objectSrc: ["'none'"], 33 | }, 34 | }, 35 | hsts: false, // Tắt HSTS để chạy HTTP 36 | }), 37 | ); 38 | 39 | const reflector = app.get(Reflector); 40 | app.useGlobalGuards(new JwtAuthGuard(reflector)); 41 | 42 | app.use(cookieParser()); 43 | 44 | app.useGlobalPipes(new ValidationPipe()); 45 | 46 | // Interceptor 47 | app.useGlobalInterceptors(new TransformInterceptor(reflector)); 48 | 49 | // Config CORS 50 | app.enableCors({ 51 | origin: ['http://localhost:3000', 'http://localhost:5173'], 52 | methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', 53 | allowedHeaders: ['Content-Type', 'Authorization'], 54 | credentials: true, // Allow send cookie and Authorization header 55 | }); 56 | 57 | // Config swagger 58 | const config = new DocumentBuilder() 59 | .setTitle('Project social spaces') 60 | .setDescription('All Module API') 61 | .addBearerAuth( 62 | { 63 | type: 'http', 64 | scheme: 'Bearer', 65 | bearerFormat: 'JWT', 66 | in: 'header', 67 | }, 68 | 'token', 69 | ) 70 | .addSecurityRequirements('token') 71 | .setVersion('1.0') 72 | .build(); 73 | 74 | // Để mở documentation thì dùng lệnh sau npx @compodoc/compodoc -p tsconfig.json -s 75 | // Rồi vào cổng http://localhost:8080/ để mở documentation 76 | const documentFactory = () => SwaggerModule.createDocument(app, config); 77 | SwaggerModule.setup('swagger', app, documentFactory, { 78 | swaggerOptions: { 79 | persistAuthorization: true, 80 | }, 81 | }); 82 | 83 | const logger = new Logger('Social-Network-SNET'); 84 | await app.listen(configService.get('PORT'), () => { 85 | logger.log( 86 | `Server running on port http://localhost:${configService.get('PORT')}`, 87 | ); 88 | }); 89 | } 90 | bootstrap(); 91 | -------------------------------------------------------------------------------- /src/notification-users/dto/delete-noti-user.dto.ts: -------------------------------------------------------------------------------- 1 | export class DeleteNotificationUserDto { 2 | noti_user_id: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/notification-users/entities/notification-user.entity.ts: -------------------------------------------------------------------------------- 1 | import { Notification } from 'src/notifications/entities/notification.entity'; 2 | import { User } from 'src/users/entities/user.entity'; 3 | import { 4 | Column, 5 | Entity, 6 | Index, 7 | JoinColumn, 8 | ManyToOne, 9 | PrimaryGeneratedColumn, 10 | } from 'typeorm'; 11 | 12 | @Entity() 13 | export class NotificationUser { 14 | @PrimaryGeneratedColumn('uuid') 15 | id: string; 16 | 17 | @Index() 18 | @Column() 19 | notification_id: string; 20 | 21 | @Index() 22 | @Column() 23 | user_id: string; 24 | 25 | @Index() 26 | @Column({ default: false }) 27 | is_sent: boolean; 28 | 29 | @Index() 30 | @Column({ default: false }) 31 | is_read: boolean; 32 | 33 | @ManyToOne( 34 | () => Notification, 35 | (notification) => notification.notification_user, 36 | ) 37 | @JoinColumn({ name: 'notification_id' }) 38 | notification: Notification; 39 | 40 | @ManyToOne(() => User, (user) => user.notification_users) 41 | @JoinColumn({ name: 'user_id' }) 42 | user: User; 43 | } 44 | -------------------------------------------------------------------------------- /src/notification-users/notification-users.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Delete } from '@nestjs/common'; 2 | import { NotificationUsersService } from './notification-users.service'; 3 | import { ApiOperation, ApiTags } from '@nestjs/swagger'; 4 | import { IUser } from 'src/users/users.interface'; 5 | import { DeleteNotificationUserDto } from './dto/delete-noti-user.dto'; 6 | import { ResponseMessage, User } from 'src/decorator/customize'; 7 | 8 | @Controller('notification-users') 9 | @ApiTags('Notification Users') 10 | export class NotificationUsersController { 11 | constructor(private readonly notiUsersService: NotificationUsersService) {} 12 | 13 | @Delete() 14 | @ApiOperation({ summary: 'Delete notification' }) 15 | @ResponseMessage('Delete notification successfully') 16 | deleteNoti(@User() user: IUser, @Body() dto: DeleteNotificationUserDto) { 17 | return this.notiUsersService.deleteNoti(user, dto); 18 | } 19 | 20 | @Delete('all') 21 | @ApiOperation({ summary: 'Delete all notification' }) 22 | @ResponseMessage('Delete all notification successfully') 23 | deleteAllNoti(@User() user: IUser) { 24 | return this.notiUsersService.deleteAllNoti(user); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/notification-users/notification-users.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { NotificationUsersService } from './notification-users.service'; 3 | import { NotificationUsersController } from './notification-users.controller'; 4 | import { NotificationUser } from './entities/notification-user.entity'; 5 | import { TypeOrmModule } from '@nestjs/typeorm'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([NotificationUser])], 9 | controllers: [NotificationUsersController], 10 | providers: [NotificationUsersService], 11 | exports: [NotificationUsersService], 12 | }) 13 | export class NotificationUsersModule {} 14 | -------------------------------------------------------------------------------- /src/notification-users/notification-users.service.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { NotificationUser } from './entities/notification-user.entity'; 4 | import { Repository } from 'typeorm'; 5 | import { IUser } from 'src/users/users.interface'; 6 | import { DeleteNotificationUserDto } from './dto/delete-noti-user.dto'; 7 | 8 | @Injectable() 9 | export class NotificationUsersService { 10 | constructor( 11 | @InjectRepository(NotificationUser) 12 | private readonly notiUserRepository: Repository, 13 | ) {} 14 | 15 | async readNoti(user_id: string, noti_user_id: string) { 16 | await this.notiUserRepository.update( 17 | { id: noti_user_id, user_id: user_id }, 18 | { is_read: true }, 19 | ); 20 | } 21 | 22 | async deleteNoti(user: IUser, dto: DeleteNotificationUserDto) { 23 | const result = await this.notiUserRepository.delete({ 24 | id: dto.noti_user_id, 25 | user_id: user.id, 26 | }); 27 | 28 | if (result.affected === 0) { 29 | throw new BadRequestException('Notification not found'); 30 | } 31 | } 32 | 33 | async deleteAllNoti(user: IUser) { 34 | const result = await this.notiUserRepository.delete({ 35 | user_id: user.id, 36 | }); 37 | 38 | if (result.affected === 0) { 39 | throw new BadRequestException('Notification not found'); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/notifications/dto/create-noti-system.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator'; 3 | 4 | export class CreateNotiSystemDto { 5 | @IsString() 6 | @IsNotEmpty() 7 | @MaxLength(64) 8 | @MinLength(1) 9 | @ApiProperty({ example: 'Thông báo hệ thống' }) 10 | title: string; 11 | 12 | @IsString() 13 | @IsNotEmpty() 14 | @MaxLength(256) 15 | @MinLength(1) 16 | @ApiProperty({ example: 'Hệ thống quá tốt' }) 17 | message: string; 18 | } 19 | -------------------------------------------------------------------------------- /src/notifications/entities/notification.entity.ts: -------------------------------------------------------------------------------- 1 | import { NotificationType } from 'src/helper/notification.enum'; 2 | import { NotificationUser } from 'src/notification-users/entities/notification-user.entity'; 3 | import { 4 | Column, 5 | CreateDateColumn, 6 | Entity, 7 | Index, 8 | OneToMany, 9 | PrimaryGeneratedColumn, 10 | } from 'typeorm'; 11 | 12 | @Entity() 13 | export class Notification { 14 | @PrimaryGeneratedColumn('uuid') 15 | id: string; 16 | 17 | @Index() 18 | @Column({ default: null }) 19 | title: string; 20 | 21 | @Index() 22 | @Column({ default: null }) 23 | message: string; 24 | 25 | @Column({ type: 'enum', enum: NotificationType }) 26 | notification_type: NotificationType; 27 | 28 | @CreateDateColumn() 29 | created_at: Date; 30 | 31 | @OneToMany( 32 | () => NotificationUser, 33 | (notificationUser) => notificationUser.notification, 34 | ) 35 | notification_user: NotificationUser[]; 36 | } 37 | -------------------------------------------------------------------------------- /src/notifications/notification.interface.ts: -------------------------------------------------------------------------------- 1 | export interface INotiUser { 2 | user_id: string; 3 | noti_user_id: string; 4 | title: string; 5 | message: string; 6 | notification_type?: string; 7 | created_at?: Date; 8 | is_read?: boolean; 9 | } 10 | -------------------------------------------------------------------------------- /src/notifications/notifications.controller.ts: -------------------------------------------------------------------------------- 1 | import { ApiOperation, ApiTags } from '@nestjs/swagger'; 2 | import { NotificationService } from './notifications.service'; 3 | import { Body, Controller, Post, UseGuards } from '@nestjs/common'; 4 | 5 | import { AdminGuard } from 'src/admins/admin.guard'; 6 | import { IAdmin } from 'src/admins/admin.interface'; 7 | import { Admin, ResponseMessage } from 'src/decorator/customize'; 8 | import { CreateNotiSystemDto } from './dto/create-noti-system.dto'; 9 | 10 | @ApiTags('Notifications') 11 | @Controller('notifications') 12 | export class NotificationController { 13 | constructor(private readonly notificationService: NotificationService) {} 14 | 15 | @UseGuards(AdminGuard) 16 | @Post() 17 | @ApiOperation({ summary: 'Admin: Create notification system' }) 18 | @ResponseMessage('Create notification system successfully') 19 | createNotiSystem(@Admin() admin: IAdmin, @Body() dto: CreateNotiSystemDto) { 20 | return this.notificationService.createNotiSystem(admin, dto); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/notifications/notifications.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { NotificationController } from './notifications.controller'; 4 | import { NotificationService } from './notifications.service'; 5 | import { Notification } from './entities/notification.entity'; 6 | import { ConfigModule } from '@nestjs/config'; 7 | import { BullModule } from '@nestjs/bullmq'; 8 | 9 | @Module({ 10 | imports: [ 11 | ConfigModule, 12 | TypeOrmModule.forFeature([Notification]), 13 | BullModule.registerQueue({ name: 'noti-system' }), 14 | ], 15 | controllers: [NotificationController], 16 | providers: [NotificationService], 17 | exports: [NotificationService], 18 | }) 19 | export class NotificationModule {} 20 | -------------------------------------------------------------------------------- /src/notifications/notifications.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Repository } from 'typeorm'; 4 | import { Notification } from './entities/notification.entity'; 5 | import { IAdmin } from 'src/admins/admin.interface'; 6 | import { CreateNotiSystemDto } from './dto/create-noti-system.dto'; 7 | import { InjectQueue } from '@nestjs/bullmq'; 8 | import { Queue } from 'bullmq'; 9 | import { NotificationType } from 'src/helper/notification.enum'; 10 | import { v4 as uuidv4 } from 'uuid'; 11 | 12 | @Injectable() 13 | export class NotificationService { 14 | constructor( 15 | @InjectRepository(Notification) 16 | private readonly notificationRepo: Repository, 17 | @InjectQueue('noti-system') private notiQueue: Queue, 18 | ) {} 19 | 20 | /** 21 | * Creates a system notification and queues it for distribution to all users. 22 | * 23 | * This function generates a new notification entry with the details provided 24 | * in the `CreateNotiSystemDto`, assigns it a unique ID, and categorizes it 25 | * as a system notification. The notification is saved to the database, and 26 | * a job is added to the notification queue to broadcast the notification to 27 | * all users. Logs the creation of the notification with the admin's ID for 28 | * auditing purposes. 29 | * 30 | * @param admin - The admin initiating the creation of the notification. 31 | * @param dto - Data transfer object containing the title and message of the notification. 32 | */ 33 | async createNotiSystem(admin: IAdmin, dto: CreateNotiSystemDto) { 34 | // Create notification system 35 | const notification = new Notification(); 36 | notification.id = uuidv4(); 37 | notification.title = dto.title; 38 | notification.message = dto.message; 39 | notification.notification_type = NotificationType.SYSTEM; 40 | 41 | await this.notificationRepo.save(notification); 42 | 43 | // Notification for all user 44 | this.notiQueue.add( 45 | 'system', 46 | { 47 | notification_id: notification.id, 48 | title: dto.title, 49 | message: dto.message, 50 | }, 51 | { removeOnComplete: true }, 52 | ); 53 | // Log 54 | 55 | return; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/parent-child-comments/dto/create-parent-child-comment.dto.ts: -------------------------------------------------------------------------------- 1 | export class CreateParentChildCommentDto {} 2 | -------------------------------------------------------------------------------- /src/parent-child-comments/dto/update-parent-child-comment.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/swagger'; 2 | import { CreateParentChildCommentDto } from './create-parent-child-comment.dto'; 3 | 4 | export class UpdateParentChildCommentDto extends PartialType(CreateParentChildCommentDto) {} 5 | -------------------------------------------------------------------------------- /src/parent-child-comments/entities/parent-child-comment.entity.ts: -------------------------------------------------------------------------------- 1 | import { Comment } from 'src/comments/entities/comment.entity'; 2 | import { Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm'; 3 | 4 | @Entity() 5 | export class ParentChildComment { 6 | @Index() 7 | @PrimaryColumn() 8 | parent_comment: string; 9 | 10 | @Index() 11 | @PrimaryColumn() 12 | child_comment: string; 13 | 14 | @ManyToOne(() => Comment, (comment) => comment.childComments) 15 | @JoinColumn({ name: 'parent_comment' }) 16 | parentComment: Comment; 17 | 18 | @ManyToOne(() => Comment, (comment) => comment.parentComments) 19 | @JoinColumn({ name: 'child_comment' }) 20 | childComment: Comment; 21 | } 22 | -------------------------------------------------------------------------------- /src/parent-child-comments/parent-child-comments.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common'; 2 | import { ParentChildCommentsService } from './parent-child-comments.service'; 3 | 4 | @Controller('parent-child-comments') 5 | export class ParentChildCommentsController { 6 | constructor( 7 | private readonly parentChildCommentsService: ParentChildCommentsService, 8 | ) {} 9 | } 10 | -------------------------------------------------------------------------------- /src/parent-child-comments/parent-child-comments.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ParentChildCommentsService } from './parent-child-comments.service'; 3 | import { ParentChildCommentsController } from './parent-child-comments.controller'; 4 | 5 | @Module({ 6 | controllers: [ParentChildCommentsController], 7 | providers: [ParentChildCommentsService], 8 | }) 9 | export class ParentChildCommentsModule {} 10 | -------------------------------------------------------------------------------- /src/parent-child-comments/parent-child-comments.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class ParentChildCommentsService {} 5 | -------------------------------------------------------------------------------- /src/pin-chats/entities/pin-chat.entity.ts: -------------------------------------------------------------------------------- 1 | import { ChatRoom } from 'src/chat-rooms/entities/chat-room.entity'; 2 | import { User } from 'src/users/entities/user.entity'; 3 | import { 4 | Column, 5 | Entity, 6 | Index, 7 | JoinColumn, 8 | ManyToOne, 9 | PrimaryGeneratedColumn, 10 | } from 'typeorm'; 11 | 12 | @Entity() 13 | export class PinChat { 14 | @PrimaryGeneratedColumn('uuid') 15 | id: string; 16 | 17 | @Index() 18 | @Column() 19 | chat_room_id: string; 20 | 21 | @Index() 22 | @Column() 23 | user_id: string; 24 | 25 | @ManyToOne(() => ChatRoom, (chatRoom) => chatRoom.pin_chats) 26 | @JoinColumn({ name: 'chat_room_id' }) 27 | chat_room: ChatRoom; 28 | 29 | @ManyToOne(() => User, (user) => user.pin_chats) 30 | @JoinColumn({ name: 'user_id' }) 31 | user: User; 32 | } 33 | -------------------------------------------------------------------------------- /src/pin-chats/pin-chats.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common'; 2 | import { PinChatsService } from './pin-chats.service'; 3 | import { ApiTags } from '@nestjs/swagger'; 4 | 5 | @ApiTags('Pin Chats') 6 | @Controller('pin-chats') 7 | export class PinChatsController { 8 | constructor(private readonly pinChatsService: PinChatsService) {} 9 | } 10 | -------------------------------------------------------------------------------- /src/pin-chats/pin-chats.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PinChatsService } from './pin-chats.service'; 3 | import { PinChatsController } from './pin-chats.controller'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { PinChat } from './entities/pin-chat.entity'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([PinChat])], 9 | controllers: [PinChatsController], 10 | providers: [PinChatsService], 11 | }) 12 | export class PinChatsModule {} 13 | -------------------------------------------------------------------------------- /src/pin-chats/pin-chats.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class PinChatsService {} 5 | -------------------------------------------------------------------------------- /src/pin-messages/entities/pin-messages.entity.ts: -------------------------------------------------------------------------------- 1 | import { ChatMessage } from 'src/chat-messages/entities/chat-message.entity'; 2 | import { ChatRoom } from 'src/chat-rooms/entities/chat-room.entity'; 3 | import { 4 | Column, 5 | Entity, 6 | Index, 7 | JoinColumn, 8 | ManyToOne, 9 | OneToOne, 10 | PrimaryGeneratedColumn, 11 | } from 'typeorm'; 12 | 13 | @Entity() 14 | export class PinMessage { 15 | @PrimaryGeneratedColumn('uuid') 16 | id: string; 17 | 18 | @Index() 19 | @Column() 20 | chat_room_id: string; 21 | 22 | @Index() 23 | @Column() 24 | chat_message_id: string; 25 | 26 | @OneToOne(() => ChatMessage, (chatMessage) => chatMessage.pin_messages) 27 | @JoinColumn({ name: 'chat_message_id' }) 28 | chat_message: ChatMessage; 29 | 30 | @ManyToOne(() => ChatRoom, (chatRoom) => chatRoom.pin_messages) 31 | @JoinColumn({ name: 'chat_room_id' }) 32 | chat_room: ChatRoom; 33 | } 34 | -------------------------------------------------------------------------------- /src/pin-messages/pin-messages.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common'; 2 | import { PinMessagesService } from './pin-messages.service'; 3 | import { ApiTags } from '@nestjs/swagger'; 4 | 5 | @ApiTags('pin-messages') 6 | @Controller('pin-messages') 7 | export class PinMessagesController { 8 | constructor(private readonly pinMessagesService: PinMessagesService) {} 9 | } 10 | -------------------------------------------------------------------------------- /src/pin-messages/pin-messages.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PinMessagesService } from './pin-messages.service'; 3 | import { PinMessagesController } from './pin-messages.controller'; 4 | import { PinMessage } from './entities/pin-messages.entity'; 5 | import { TypeOrmModule } from '@nestjs/typeorm'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([PinMessage])], 9 | controllers: [PinMessagesController], 10 | providers: [PinMessagesService], 11 | }) 12 | export class PinMessagesModule {} 13 | -------------------------------------------------------------------------------- /src/pin-messages/pin-messages.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class PinMessagesService {} 5 | -------------------------------------------------------------------------------- /src/posts/dto/create-post.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { 3 | IsEnum, 4 | IsNotEmpty, 5 | IsOptional, 6 | IsString, 7 | MaxLength, 8 | MinLength, 9 | } from 'class-validator'; 10 | import { PrivacyType } from 'src/helper/privacy.enum'; 11 | 12 | export class CreatePostDto { 13 | @IsString() 14 | @IsOptional() 15 | @MaxLength(256) 16 | @MinLength(1) 17 | @ApiProperty({ example: 'Hello world', description: 'content' }) 18 | content: string; 19 | 20 | @IsOptional() 21 | @ApiProperty({ 22 | example: ['human', 'it', 'life'], 23 | description: 'hashtags', 24 | }) 25 | hashtags: string[]; 26 | 27 | @IsEnum(PrivacyType) 28 | @IsNotEmpty() 29 | @ApiProperty({ example: PrivacyType.PUBLIC, description: 'privacy' }) 30 | privacy: PrivacyType; 31 | } 32 | -------------------------------------------------------------------------------- /src/posts/dto/update-post.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { 3 | IsArray, 4 | IsEnum, 5 | IsNotEmpty, 6 | IsOptional, 7 | IsString, 8 | MaxLength, 9 | MinLength, 10 | } from 'class-validator'; 11 | import { PrivacyType } from 'src/helper/privacy.enum'; 12 | 13 | export class UpdatePostDto { 14 | // @IsUUID() 15 | @IsNotEmpty() 16 | @ApiProperty({ 17 | example: '452a1e21-d4eb-4f14-a52f-fbfaf0bb7d99', 18 | description: 'id', 19 | }) 20 | id: string; 21 | 22 | @IsString() 23 | @IsOptional() 24 | @MaxLength(256) 25 | @MinLength(0) 26 | @ApiProperty({ example: 'Hello world', description: 'content' }) 27 | content: string; 28 | 29 | @IsOptional() 30 | @ApiProperty({ 31 | example: ['img1.jpg', 'img2.jpg', 'img3.jpg'], 32 | description: 'medias', 33 | }) 34 | @IsArray() 35 | medias: string[]; 36 | 37 | @IsEnum(PrivacyType) 38 | @IsNotEmpty() 39 | @ApiProperty({ example: PrivacyType.PUBLIC, description: 'privacy' }) 40 | privacy: PrivacyType; 41 | } 42 | -------------------------------------------------------------------------------- /src/posts/dto/update-privacy-post.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsEnum, IsNotEmpty, IsUUID } from 'class-validator'; 3 | import { PrivacyType } from 'src/helper/privacy.enum'; 4 | 5 | export class UpdatePrivacyPostDto { 6 | @IsUUID() 7 | id: string; 8 | 9 | @IsEnum(PrivacyType) 10 | @IsNotEmpty() 11 | @ApiProperty({ example: PrivacyType.PUBLIC, description: 'privacy' }) 12 | privacy: PrivacyType; 13 | } 14 | -------------------------------------------------------------------------------- /src/posts/entities/post.entity.ts: -------------------------------------------------------------------------------- 1 | import { Comment } from 'src/comments/entities/comment.entity'; 2 | import { PrivacyType } from 'src/helper/privacy.enum'; 3 | import { SavePost } from 'src/save-posts/entities/save-post.entity'; 4 | import { User } from 'src/users/entities/user.entity'; 5 | import { 6 | Column, 7 | Entity, 8 | Index, 9 | JoinColumn, 10 | ManyToOne, 11 | OneToMany, 12 | OneToOne, 13 | PrimaryGeneratedColumn, 14 | } from 'typeorm'; 15 | 16 | @Entity() 17 | export class Post { 18 | @PrimaryGeneratedColumn('uuid') 19 | id: string; 20 | 21 | @Index() 22 | @Column() 23 | user_id: string; 24 | 25 | @Index() 26 | @Column({ default: null }) 27 | content: string; 28 | 29 | @Index() 30 | @Column('text', { array: true, default: null }) 31 | medias: string[]; 32 | 33 | @Column({ default: PrivacyType.PUBLIC, enum: PrivacyType }) 34 | privacy: PrivacyType; 35 | 36 | @Column() 37 | created_at: Date; 38 | 39 | @ManyToOne(() => User, (user) => user.posts) 40 | @JoinColumn({ name: 'user_id' }) 41 | user: User; 42 | 43 | @OneToOne(() => SavePost, (savePost) => savePost.post) 44 | save_posts: SavePost; 45 | 46 | @OneToMany(() => Comment, (comment) => comment.post) 47 | comments: Comment[]; 48 | } 49 | -------------------------------------------------------------------------------- /src/posts/posts.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Post, 5 | Body, 6 | Patch, 7 | Param, 8 | Delete, 9 | UseInterceptors, 10 | HttpStatus, 11 | ParseFilePipeBuilder, 12 | UploadedFiles, 13 | BadRequestException, 14 | } from '@nestjs/common'; 15 | import { PostsService } from './posts.service'; 16 | import { CreatePostDto } from './dto/create-post.dto'; 17 | import { UpdatePostDto } from './dto/update-post.dto'; 18 | import { IUser } from 'src/users/users.interface'; 19 | import { ResponseMessage, User } from 'src/decorator/customize'; 20 | import { ApiBody, ApiOperation, ApiTags } from '@nestjs/swagger'; 21 | import { FilesInterceptor } from '@nestjs/platform-express'; 22 | import { isUUID } from 'class-validator'; 23 | 24 | @ApiTags('Posts') 25 | @Controller('posts') 26 | export class PostsController { 27 | constructor(private readonly postsService: PostsService) {} 28 | 29 | @Post() 30 | @ResponseMessage('Create post successfully') 31 | @ApiOperation({ summary: 'Create post' }) 32 | @UseInterceptors(FilesInterceptor('medias-posts', 10)) 33 | create( 34 | @User() user: IUser, 35 | @Body() createPostDto: CreatePostDto, 36 | @UploadedFiles( 37 | new ParseFilePipeBuilder() 38 | .addFileTypeValidator({ 39 | fileType: /(jpg|jpeg|png|gif)$/, 40 | }) 41 | .addMaxSizeValidator({ 42 | maxSize: 1024 * 1024 * 10, // 10MB 43 | }) 44 | .build({ 45 | errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, 46 | fileIsRequired: false, 47 | }), 48 | ) 49 | file: Express.Multer.File[], 50 | ) { 51 | return this.postsService.create(user, createPostDto, file); 52 | } 53 | 54 | @Get(':id') 55 | @ResponseMessage('Find post by id successfully') 56 | @ApiOperation({ summary: 'Find post by id' }) 57 | findOne(@Param('id') id: string, @User() user: IUser) { 58 | if (!isUUID(id)) { 59 | throw new BadRequestException(`Invalid ID format: ${id}`); 60 | } 61 | return this.postsService.findOne(user, id); 62 | } 63 | 64 | @Patch('all') 65 | @ResponseMessage('Xóa bài viết thành công') 66 | @ApiOperation({ summary: 'Cập nhật bài viết' }) 67 | @ApiBody({ type: UpdatePostDto }) 68 | update(@User() user: IUser, dto: UpdatePostDto) { 69 | return this.postsService.update(user, dto); 70 | } 71 | 72 | @Delete(':id') 73 | @ResponseMessage('Xóa bài viết thành công') 74 | @ApiOperation({ summary: 'Xóa bài viết' }) 75 | remove(@Param('id') id: string) { 76 | return this.postsService.remove(+id); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/posts/posts.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PostsService } from './posts.service'; 3 | import { PostsController } from './posts.controller'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { DatabaseModule } from 'src/database/database.module'; 6 | import { Post } from './entities/post.entity'; 7 | import { RedisModule } from 'src/redis/redis.module'; 8 | import { MulterModule } from '@nestjs/platform-express'; 9 | import { MulterConfigService } from 'src/core/multer.config'; 10 | import { BullModule } from '@nestjs/bullmq'; 11 | 12 | @Module({ 13 | imports: [ 14 | TypeOrmModule.forFeature([Post]), 15 | DatabaseModule, 16 | RedisModule, 17 | MulterModule.registerAsync({ 18 | useClass: MulterConfigService, 19 | }), 20 | BullModule.registerQueue({ name: 'create-posts' }), 21 | ], 22 | controllers: [PostsController], 23 | providers: [PostsService], 24 | exports: [], 25 | }) 26 | export class PostsModule {} 27 | -------------------------------------------------------------------------------- /src/posts/posts.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadRequestException, 3 | Injectable, 4 | InternalServerErrorException, 5 | NotFoundException, 6 | } from '@nestjs/common'; 7 | import { CreatePostDto } from './dto/create-post.dto'; 8 | import { IUser } from 'src/users/users.interface'; 9 | import { InjectRepository } from '@nestjs/typeorm'; 10 | import { Post } from './entities/post.entity'; 11 | import { Repository } from 'typeorm'; 12 | import { RedisService } from 'src/redis/redis.service'; 13 | import { UpdatePostDto } from './dto/update-post.dto'; 14 | import { v4 as uuidv4 } from 'uuid'; 15 | import { Queue } from 'bullmq'; 16 | import { InjectQueue } from '@nestjs/bullmq'; 17 | 18 | @Injectable() 19 | export class PostsService { 20 | constructor( 21 | @InjectRepository(Post) 22 | private repository: Repository, 23 | private readonly redisService: RedisService, 24 | @InjectQueue('create-posts') 25 | private mediasPostsQueue: Queue, 26 | ) {} 27 | 28 | async findPostByID(id: string) { 29 | try { 30 | const postCache = await this.redisService.get(`post:${id}`); 31 | 32 | if (postCache) return postCache; 33 | 34 | const postDb = await this.repository.findOne({ where: { id } }); 35 | if (!postDb) throw new NotFoundException(`Post id: ${id} does not exist`); 36 | 37 | await this.redisService.set(`post:${id}`, JSON.stringify(postDb), 300); 38 | 39 | return postDb; 40 | } catch (error) { 41 | if (error instanceof NotFoundException) throw error; 42 | throw new InternalServerErrorException(`Error find post with id: ${id}`); 43 | } 44 | } 45 | 46 | /** 47 | * Create a new post with the given content, medias and privacy. 48 | * 49 | * @param user The user who creates the post 50 | * @param dto The create post dto 51 | * @param file The uploaded medias 52 | * 53 | * @throws BadRequestException If the content and medias are empty 54 | * @throws InternalServerErrorException If there is an error when creating the post 55 | * 56 | * @returns A message indicating that the post has been created successfully 57 | */ 58 | async create(user: IUser, dto: CreatePostDto, file: Express.Multer.File[]) { 59 | // Check if content and medias are empty 60 | if (!dto.content && !file) 61 | throw new BadRequestException('Content and medias are required'); 62 | 63 | // Map path of file to string 64 | const medias: string[] = file.map((f) => (f ? f.filename : '')); 65 | 66 | try { 67 | // Create a new post 68 | const newPost = new Post(); 69 | newPost.id = uuidv4(); 70 | newPost.user_id = user.id; 71 | newPost.content = dto?.content; 72 | newPost.medias = medias; 73 | newPost.privacy = dto.privacy; 74 | newPost.created_at = new Date(); 75 | 76 | await this.repository.save(newPost); 77 | 78 | // Add job to queue 79 | this.mediasPostsQueue.add( 80 | 'create-posts', 81 | { 82 | ...newPost, 83 | }, 84 | { 85 | removeOnComplete: true, 86 | removeOnFail: true, 87 | }, 88 | ); 89 | return { 90 | message: 'Create post successfully', 91 | }; 92 | } catch (err) { 93 | if (err instanceof BadRequestException) throw err; 94 | 95 | throw new InternalServerErrorException('Error when create post'); 96 | } 97 | } 98 | 99 | findAll() { 100 | return `This action returns all posts`; 101 | } 102 | 103 | findOne(user: IUser, id: string) { 104 | return `This action returns a #${id} post`; 105 | } 106 | 107 | async update(user: IUser, dto: UpdatePostDto) { 108 | await this.findPostByID(dto.id); 109 | } 110 | 111 | remove(id: number) { 112 | return `This action removes a #${id} post`; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/reactions/dto/create-reaction.dto.ts: -------------------------------------------------------------------------------- 1 | export class CreateReactionDto {} 2 | -------------------------------------------------------------------------------- /src/reactions/dto/update-reaction.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/swagger'; 2 | import { CreateReactionDto } from './create-reaction.dto'; 3 | 4 | export class UpdateReactionDto extends PartialType(CreateReactionDto) {} 5 | -------------------------------------------------------------------------------- /src/reactions/entities/reaction.entity.ts: -------------------------------------------------------------------------------- 1 | import { ReactionType } from 'src/helper/reaction.enum'; 2 | import { Column, Entity, Index, PrimaryColumn } from 'typeorm'; 3 | 4 | @Entity() 5 | export class Reaction { 6 | @Index() 7 | @PrimaryColumn() 8 | user_id: string; 9 | 10 | @Index() 11 | @PrimaryColumn() 12 | post_comment_id: string; 13 | 14 | @Column({ enum: ReactionType, default: ReactionType.LIKE }) 15 | reaction: ReactionType; 16 | } 17 | -------------------------------------------------------------------------------- /src/reactions/reactions.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common'; 2 | import { ReactionsService } from './reactions.service'; 3 | 4 | @Controller('reactions') 5 | export class ReactionsController { 6 | constructor(private readonly reactionsService: ReactionsService) {} 7 | } 8 | -------------------------------------------------------------------------------- /src/reactions/reactions.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ReactionsService } from './reactions.service'; 3 | import { ReactionsController } from './reactions.controller'; 4 | 5 | @Module({ 6 | controllers: [ReactionsController], 7 | providers: [ReactionsService], 8 | }) 9 | export class ReactionsModule {} 10 | -------------------------------------------------------------------------------- /src/reactions/reactions.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class ReactionsService {} 5 | -------------------------------------------------------------------------------- /src/redis/redis.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, Global } from '@nestjs/common'; 2 | import { RedisService } from './redis.service'; 3 | import { ConfigModule, ConfigService } from '@nestjs/config'; 4 | import { Redis } from 'ioredis'; 5 | 6 | @Global() 7 | @Module({ 8 | imports: [ConfigModule], 9 | providers: [ 10 | { 11 | provide: 'REDIS_CLIENT', 12 | useFactory: (configService: ConfigService) => { 13 | return new Redis({ 14 | host: configService.get('REDIS_HOST') || 'localhost', 15 | port: configService.get('REDIS_PORT') || 6379, 16 | password: configService.get('REDIS_PASSWORD') || undefined, 17 | db: configService.get('REDIS_DB') || 0, 18 | keyPrefix: configService.get('REDIS_KEY_PREFIX') || '', 19 | tls: configService.get('REDIS_TLS') ? {} : undefined, 20 | maxRetriesPerRequest: null, 21 | }); 22 | }, 23 | inject: [ConfigService], 24 | }, 25 | RedisService, 26 | ], 27 | exports: ['REDIS_CLIENT', RedisService], 28 | }) 29 | export class RedisModule {} 30 | -------------------------------------------------------------------------------- /src/relations/dto/relation.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsEnum, IsNotEmpty, IsUUID } from 'class-validator'; 3 | import { RelationType } from 'src/helper/relation.enum'; 4 | 5 | export class RelationDto { 6 | @IsUUID() 7 | @ApiProperty({ example: '123123123123', description: 'user_id' }) 8 | @IsNotEmpty() 9 | user_id: string; 10 | 11 | @IsEnum(RelationType) 12 | relation: RelationType; 13 | } 14 | -------------------------------------------------------------------------------- /src/relations/dto/update-relation.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsEnum, IsNotEmpty, IsUUID } from 'class-validator'; 3 | import { RelationType } from 'src/helper/relation.enum'; 4 | 5 | export class UpdateRelationDto { 6 | @IsUUID() 7 | @ApiProperty({ example: '123123123123', description: 'ID user other' }) 8 | @IsNotEmpty() 9 | user_id: string; 10 | 11 | @IsEnum(RelationType) 12 | @ApiProperty({ example: RelationType.FOLLOWING }) 13 | @IsNotEmpty() 14 | relation: string; 15 | } 16 | -------------------------------------------------------------------------------- /src/relations/entities/relation.entity.ts: -------------------------------------------------------------------------------- 1 | import { RelationType } from 'src/helper/relation.enum'; 2 | import { User } from 'src/users/entities/user.entity'; 3 | import { 4 | Column, 5 | CreateDateColumn, 6 | Entity, 7 | ManyToOne, 8 | PrimaryGeneratedColumn, 9 | JoinColumn, 10 | Index, 11 | } from 'typeorm'; 12 | 13 | @Entity() 14 | @Index(['request_side_id', 'accept_side_id'], { unique: true }) 15 | export class Relation { 16 | @PrimaryGeneratedColumn('uuid') 17 | id: string; 18 | 19 | @Index() 20 | @Column() 21 | request_side_id: string; 22 | 23 | @Index() 24 | @Column() 25 | accept_side_id: string; 26 | 27 | @ManyToOne(() => User, (user) => user.sent_relations) 28 | @JoinColumn({ name: 'request_side_id' }) 29 | request_side: User; 30 | 31 | @ManyToOne(() => User, (user) => user.received_relations) 32 | @JoinColumn({ name: 'accept_side_id' }) 33 | accept_side: User; 34 | 35 | @Column({ type: 'enum', enum: RelationType }) 36 | relation_type: RelationType; 37 | 38 | @Index() 39 | @CreateDateColumn() 40 | created_at: Date; 41 | } 42 | -------------------------------------------------------------------------------- /src/relations/relations.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Post, 4 | Body, 5 | Get, 6 | BadRequestException, 7 | Param, 8 | Query, 9 | forwardRef, 10 | Inject, 11 | } from '@nestjs/common'; 12 | import { ApiOperation, ApiTags } from '@nestjs/swagger'; 13 | import { User, ResponseMessage } from 'src/decorator/customize'; 14 | import { IUser } from 'src/users/users.interface'; 15 | import { RelationDto } from './dto/relation.dto'; 16 | import { RelationsService } from './relations.service'; 17 | import { UpdateRelationDto } from './dto/update-relation.dto'; 18 | import { UsersService } from 'src/users/users.service'; 19 | 20 | @ApiTags('Relations') 21 | @Controller('relations') 22 | export class RelationsController { 23 | constructor( 24 | private readonly relationShipsService: RelationsService, 25 | @Inject(forwardRef(() => UsersService)) 26 | private readonly usersService: UsersService, 27 | ) {} 28 | 29 | @ResponseMessage( 30 | `Get list relation ['friend', 'following', 'block'] of user successfully`, 31 | ) 32 | @Get('friends/:user_id') 33 | @ApiOperation({ 34 | summary: `Get list relation ['friend', 'following', 'block'] of user`, 35 | }) 36 | async getListRelation( 37 | @User() user: IUser, 38 | @Param() params: RelationDto, 39 | @Query('page') page: number = 1, 40 | @Query('limit') limit: number = 10, 41 | ) { 42 | const privacy = await this.usersService.privacySeeProfile( 43 | user.id, 44 | params.user_id, 45 | ); 46 | if (!privacy) { 47 | throw new BadRequestException('You are not allowed to see list friend'); 48 | } 49 | return this.relationShipsService.getListRelation( 50 | params.user_id, 51 | page, 52 | limit, 53 | params.relation, 54 | ); 55 | } 56 | 57 | @Get(':user_id') 58 | @ResponseMessage('Get relation between 2 users successfully') 59 | @ApiOperation({ summary: 'Get relation between 2 users' }) 60 | async getRelation(@User() user: IUser, @Param() params: RelationDto) { 61 | const relation = await this.relationShipsService.getRelation( 62 | user.id, 63 | params.user_id, 64 | ); 65 | 66 | return relation; 67 | } 68 | 69 | @Post('update') 70 | @ResponseMessage('Update relation successfully') 71 | @ApiOperation({ summary: 'Update relation' }) 72 | updateRelation(@User() user: IUser, @Body() dto: UpdateRelationDto) { 73 | return this.relationShipsService.updateRelation(user, dto); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/relations/relations.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { UsersModule } from 'src/users/users.module'; 4 | import { RelationsService } from './relations.service'; 5 | import { RelationsController } from './relations.controller'; 6 | import { Relation } from './entities/relation.entity'; 7 | 8 | @Module({ 9 | imports: [ 10 | TypeOrmModule.forFeature([Relation]), 11 | forwardRef(() => UsersModule), 12 | ], 13 | controllers: [RelationsController], 14 | providers: [RelationsService], 15 | exports: [RelationsService], 16 | }) 17 | export class RelationsModule {} 18 | -------------------------------------------------------------------------------- /src/save-lists/dto/create-save-list.dto.ts: -------------------------------------------------------------------------------- 1 | export class CreateSaveListDto {} 2 | -------------------------------------------------------------------------------- /src/save-lists/dto/update-save-list.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/swagger'; 2 | import { CreateSaveListDto } from './create-save-list.dto'; 3 | 4 | export class UpdateSaveListDto extends PartialType(CreateSaveListDto) {} 5 | -------------------------------------------------------------------------------- /src/save-lists/entities/save-list.entity.ts: -------------------------------------------------------------------------------- 1 | import { SavePost } from 'src/save-posts/entities/save-post.entity'; 2 | import { User } from 'src/users/entities/user.entity'; 3 | import { 4 | Column, 5 | Entity, 6 | Index, 7 | JoinColumn, 8 | ManyToOne, 9 | OneToMany, 10 | PrimaryGeneratedColumn, 11 | } from 'typeorm'; 12 | 13 | @Entity() 14 | export class SaveList { 15 | @PrimaryGeneratedColumn('uuid') 16 | id: string; 17 | 18 | @Index() 19 | @Column() 20 | user_id: string; 21 | 22 | @Index() 23 | @Column() 24 | name: string; 25 | 26 | @ManyToOne(() => User, (user) => user.save_lists) 27 | @JoinColumn({ name: 'user_id' }) 28 | user: User; 29 | 30 | @OneToMany(() => SavePost, (savePost) => savePost.save_list) 31 | save_posts: SavePost[]; 32 | } 33 | -------------------------------------------------------------------------------- /src/save-lists/save-lists.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common'; 2 | import { SaveListsService } from './save-lists.service'; 3 | 4 | @Controller('save-lists') 5 | export class SaveListsController { 6 | constructor(private readonly saveListsService: SaveListsService) {} 7 | } 8 | -------------------------------------------------------------------------------- /src/save-lists/save-lists.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { SaveListsService } from './save-lists.service'; 3 | import { SaveListsController } from './save-lists.controller'; 4 | 5 | @Module({ 6 | controllers: [SaveListsController], 7 | providers: [SaveListsService], 8 | }) 9 | export class SaveListsModule {} 10 | -------------------------------------------------------------------------------- /src/save-lists/save-lists.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class SaveListsService {} 5 | -------------------------------------------------------------------------------- /src/save-posts/dto/create-save-post.dto.ts: -------------------------------------------------------------------------------- 1 | export class CreateSavePostDto {} 2 | -------------------------------------------------------------------------------- /src/save-posts/dto/update-save-post.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/swagger'; 2 | import { CreateSavePostDto } from './create-save-post.dto'; 3 | 4 | export class UpdateSavePostDto extends PartialType(CreateSavePostDto) {} 5 | -------------------------------------------------------------------------------- /src/save-posts/entities/save-post.entity.ts: -------------------------------------------------------------------------------- 1 | import { Post } from 'src/posts/entities/post.entity'; 2 | import { SaveList } from 'src/save-lists/entities/save-list.entity'; 3 | import { 4 | Column, 5 | Entity, 6 | Index, 7 | JoinColumn, 8 | ManyToOne, 9 | OneToOne, 10 | PrimaryGeneratedColumn, 11 | } from 'typeorm'; 12 | 13 | @Entity() 14 | export class SavePost { 15 | @PrimaryGeneratedColumn('uuid') 16 | id: string; 17 | 18 | @Index() 19 | @Column() 20 | save_list_id: string; 21 | 22 | @Index() 23 | @Column() 24 | post_id: string; 25 | 26 | @ManyToOne(() => SaveList, (saveList) => saveList.save_posts) 27 | @JoinColumn({ name: 'save_list_id' }) 28 | save_list: SaveList; 29 | 30 | @OneToOne(() => Post, (post) => post.save_posts) 31 | @JoinColumn({ name: 'post_id' }) 32 | post: Post; 33 | } 34 | -------------------------------------------------------------------------------- /src/save-posts/save-posts.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common'; 2 | import { SavePostsService } from './save-posts.service'; 3 | 4 | @Controller('save-posts') 5 | export class SavePostsController { 6 | constructor(private readonly savePostsService: SavePostsService) {} 7 | } 8 | -------------------------------------------------------------------------------- /src/save-posts/save-posts.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { SavePostsService } from './save-posts.service'; 3 | import { SavePostsController } from './save-posts.controller'; 4 | 5 | @Module({ 6 | controllers: [SavePostsController], 7 | providers: [SavePostsService], 8 | }) 9 | export class SavePostsModule {} 10 | -------------------------------------------------------------------------------- /src/save-posts/save-posts.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class SavePostsService {} 5 | -------------------------------------------------------------------------------- /src/search-engine/dto/user-search-body.interface.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { 3 | IsUUID, 4 | IsString, 5 | IsEmail, 6 | IsOptional, 7 | IsEnum, 8 | IsArray, 9 | IsDate, 10 | } from 'class-validator'; 11 | import { GenderType } from 'src/helper/gender.enum'; 12 | import { PrivacyType } from 'src/helper/privacy.enum'; 13 | import { RoleType } from 'src/helper/role.enum'; 14 | import { UserCategoryType } from 'src/helper/user-category.enum'; 15 | 16 | export class UserSearchBody { 17 | @ApiProperty() 18 | @IsUUID() 19 | id: string; 20 | 21 | @ApiProperty() 22 | @IsString() 23 | username: string; 24 | 25 | @ApiProperty() 26 | @IsEmail() 27 | email: string; 28 | 29 | @ApiProperty() 30 | @IsOptional() 31 | @IsString() 32 | bio?: string; 33 | 34 | @ApiProperty() 35 | @IsOptional() 36 | @IsString() 37 | website?: string; 38 | 39 | @ApiProperty() 40 | @IsString() 41 | avatar: string; 42 | 43 | @ApiProperty() 44 | @IsOptional() 45 | @IsString() 46 | address?: string; 47 | 48 | @ApiProperty() 49 | @IsOptional() 50 | @IsEnum(GenderType) 51 | gender?: GenderType; 52 | 53 | @ApiProperty() 54 | @IsOptional() 55 | birthday?: string; 56 | 57 | @ApiProperty() 58 | @IsOptional() 59 | @IsArray() 60 | @IsString({ each: true }) 61 | company?: string[]; 62 | 63 | @ApiProperty() 64 | @IsOptional() 65 | @IsArray() 66 | @IsString({ each: true }) 67 | education?: string[]; 68 | 69 | @ApiProperty() 70 | @IsOptional() 71 | @IsDate() 72 | last_active?: Date; 73 | 74 | @ApiProperty() 75 | @IsOptional() 76 | @IsEnum(UserCategoryType) 77 | user_category?: UserCategoryType; 78 | 79 | @ApiProperty() 80 | @IsEnum(PrivacyType) 81 | privacy: PrivacyType; 82 | 83 | @ApiProperty() 84 | @IsDate() 85 | created_at: Date; 86 | 87 | @ApiProperty() 88 | @IsDate() 89 | updated_at: Date; 90 | 91 | @ApiProperty() 92 | @IsEnum(RoleType) 93 | role: RoleType; 94 | } 95 | -------------------------------------------------------------------------------- /src/search-engine/dto/user-search.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString, IsNotEmpty, Length } from 'class-validator'; 2 | 3 | export class UserSearchDto { 4 | @IsString() 5 | @IsNotEmpty() 6 | @Length(3, 50) 7 | query: string; 8 | 9 | page?: number = 1; 10 | limit?: number = 10; 11 | } 12 | -------------------------------------------------------------------------------- /src/search-engine/post-search.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ElasticsearchService } from '@nestjs/elasticsearch'; 3 | 4 | @Injectable() 5 | export class PostSearchService { 6 | constructor(private readonly elasticsearchService: ElasticsearchService) {} 7 | } 8 | -------------------------------------------------------------------------------- /src/search-engine/search-engine.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Query } from '@nestjs/common'; 2 | import { ApiOperation, ApiTags } from '@nestjs/swagger'; 3 | import { UserSearchService } from './user-search.service'; 4 | import { UserSearchDto } from './dto/user-search.dto'; 5 | import { ResponseMessage } from 'src/decorator/customize'; 6 | 7 | @ApiTags('Search Engine') 8 | @Controller('search-engine') 9 | export class SearchEngineController { 10 | constructor(private readonly userSearchService: UserSearchService) {} 11 | 12 | @Get('search-user') 13 | @ResponseMessage('Search user full text success') 14 | @ApiOperation({ summary: 'Search user full text' }) 15 | async searchUsers(@Query() searchDto: UserSearchDto) { 16 | const result = await this.userSearchService.searchUsers(searchDto); 17 | return { 18 | success: true, 19 | data: result.users, 20 | meta: { 21 | total: result.total, 22 | page: searchDto.page, 23 | limit: searchDto.limit, 24 | }, 25 | }; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/search-engine/search-engine.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { SearchEngineController } from './search-engine.controller'; 3 | import { ElasticsearchModule } from '@nestjs/elasticsearch'; 4 | import { ConfigModule, ConfigService } from '@nestjs/config'; 5 | import { UserSearchService } from './user-search.service'; 6 | import { PostSearchService } from './post-search.service'; 7 | import { TypeOrmModule } from '@nestjs/typeorm'; 8 | import { User } from 'src/users/entities/user.entity'; 9 | import { Relation } from 'src/relations/entities/relation.entity'; 10 | 11 | @Module({ 12 | imports: [ 13 | TypeOrmModule.forFeature([User]), 14 | TypeOrmModule.forFeature([Relation]), 15 | ElasticsearchModule.registerAsync({ 16 | imports: [ConfigModule], 17 | useFactory: async (configService: ConfigService) => ({ 18 | node: configService.get('ELASTICSEARCH_HOSTS'), 19 | auth: { 20 | username: configService.get('ELASTICSEARCH_USERNAME'), 21 | password: configService.get('ELASTICSEARCH_PASSWORD'), 22 | }, 23 | maxRetries: 10, 24 | requestTimeout: 60000, 25 | }), 26 | inject: [ConfigService], 27 | }), 28 | ], 29 | controllers: [SearchEngineController], 30 | providers: [UserSearchService, PostSearchService], 31 | exports: [UserSearchService, PostSearchService], 32 | }) 33 | export class SearchEngineModule {} 34 | -------------------------------------------------------------------------------- /src/search-engine/user-search.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ElasticsearchService } from '@nestjs/elasticsearch'; 3 | import { UserSearchDto } from './dto/user-search.dto'; 4 | import { UserSearchBody } from './dto/user-search-body.interface'; 5 | 6 | @Injectable() 7 | export class UserSearchService { 8 | constructor(private readonly elasticsearchService: ElasticsearchService) {} 9 | 10 | async searchUsers(searchDto: UserSearchDto): Promise<{ 11 | users: UserSearchDto[]; 12 | total: number; 13 | }> { 14 | const { query, page, limit } = searchDto; 15 | const from = (page - 1) * limit; 16 | 17 | try { 18 | const result = await this.elasticsearchService.search({ 19 | index: 'snet-users', // name index in elasticsearch 20 | body: { 21 | from, 22 | size: limit, 23 | query: { 24 | multi_match: { 25 | query, 26 | fields: ['id', 'username', 'email', 'bio'], 27 | fuzziness: 'AUTO', // Allows approximate search 28 | operator: 'and', 29 | }, 30 | }, 31 | }, 32 | }); 33 | 34 | const hits = result.hits.hits; 35 | const users = hits.map((hit: any) => ({ 36 | id: hit._id, 37 | ...hit._source, 38 | })); 39 | 40 | return { 41 | users, 42 | total: 43 | typeof result.hits.total === 'number' 44 | ? result.hits.total 45 | : result.hits.total.value, 46 | }; 47 | } catch (error) { 48 | throw new Error(`Search failed: ${error.message}`); 49 | } 50 | } 51 | 52 | // Create/Update index user 53 | async indexUser(user: UserSearchBody): Promise { 54 | await this.elasticsearchService.index({ 55 | index: 'users', 56 | id: user.id, 57 | body: user, 58 | }); 59 | } 60 | 61 | // Delete user 62 | async deleteUser(userId: string): Promise { 63 | try { 64 | await this.elasticsearchService.delete({ 65 | index: 'users', 66 | id: userId, 67 | }); 68 | } catch (error) { 69 | if (error.meta?.body?.result === 'not_found') { 70 | throw new Error(`User with id ${userId} not found`); 71 | } 72 | throw new Error(`Failed to delete user: ${error.message}`); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/templates/email/otp-delete-account.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Xác nhận xóa tài khoản 6 | 78 | 79 | 80 | 81 |
82 |

🎉 Xin chào, {{username}}! 🎉

83 | 84 |

85 | Bạn đã yêu cầu xóa tài khoản của mình trên SNet. Để tiếp tục, vui lòng 86 | xác thực yêu cầu của bạn bằng cách nhập mã OTP dưới đây. 87 |

88 | 89 |

90 | Mã OTP của bạn để xác nhận yêu cầu xóa tài khoản là: 91 |

92 | 93 |
{{otp}}
94 | 95 |

96 | Lưu ý quan trọng: 97 | 🔹 Mã OTP này có hiệu lực **trong 2 phút** và chỉ được sử dụng một lần. 98 | 🔹 Nếu bạn không yêu cầu xóa tài khoản, vui lòng bỏ qua thông báo này. 99 |

100 | 101 |

💬 Cần hỗ trợ?

102 |

103 | Nếu bạn có bất kỳ thắc mắc nào về việc xóa tài khoản hoặc cần hỗ trợ, 104 | hãy truy cập **Chatbot AI hỗ trợ** của chúng tôi để được giải đáp nhanh 105 | chóng. 106 |

107 |

👉 Truy cập Chatbot AI ngay

108 | 109 | 113 |
114 | 115 | -------------------------------------------------------------------------------- /src/templates/email/otp-forgot-password-account.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Đăng Nhập SNet 6 | 78 | 79 | 80 | 81 |
82 |

🎉 Xin chào, {{username}}! 🎉

83 | 84 |

85 | Chào mừng bạn đến với 86 | SNet 87 | - nơi kết nối bạn bè, chia sẻ khoảnh khắc và khám phá vô số điều thú vị. 88 | Chúng tôi rất vui khi bạn chọn SNet để đồng hành trong hành trình trực 89 | tuyến của mình. 90 |

91 | 92 |

93 | Để bảo vệ tài khoản của bạn và đảm bảo tính an toàn khi đăng nhập, vui 94 | lòng xác thực tài khoản bằng mã OTP dưới đây. Mã OTP sẽ giúp bảo vệ tài 95 | khoản của bạn khỏi truy cập trái phép. 96 |

97 | 98 |
{{otp}}
99 | 100 |

101 | Lưu ý quan trọng: 102 | 🔹 Mã OTP này có hiệu lực trong 2 phút và chỉ sử dụng được một lần. 🔹 103 | Không chia sẻ mã này với ai để bảo vệ tài khoản của bạn. 🔹 Nếu bạn 104 | không yêu cầu mã OTP này, có thể ai đó đang cố gắng đăng nhập vào tài 105 | khoản của bạn. Hãy đổi mật khẩu ngay lập tức để bảo vệ tài khoản. 106 |

107 | 108 |

🚀 Các tính năng thú vị trên SNet:

109 |

110 | ✨ 111 | Kết nối bạn bè: Dễ dàng kết nối với những người có cùng 112 | sở thích. 113 |
📸 114 | Chia sẻ khoảnh khắc: Đăng ảnh, video và bài viết để mọi 115 | người cùng thưởng thức. 116 |
📰 117 | Cập nhật xu hướng: Theo dõi tin tức và khám phá các chủ 118 | đề bạn quan tâm. 119 |
🔐 120 | Bảo mật nâng cao: Sử dụng xác thực hai lớp và mã hóa dữ 121 | liệu giúp tài khoản của bạn luôn an toàn. 122 |

123 | 124 |

💬 Cần hỗ trợ?

125 |

126 | Nếu bạn gặp bất kỳ vấn đề nào trong quá trình đăng nhập hoặc có thắc mắc 127 | khác, đừng ngần ngại liên hệ với chúng tôi qua Chatbot AI. 128 |

129 |

👉 Truy cập Chatbot AI ngay

130 | 131 | 135 |
136 | 137 | -------------------------------------------------------------------------------- /src/templates/email/otp-login-account.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Đăng Nhập SNet 7 | 79 | 80 | 81 | 82 |
83 |

🎉 Xin chào, {{username}}! 🎉

84 | 85 |

86 | Chào mừng bạn đến với 87 | SNet 88 | - nơi kết nối bạn bè, chia sẻ khoảnh khắc và khám phá vô số điều thú vị. 89 | Chúng tôi rất vui khi bạn chọn SNet để đồng hành trong hành trình trực 90 | tuyến của mình. 91 |

92 | 93 |

94 | Để bảo vệ tài khoản của bạn và đảm bảo tính an toàn khi đăng nhập, vui 95 | lòng xác thực tài khoản bằng mã OTP dưới đây. Mã OTP sẽ giúp bảo vệ tài 96 | khoản của bạn khỏi truy cập trái phép. 97 |

98 | 99 |
{{otp}}
100 | 101 |

102 | Lưu ý quan trọng: 103 | 🔹 Mã OTP này có hiệu lực **trong 2 phút** và chỉ sử dụng được một lần. 104 | 🔹 Không chia sẻ mã này với ai để bảo vệ tài khoản của bạn. 🔹 Nếu bạn 105 | không yêu cầu mã OTP này, có thể ai đó đang cố gắng đăng nhập vào tài 106 | khoản của bạn. Hãy **đổi mật khẩu ngay lập tức** để bảo vệ tài khoản. 107 |

108 | 109 |

🚀 Các tính năng thú vị trên SNet:

110 |

111 | ✨ 112 | Kết nối bạn bè: Dễ dàng kết nối với những người có cùng 113 | sở thích. 114 |
📸 115 | Chia sẻ khoảnh khắc: Đăng ảnh, video và bài viết để mọi 116 | người cùng thưởng thức. 117 |
📰 118 | Cập nhật xu hướng: Theo dõi tin tức và khám phá các chủ 119 | đề bạn quan tâm. 120 |
🔐 121 | Bảo mật nâng cao: Sử dụng xác thực hai lớp và mã hóa dữ 122 | liệu giúp tài khoản của bạn luôn an toàn. 123 |

124 | 125 |

💬 Cần hỗ trợ?

126 |

127 | Nếu bạn gặp bất kỳ vấn đề nào trong quá trình đăng nhập hoặc có thắc mắc 128 | khác, đừng ngần ngại liên hệ với chúng tôi qua **Chatbot AI**. 129 |

130 |

👉 Truy cập Chatbot AI ngay

131 | 132 | 136 |
137 | 138 | 139 | -------------------------------------------------------------------------------- /src/templates/email/signup-success.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Chào mừng bạn đến với SNet! 7 | 73 | 74 | 75 | 76 |
77 |

🎉 Chào mừng bạn đến với SNet! 🎉

78 |

Xin chào, {{username}}!

79 | 80 |

Chúng tôi rất vui mừng khi bạn đã gia nhập cộng đồng SNet - nền tảng 81 | mạng xã hội dành cho những người đam mê kết nối, chia sẻ và khám phá 82 | những điều thú vị.

83 | 84 |

SNet không chỉ là nơi để trò chuyện với bạn bè, mà còn giúp bạn tìm 85 | kiếm những người cùng sở thích, tham gia vào các nhóm thảo luận, cập 86 | nhật tin tức và thể hiện bản thân theo cách riêng của mình.

87 | 88 |

🚀 SNet có gì thú vị?

89 |

90 | ✨ 91 | Kết nối bạn bè: Dễ dàng tìm kiếm và kết bạn với những 92 | người có cùng sở thích. 93 |
📸 94 | Chia sẻ khoảnh khắc: Đăng ảnh, video và bài viết để mọi 95 | người cùng thưởng thức. 96 |
📰 97 | Cập nhật xu hướng: Theo dõi tin tức mới nhất và khám 98 | phá những chủ đề bạn quan tâm. 99 |
🔐 100 | Bảo mật nâng cao: Xác thực hai lớp và hệ thống mã hóa 101 | dữ liệu giúp tài khoản luôn an toàn. 102 |

103 | 104 |

💬 Cần hỗ trợ?

105 |

106 | Nếu bạn có bất kỳ thắc mắc nào về việc đăng ký tài khoản hoặc cần hỗ 107 | trợ, hãy truy cập **Chatbot AI hỗ trợ** của chúng tôi để được giải đáp 108 | nhanh chóng. 109 |

110 |

👉 Truy cập Chatbot AI ngay

111 | 112 | 116 |
117 | 118 | 119 | -------------------------------------------------------------------------------- /src/users/birthday.job.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { User } from 'src/users/entities/user.entity'; 4 | import { Repository } from 'typeorm'; 5 | import { Cron } from '@nestjs/schedule'; 6 | import { InjectQueue } from '@nestjs/bullmq'; 7 | import { Queue } from 'bullmq'; 8 | 9 | @Injectable() 10 | export class BirthdayJob { 11 | constructor( 12 | @InjectRepository(User) 13 | private readonly userRepository: Repository, 14 | @InjectQueue('noti-birthday') private notiQueue: Queue, 15 | ) {} 16 | 17 | @Cron('0 0 * * *') // Running every day at 00h00 18 | // @Cron('* * * * *') // Running every minute 19 | async checkBirthdays() { 20 | console.log('Checking birthdays...'); 21 | const today = new Date(); 22 | const month = today.getMonth() + 1; 23 | const day = today.getDate(); 24 | 25 | const birthdayUsers = await this.userRepository 26 | .createQueryBuilder('user') 27 | .where( 28 | 'EXTRACT(DAY FROM user.birthday) = :day AND EXTRACT(MONTH FROM user.birthday) = :month', 29 | { day, month }, 30 | ) 31 | .getMany(); 32 | 33 | if (birthdayUsers.length > 0) { 34 | birthdayUsers.forEach((user) => { 35 | this.notiQueue.add( 36 | 'birthday', 37 | { 38 | id: user.id, 39 | email: user.email, 40 | avatar: user.avatar, 41 | username: user.username, 42 | birthday: user.birthday, 43 | }, 44 | { removeOnComplete: true }, 45 | ); 46 | }); 47 | } 48 | return; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/users/dto/after-delete.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator'; 3 | 4 | export class AfterDeleteDto { 5 | @IsString() 6 | @IsNotEmpty() 7 | @MaxLength(6) 8 | @MinLength(6) 9 | @ApiProperty({ example: '123123', description: 'otp' }) 10 | otp: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/users/dto/after-forgot-password.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsString, Length } from 'class-validator'; 3 | 4 | export class AfterForgotPasswordDto { 5 | @IsString() 6 | @Length(6, 6) 7 | @ApiProperty({ example: '123123', description: 'otp' }) 8 | otp: string; 9 | 10 | @IsString() 11 | @ApiProperty({ example: 'user@gmail.com', description: 'email' }) 12 | email: string; 13 | 14 | @IsString() 15 | @ApiProperty({ example: 'Nguyễn Tuấn Thành', description: 'username' }) 16 | username: string; 17 | 18 | @IsString() 19 | @ApiProperty({ example: '12345678', description: 'password' }) 20 | @Length(8, 15) 21 | password: string; 22 | } 23 | -------------------------------------------------------------------------------- /src/users/dto/after-login.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { 3 | IsEmail, 4 | IsNotEmpty, 5 | IsString, 6 | MaxLength, 7 | MinLength, 8 | } from 'class-validator'; 9 | 10 | export class AfterLoginDto { 11 | @IsString() 12 | @IsNotEmpty() 13 | @MaxLength(6) 14 | @MinLength(6) 15 | @ApiProperty({ example: '123123', description: 'otp' }) 16 | otp: string; 17 | @IsNotEmpty({ message: 'Email not empty' }) 18 | @IsEmail({}, { message: 'Email is not valid' }) 19 | @IsString({ message: 'Type email is string' }) 20 | @ApiProperty({ example: 'user@gmail.com', description: 'email' }) 21 | email: string; 22 | 23 | @IsNotEmpty({ message: 'Password not empty' }) 24 | @MinLength(8, { message: 'Password not less than 8 characters' }) 25 | @MaxLength(10, { message: 'Password not more than 10 characters' }) 26 | @IsString({ message: 'Type password is string' }) 27 | @ApiProperty({ example: '12345678', description: 'password' }) 28 | password: string; 29 | } 30 | -------------------------------------------------------------------------------- /src/users/dto/after-signup.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { 3 | IsEmail, 4 | IsNotEmpty, 5 | IsOptional, 6 | IsString, 7 | MaxLength, 8 | MinLength, 9 | } from 'class-validator'; 10 | import { GenderType } from 'src/helper/gender.enum'; 11 | 12 | export class AfterSignUpDto { 13 | @IsString() 14 | @IsNotEmpty() 15 | @MaxLength(6) 16 | @MinLength(6) 17 | @ApiProperty({ example: '123123', description: 'otp' }) 18 | otp?: string; 19 | 20 | @IsEmail() 21 | @IsString() 22 | @IsNotEmpty() 23 | @ApiProperty({ example: 'user@gmail.com', description: 'email' }) 24 | email: string; 25 | 26 | @MinLength(8) 27 | @MaxLength(15) 28 | @IsString() 29 | @IsNotEmpty() 30 | @ApiProperty({ example: '12345678', description: 'password' }) 31 | password: string; 32 | 33 | @ApiProperty({ 34 | example: 'abc.jpg', 35 | description: 'Your avatar', 36 | }) 37 | @IsOptional() 38 | avatar: string; 39 | 40 | @MinLength(2) 41 | @MaxLength(20) 42 | @IsString() 43 | @IsNotEmpty() 44 | @ApiProperty({ 45 | example: 'Nguyễn Tuấn Thành', 46 | description: 'Your last username', 47 | }) 48 | username: string; 49 | 50 | @ApiProperty({ example: 'Good boy', description: 'bio' }) 51 | @IsString() 52 | @IsOptional() 53 | bio: string; 54 | 55 | @ApiProperty({ 56 | example: 'https://github.com/ntthanh2603', 57 | description: 'website', 58 | }) 59 | @IsString() 60 | @IsOptional() 61 | website: string; 62 | 63 | @ApiProperty({ 64 | example: '2025-02-11 08:14:57.142000', 65 | description: 'birthday', 66 | }) 67 | @IsOptional() 68 | birthday: Date; 69 | 70 | @ApiProperty({ example: GenderType.MALE, description: 'gender' }) 71 | @IsOptional() 72 | gender: GenderType; 73 | 74 | @ApiProperty({ example: 'Cau Giay, Ha Noi', description: 'address' }) 75 | @IsString() 76 | @IsOptional() 77 | address: string; 78 | } 79 | -------------------------------------------------------------------------------- /src/users/dto/before-login.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { 3 | IsEmail, 4 | IsNotEmpty, 5 | IsString, 6 | MaxLength, 7 | MinLength, 8 | } from 'class-validator'; 9 | 10 | export class BeforeLoginDto { 11 | @IsNotEmpty({ message: 'Email not empty' }) 12 | @IsEmail({}, { message: 'Email not valid' }) 13 | @IsString({ message: 'Type email is string' }) 14 | @ApiProperty({ example: 'user@gmail.com', description: 'email' }) 15 | email: string; 16 | 17 | @IsNotEmpty({ message: 'Password not empty' }) 18 | @MinLength(8, { message: 'Password not less than 8 characters' }) 19 | @MaxLength(10, { message: 'Password not more than 10 characters' }) 20 | @IsString({ message: 'Type password is string' }) 21 | @ApiProperty({ example: '12345678', description: 'password' }) 22 | password: string; 23 | } 24 | -------------------------------------------------------------------------------- /src/users/dto/create-account-with-google.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { 3 | IsEmail, 4 | IsNotEmpty, 5 | IsOptional, 6 | IsString, 7 | MaxLength, 8 | MinLength, 9 | } from 'class-validator'; 10 | 11 | export class CreateAccountWithGoogleDto { 12 | @IsEmail() 13 | @IsString() 14 | @IsNotEmpty() 15 | @ApiProperty({ example: 'user@gmail.com', description: 'email' }) 16 | email: string; 17 | 18 | @IsString() 19 | @IsNotEmpty() 20 | @ApiProperty({ example: '12345678', description: 'password' }) 21 | password: string; 22 | 23 | @ApiProperty({ 24 | example: 'abc.jpg', 25 | description: 'Your avatar', 26 | }) 27 | @IsOptional() 28 | avatar: string; 29 | 30 | @MinLength(2) 31 | @MaxLength(20) 32 | @IsString() 33 | @IsNotEmpty() 34 | @ApiProperty({ 35 | example: 'Nguyễn Tuấn Thành', 36 | description: 'Your last username', 37 | }) 38 | username: string; 39 | } 40 | -------------------------------------------------------------------------------- /src/users/dto/refresh-token.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty } from 'class-validator'; 2 | 3 | export class RefreshTokenDto { 4 | @IsNotEmpty({ message: 'Email not empty' }) 5 | refresh_token: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/users/dto/send-otp.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { 3 | IsEmail, 4 | IsNotEmpty, 5 | IsString, 6 | MaxLength, 7 | MinLength, 8 | } from 'class-validator'; 9 | 10 | export class SendOtpDto { 11 | @IsEmail() 12 | @IsString() 13 | @IsNotEmpty() 14 | @ApiProperty({ example: 'user@gmail.com', description: 'email' }) 15 | email: string; 16 | 17 | @MinLength(2) 18 | @MaxLength(20) 19 | @IsString() 20 | @IsNotEmpty() 21 | @ApiProperty({ 22 | example: 'Nguyễn Tuấn Thành', 23 | description: 'Your last username', 24 | }) 25 | username: string; 26 | } 27 | -------------------------------------------------------------------------------- /src/users/dto/update-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsOptional, IsString, MaxLength, MinLength } from 'class-validator'; 3 | import { GenderType } from 'src/helper/gender.enum'; 4 | import { PrivacyType } from 'src/helper/privacy.enum'; 5 | 6 | export class UpdateUserDto { 7 | @MinLength(2) 8 | @MaxLength(20) 9 | @IsString() 10 | @IsOptional() 11 | @ApiProperty({ example: 'Thành', description: 'Your last username' }) 12 | username: string; 13 | 14 | @ApiProperty({ example: 'Good boy', description: 'bio' }) 15 | @MinLength(5, { message: 'Bio not less than 5 characters' }) 16 | @MaxLength(100, { message: 'Bio not more than 100 characters' }) 17 | @IsOptional() 18 | bio: string; 19 | 20 | @ApiProperty({ 21 | example: 'https://github.com/ntthanh2603', 22 | description: 'website', 23 | }) 24 | @MinLength(5) 25 | @MaxLength(100) 26 | @IsOptional() 27 | website: string; 28 | 29 | @ApiProperty({ 30 | example: '2025-02-11 08:14:57.142000', 31 | description: 'Birthday', 32 | }) 33 | @IsOptional() 34 | birthday: Date; 35 | 36 | @ApiProperty({ example: GenderType.MALE, description: 'gender' }) 37 | @IsOptional() 38 | gender: GenderType; 39 | 40 | @ApiProperty({ example: 'Cau Giay, Ha Noi', description: 'address' }) 41 | @MinLength(5, { message: 'Address min lenth is 5' }) 42 | @MaxLength(100, { message: 'Address max lenth is 100' }) 43 | @IsOptional() 44 | address: string; 45 | 46 | @ApiProperty({ example: PrivacyType.PUBLIC, description: 'privacy' }) 47 | @IsOptional() 48 | privacy: PrivacyType; 49 | } 50 | -------------------------------------------------------------------------------- /src/users/entities/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { MaxLength, MinLength } from 'class-validator'; 2 | import { ChatMember } from 'src/chat-members/entities/chat-member.entity'; 3 | import { WaitingMembers } from 'src/chat-members/entities/waiting-members.entity'; 4 | import { ChatMessage } from 'src/chat-messages/entities/chat-message.entity'; 5 | import { ChatRoom } from 'src/chat-rooms/entities/chat-room.entity'; 6 | import { Comment } from 'src/comments/entities/comment.entity'; 7 | import { DeviceSession } from 'src/device-sessions/entities/device-session.entity'; 8 | import { GenderType } from 'src/helper/gender.enum'; 9 | import { PrivacyType } from 'src/helper/privacy.enum'; 10 | import { RoleType } from 'src/helper/role.enum'; 11 | import { UserCategoryType } from 'src/helper/user-category.enum'; 12 | import { NotificationUser } from 'src/notification-users/entities/notification-user.entity'; 13 | import { PinChat } from 'src/pin-chats/entities/pin-chat.entity'; 14 | import { Post } from 'src/posts/entities/post.entity'; 15 | import { Relation } from 'src/relations/entities/relation.entity'; 16 | import { SaveList } from 'src/save-lists/entities/save-list.entity'; 17 | 18 | import { 19 | Column, 20 | CreateDateColumn, 21 | Entity, 22 | Index, 23 | OneToMany, 24 | PrimaryGeneratedColumn, 25 | UpdateDateColumn, 26 | } from 'typeorm'; 27 | 28 | @Entity() 29 | export class User { 30 | @PrimaryGeneratedColumn('uuid') 31 | id: string; 32 | 33 | @Index() 34 | @Column({ unique: true }) 35 | email: string; 36 | 37 | @Index() 38 | @Column() 39 | @MinLength(8) 40 | @MaxLength(15) 41 | password: string; 42 | 43 | @Index() 44 | @Column({ nullable: true }) 45 | avatar: string; 46 | 47 | @Index() 48 | @Column() 49 | username: string; 50 | 51 | @Column({ nullable: true }) 52 | bio: string; 53 | 54 | @Column({ nullable: true }) 55 | website: string; 56 | 57 | @Column({ nullable: true }) 58 | birthday: Date; 59 | 60 | @Column({ type: 'enum', enum: GenderType, nullable: true }) 61 | gender: GenderType; 62 | 63 | @Column({ nullable: true }) 64 | address: string; 65 | 66 | @Column({ type: 'enum', enum: PrivacyType, default: PrivacyType.PUBLIC }) 67 | privacy: PrivacyType; 68 | 69 | @Column({ nullable: true }) 70 | last_active: Date; 71 | 72 | @Column({ 73 | type: 'enum', 74 | enum: UserCategoryType, 75 | default: UserCategoryType.CASUALUSER, 76 | }) 77 | user_category: UserCategoryType; 78 | 79 | @Column({ type: 'text', array: true, default: () => "'{}'" }) 80 | company: string[]; 81 | 82 | @Column({ type: 'text', array: true, default: () => "'{}'" }) 83 | education: string[]; 84 | 85 | @Column({ type: 'enum', enum: RoleType, default: RoleType.USER }) 86 | role: RoleType; 87 | 88 | @CreateDateColumn() 89 | created_at: Date; 90 | 91 | @UpdateDateColumn({ nullable: true }) 92 | updated_at: Date; 93 | 94 | @OneToMany(() => DeviceSession, (deviceSession) => deviceSession.user) 95 | device_sessions: DeviceSession[]; 96 | 97 | @OneToMany(() => Relation, (relation) => relation.request_side) 98 | sent_relations: Relation[]; 99 | 100 | @OneToMany(() => Relation, (relation) => relation.accept_side) 101 | received_relations: Relation[]; 102 | 103 | @OneToMany( 104 | () => NotificationUser, 105 | (notification_user) => notification_user.user, 106 | ) 107 | notification_users: NotificationUser[]; 108 | 109 | @OneToMany(() => ChatRoom, (chatRoom) => chatRoom.user) 110 | chat_rooms: ChatRoom[]; 111 | 112 | @OneToMany(() => ChatMember, (chatMember) => chatMember.user) 113 | chat_members: ChatMember[]; 114 | 115 | @OneToMany(() => ChatMessage, (chatMessage) => chatMessage.user) 116 | chat_messages: ChatMessage[]; 117 | 118 | @OneToMany(() => Post, (post) => post.user) 119 | posts: Post[]; 120 | 121 | @OneToMany(() => SaveList, (saveList) => saveList.user) 122 | save_lists: SaveList[]; 123 | 124 | @OneToMany(() => Comment, (comment) => comment.user) 125 | comments: Comment[]; 126 | 127 | @OneToMany(() => PinChat, (pinChat) => pinChat.user) 128 | pin_chats: PinChat[]; 129 | 130 | @OneToMany(() => WaitingMembers, (waitingMembers) => waitingMembers.user) 131 | waiting_members: WaitingMembers[]; 132 | } 133 | -------------------------------------------------------------------------------- /src/users/users.interface.ts: -------------------------------------------------------------------------------- 1 | export interface IUser { 2 | id: string; 3 | deviceSecssionId: string; 4 | role: string; 5 | iat: number; 6 | exp: number; 7 | } 8 | -------------------------------------------------------------------------------- /src/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Module } from '@nestjs/common'; 2 | import { UsersService } from './users.service'; 3 | import { UsersController } from './users.controller'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { User } from './entities/user.entity'; 6 | import { RedisModule } from 'src/redis/redis.module'; 7 | import { DeviceSessionsModule } from 'src/device-sessions/device-sessions.module'; 8 | import { ScheduleModule } from '@nestjs/schedule'; 9 | import { BirthdayJob } from './birthday.job'; 10 | import { NotificationModule } from 'src/notifications/notifications.module'; 11 | import { BullModule } from '@nestjs/bullmq'; 12 | import { MulterModule } from '@nestjs/platform-express'; 13 | import { MulterConfigService } from 'src/core/multer.config'; 14 | import { RelationsModule } from 'src/relations/relations.module'; 15 | import { NestjsFingerprintModule } from 'nestjs-fingerprint'; 16 | import { SearchEngineModule } from 'src/search-engine/search-engine.module'; 17 | import { AuthModule } from 'src/auth/auth.module'; 18 | 19 | @Module({ 20 | imports: [ 21 | NestjsFingerprintModule.forRoot({ 22 | params: ['headers', 'userAgent', 'ipAddress'], 23 | cookieOptions: { 24 | name: 'refreshToken', // optional 25 | httpOnly: true, // optional 26 | }, 27 | }), 28 | TypeOrmModule.forFeature([User]), 29 | MulterModule.registerAsync({ 30 | useClass: MulterConfigService, 31 | }), 32 | RedisModule, 33 | forwardRef(() => AuthModule), 34 | forwardRef(() => RelationsModule), 35 | forwardRef(() => DeviceSessionsModule), 36 | BullModule.registerQueue({ name: 'send-email' }), 37 | BullModule.registerQueue({ name: 'noti-birthday' }), 38 | ScheduleModule.forRoot(), 39 | NotificationModule, 40 | SearchEngineModule, 41 | ], 42 | controllers: [UsersController], 43 | providers: [UsersService, BirthdayJob], 44 | exports: [UsersService], 45 | }) 46 | export class UsersModule {} 47 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/socket.js: -------------------------------------------------------------------------------- 1 | // Code chạy test kết nối tới WebSocket server 2 | // Chạy CLI: node test/socket.js 3 | import { io } from 'socket.io-client'; 4 | 5 | // URL của WebSocket server 6 | const SOCKET_URL = 'ws://localhost:3000/chat'; 7 | 8 | // Kết nối tới WebSocket server 9 | const socket = io(SOCKET_URL, { 10 | reconnection: true, // Tự động reconnect nếu mất kết nối 11 | reconnectionAttempts: 5, // Thử reconnect tối đa 5 lần 12 | reconnectionDelay: 5000, // Đợi 5 giây trước khi thử lại 13 | transports: ['websocket'], // Chỉ dùng WebSocket, không dùng polling 14 | extraHeaders: { 15 | Authorization: 16 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImNmN2Q4NDNiLTY5NjgtNDhjZi1iMzVlLTkxN2I5YzJmMTkxOCIsImRldmljZVNlY3NzaW9uSWQiOiIyODIyNjA4YS1lZTBjLTQ3ODktYjAxOS00Y2NlNTA0MmYzZWIiLCJyb2xlIjoidXNlciIsImlhdCI6MTc0MjgwNDMwMiwiZXhwIjoxNzQzMjM2MzAyfQ.6Hx1cVuzLMCPbRJijv7CptohHkbJlwGL9nUfMgAPifE', 17 | }, 18 | }); 19 | 20 | // Sự kiện khi kết nối thành công 21 | socket.on('connect', () => { 22 | console.log(`✅ Connected to WebSocket server! Socket ID: ${socket.id}`); 23 | 24 | // Gửi tin nhắn sau khi kết nối thành công 25 | socket.emit('message', { text: 'Hello from client!' }); 26 | }); 27 | 28 | // Nhận tin nhắn từ server 29 | socket.on('message', (data) => { 30 | console.log('📩 Received message from server:', data); 31 | }); 32 | 33 | // Lắng nghe sự kiện lỗi 34 | socket.on('connect_error', (err) => { 35 | console.error('❌ Connection error:', err.message); 36 | }); 37 | 38 | // Lắng nghe sự kiện mất kết nối 39 | socket.on('disconnect', (reason) => { 40 | console.warn('⚠️ Disconnected:', reason); 41 | }); 42 | 43 | // Gửi ping mỗi 5 giây để giữ kết nối 44 | // setInterval(() => { 45 | // socket.emit('ping'); 46 | // console.log('🔄 Sent ping to server...'); 47 | // }, 5000); 48 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2021", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false, 20 | "moduleResolution": "node", 21 | "types": ["node"] 22 | } 23 | } 24 | --------------------------------------------------------------------------------