├── .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 |
5 |
6 |
7 |
8 |
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 |
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 |
--------------------------------------------------------------------------------