├── .dockerignore
├── .gitattributes
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── app
├── __init__.py
├── classes
│ ├── Auth.py
│ ├── Chatgpt.py
│ ├── Messages.py
│ ├── Permissions.py
│ ├── User.py
│ └── __init__.py
├── components
│ ├── __init__.py
│ ├── auth
│ │ ├── __init__.py
│ │ ├── check_permissions.py
│ │ ├── fastapi_auth.py
│ │ └── jwt_token_handler.py
│ ├── chat_gpt
│ │ ├── __init__.py
│ │ └── chatgpt_service.py
│ ├── hash_password.py
│ ├── initial_settings.py
│ ├── logger.py
│ └── message_dispatcher
│ │ ├── __init__.py
│ │ └── mail.py
├── db
│ ├── __init__.py
│ ├── mongoClient.py
│ └── redisClient.py
├── main.py
└── routers
│ ├── __init__.py
│ ├── auth.py
│ ├── chatgpt.py
│ ├── register.py
│ ├── senders.py
│ ├── settings
│ ├── __init__.py
│ └── messages.py
│ └── users.py
├── docker-compose.yml
├── entrypoint.sh
├── generate_env.py
├── nextjs
├── .dockerignore
├── .gitignore
├── Dockerfile
├── jsconfig.json
├── next.config.mjs
├── package-lock.json
├── package.json
└── src
│ ├── api
│ ├── auth
│ │ └── auth-context.js
│ ├── endpoints.js
│ └── headers.js
│ ├── components
│ ├── confirmation-modal.js
│ ├── error.js
│ ├── loading.js
│ ├── scroll-top-top.js
│ └── warp-effects.js
│ ├── hooks
│ ├── use-authenticated-route.js
│ └── use-popover.js
│ ├── pages
│ ├── 404.js
│ ├── _app.js
│ ├── _document.js
│ ├── dashboard
│ │ └── index.js
│ ├── extensions
│ │ └── chatgpt.js
│ ├── index.js
│ ├── login.js
│ ├── reset-password
│ │ └── [token].js
│ ├── settings.js
│ └── users.js
│ ├── sections
│ ├── auth
│ │ ├── forgot-password.js
│ │ ├── login-box.js
│ │ ├── logout.js
│ │ └── registration.js
│ ├── extensions
│ │ └── chat-gpt
│ │ │ └── chat-component.js
│ ├── footer
│ │ └── footer.js
│ ├── header
│ │ ├── buttons
│ │ │ └── settings-button.js
│ │ ├── menu-items.js
│ │ └── menu.js
│ ├── profile
│ │ └── user-profile.js
│ ├── settings
│ │ └── messages-settings.js
│ └── users
│ │ ├── user-dialog.js
│ │ └── users-table.js
│ ├── styles
│ └── globals.css
│ └── theme
│ ├── dark-theme.js
│ └── menu-icons.js
├── node_modules
└── .package-lock.json
├── requirements.txt
├── screenshots
├── ChatGPT_EXT.png
├── Forgot-Password.png
├── Login.png
├── Register.png
├── Reset-Password.png
└── youtube.png
└── tests
└── tests.py
/.dockerignore:
--------------------------------------------------------------------------------
1 | # Excluding next.js frontend when creating the backend container.
2 | nextjs/
3 |
4 | # Exclude Python bytecode files
5 | *.pyc
6 |
7 | # Exclude Python cache directories
8 | __pycache__
9 |
10 | # Exclude development and editor-specific files
11 | .vscode/
12 | .idea/
13 | *.swp
14 | *.swo
15 |
16 | # Exclude Git related files and directories
17 | .git
18 | .gitignore
19 |
20 | # Exclude test and documentation files
21 | tests/
22 | docs/
23 |
24 | # Exclude any other files or directories that are not necessary for the runtime environment
25 | # Add additional exclusions as needed based on your project structure
26 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | wheels/
24 | pip-wheel-metadata/
25 | share/python-wheels/
26 | *.egg-info/
27 | .installed.cfg
28 | *.egg
29 | MANIFEST
30 |
31 | # PyInstaller
32 | # Usually these files are written by a python script from a template
33 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
34 | *.manifest
35 | *.spec
36 |
37 | # Installer logs
38 | pip-log.txt
39 | pip-delete-this-directory.txt
40 |
41 | # Unit test / coverage reports
42 | htmlcov/
43 | .tox/
44 | .nox/
45 | .coverage
46 | .coverage.*
47 | .cache
48 | nosetests.xml
49 | coverage.xml
50 | *.cover
51 | *.py,cover
52 | .hypothesis/
53 | .pytest_cache/
54 | cover/
55 |
56 | # Translations
57 | *.mo
58 | *.pot
59 |
60 | # Django stuff:
61 | *.log
62 | local_settings.py
63 | db.sqlite3
64 | db.sqlite3-journal
65 |
66 | # Flask stuff:
67 | instance/
68 | .webassets-cache
69 |
70 | # Scrapy stuff:
71 | .scrapy
72 |
73 | # Sphinx documentation
74 | docs/_build/
75 |
76 | # PyBuilder
77 | .pybuilder/
78 | target/
79 |
80 | # Jupyter Notebook
81 | .ipynb_checkpoints
82 |
83 | # IPython
84 | profile_default/
85 | ipython_config.py
86 |
87 | # pyenv
88 | .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
98 | __pypackages__/
99 |
100 | # Celery stuff
101 | celerybeat-schedule
102 | celerybeat.pid
103 |
104 | # SageMath parsed files
105 | *.sage.py
106 |
107 | # Environments
108 | .env
109 | .venv
110 | venv/
111 | ENV/
112 | env.bak/
113 | venv.bak/
114 | chatgpt_credentials.env
115 |
116 | # Spyder project settings
117 | .spyderproject
118 | .spyproject
119 |
120 | # Rope project settings
121 | .ropeproject
122 |
123 | # mkdocs documentation
124 | /site
125 |
126 | # mypy
127 | .mypy_cache/
128 | .dmypy.json
129 | dmypy.json
130 |
131 | # Pyre type checker
132 | .pyre/
133 |
134 | # pytype static type analyzer
135 | .pytype/
136 |
137 | # Cython debug symbols
138 | cython_debug/
139 |
140 | # Log files and folders
141 | logs/
142 | *.log
143 | !/frontend/
144 |
145 |
146 | # Other
147 | .idea/
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Use an official Python runtime as a parent image
2 | FROM python:3.11-slim
3 |
4 | # Set environment variables
5 | # Don't write byte code files to disk
6 | ENV PYTHONDONTWRITEBYTECODE 1
7 |
8 | # Unbuffered output for easier container logging
9 | ENV PYTHONUNBUFFERED 1
10 |
11 | # Set the working directory in the container
12 | WORKDIR /app
13 |
14 | # Install dependencies
15 | COPY requirements.txt /app/
16 | RUN pip install --no-cache-dir --upgrade pip && \
17 | pip install --no-cache-dir -r requirements.txt
18 |
19 | # Copy the current directory contents into the container at /app
20 | COPY .. /app
21 |
22 |
23 | ## Copy the entrypoint script into the container
24 | #COPY entrypoint.sh /usr/local/bin/
25 | #
26 | ## Make sure the script is executable
27 | #RUN chmod +x /usr/local/bin/entrypoint.sh
28 | #
29 | #ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
30 |
31 | ## Command to run the application
32 | #CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
33 |
34 |
35 | ## Run the container with multiple workers instead if needed.
36 | CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 George Khananaev
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PyNextStack: A Full-Stack User Management System with FastAPI, Next.js, and MUI
2 |
3 | ### DEMO: [https://demo-pynextstack.reactatomics.com](https://demo-pynextstack.reactatomics.com)
4 |
5 | ```
6 | Username: root
7 | Password: bringthemhome
8 | ```
9 |
10 | ⚡️ **Looking for a fully serverless solution built with Next.js (no Python needed)?**
11 | Check out my **Modern Auth** template — simple, secure, and production-ready.
12 | 🔗 [github.com/georgekhananaev/modern-auth](https://github.com/georgekhananaev/modern-auth)
13 |
14 | For the DEMO, only one root user can be logged in at a time. Therefore, it is recommended to register your own user first and then promptly log in as the root user to change your role. Otherwise, you might cause each other to be disconnected while trying the demo.
15 |
16 | Please note that in the demo version, ChatGPT will not function as it is not connected to the API. Additionally, you have the option to modify the SMTP settings to suit your needs for testing the forgot-password functionality. It has been successfully tested with GMAIL. Later on, I will disable the edit functionality and add my own SMTP settings for your testing convenience.
17 |
18 | ## Overview
19 |
20 | PyNextStack is a full-stack system utilizing FastAPI with asynchronous capabilities on the backend and Next.js for the frontend, showcasing the robustness of Python in server-side development. This architecture provides a scalable, efficient solution that leverages FastAPI's high performance and ease of use for creating APIs, alongside Next.js for a reactive and server-side rendered user interface. The asynchronous nature of the backend ensures non-blocking operation, enhancing the system's ability to handle high volumes of requests simultaneously, which is ideal for real-time applications. This combination offers a modern, full-stack framework that is both powerful and developer-friendly, demonstrating the versatility of Python in web development.
21 |
22 | ## Key Features
23 |
24 | ### Security
25 | - **JWT Authentication**: Secure authentication mechanism using JWT to ensure that user actions are verified and secure.
26 | - **Protected API Documentation**: Access to API documentation is restricted, requiring authentication to prevent unauthorized use.
27 | - **Rate Limiting**: Defense against brute force attacks by limiting the number of login attempts.
28 | - **Data Encryption**: Encryption techniques are employed to securely store user data.
29 |
30 | ### User Management
31 | - **Registration and Login**: Efficient and secure processes for user registration and login.
32 | - **Profile Management**: Enables users to update their profiles and manage account settings.
33 |
34 | ### Interactivity and Notifications
35 | - **ChatGPT Integration**: Enhances user engagement with AI-driven chat support.
36 | - **Email Notifications**: Sends automated emails for actions such as registration and password resets using SMTP.
37 |
38 | ### Frontend Experience
39 | - **Next.js and MUI**: A modern, responsive UI built with Next.js and Material-UI for a seamless user experience.
40 | - **Responsive Design**: Ensures a consistent experience across various devices and screen sizes.
41 |
42 | ### Performance and Scalability
43 | - **Asynchronous Support**: Utilizes FastAPI's async features for efficient performance.
44 | - **Scalable Architecture**: Designed to handle growth in users and data smoothly.
45 |
46 | ### Logging and Monitoring
47 | - **Comprehensive Logging**: Detailed logging for user actions, system events, and errors.
48 | - **Real-Time Monitoring**: Tools and practices in place for monitoring application performance in real-time.
49 |
50 | ## Technologies Used
51 |
52 | - **FastAPI**: For building the high-performance API backend.
53 | - **Next.js**: The React framework for building the frontend.
54 | - **Material-UI (MUI)**: For designing the frontend with React UI components.
55 | - **MongoDB**: As the NoSQL database for user data.
56 | - **Redis**: For rate limiting and JWT token management.
57 | - **Docker**: For containerizing and deploying the application.
58 | - **Python-JOSE**: A library for JWT operations.
59 | - **SMTP Libraries**: For sending automated email notifications.
60 |
61 | ## Getting Started
62 |
63 | ### Prerequisites
64 |
65 | Before you begin, ensure you have met the following requirements:
66 |
67 | - **Docker**: This project is containerized with Docker, making it necessary to have Docker Desktop (for Windows or Mac) or Docker Engine (for Linux) installed on your system. To install Docker, follow the instructions on the [official Docker website](https://docs.docker.com/get-docker/).
68 |
69 | - **Python**: The project requires the latest version of Python for certain local scripts and integrations. To install Python, visit the [official Python website](https://www.python.org/downloads/) and download the latest version for your operating system. Ensure that Python is properly added to your system's PATH to allow for command-line execution.
70 |
71 | ## Installation
72 | Once you have Docker and Python installed, you're ready to proceed with the project setup. The next sections will guide you through configuring your development environment, running the project with Docker, and executing any necessary Python scripts or commands.
73 |
74 | **You can watch this:**
75 | [](https://youtu.be/H2oYT-Ame9w)
76 |
77 | ### Clone the Repository
78 | ```shell
79 | git clone https://github.com/georgekhananaev/PyNextStack
80 | ```
81 |
82 | ### Docker Installation for Full Deployment (4 Containers)
83 | 1. Create "chatgpt_credentials.env" file or revise the code in "generate_env.py".
84 |
85 | * Example of chatgpt_credentials.env:
86 | ```
87 | open_ai_organization=org-your_openai_key
88 | open_ai_secret_key=sk-your_openai_key
89 | ```
90 |
91 | 2. Run the Installation.
92 |
93 | * PowerShell / Linux (Option 1)
94 | ```shell
95 | python generate_env.py ; docker-compose build --no-cache ; docker-compose up -d
96 | ```
97 | * CMD (Option 2)
98 | ```shell
99 | python generate_env.py && docker-compose build --no-cache && docker-compose up -d
100 | ```
101 | * Manual (Option 3)
102 | ```shell
103 | python generate_env.py
104 | ```
105 | ```shell
106 | docker-compose build --no-cache
107 | ```
108 | ```shell
109 | docker-compose up -d
110 | ```
111 |
112 | ## Uninstall
113 |
114 | ```shell
115 | docker-compose down -v
116 | ```
117 |
118 | ### If you want to start just the backend (FastAPI)
119 | Just the FastAPI server. You must start MongoDB server, Redis server first. Change the username and password URI in the .env file above.
120 |
121 |
122 | Create a .env File or Run `python generate_env.py`
123 |
124 |
125 | ```text
126 | # mongodb connection
127 | mongodb_server=localhost
128 | mongodb_port=27017
129 | mongodb_username=bringthemhome
130 | mongodb_password=bringthemhome
131 |
132 | # fastapi
133 | fastapi_ui_username=bringthemhome
134 | fastapi_ui_password=bringthemhome
135 | jwt_secret_key=bringthemhome
136 | static_bearer_secret_key=bringthemhome
137 | algorithm=HS256
138 |
139 | # chatgpt
140 | open_ai_organization=org-your_openai_key
141 | open_ai_secret_key=sk-your_openai_key
142 |
143 | # default root user
144 | owner_username=root
145 | owner_password=bringthemhome
146 | owner_email=israel@israeli.com
147 |
148 | # Initial email settings located in app/components/initial settings.py
149 | ```
150 |
151 | Please note: MongoDB URI should be "localhost" if you're running it locally, or "mongodb" if you're running it inside a Docker container.
152 |
153 |
154 |
155 | * Update PIP && Install requirements.txt
156 | ```shell
157 | python.exe -m pip install --upgrade pip
158 | ```
159 | ```shell
160 | pip install -r requirements.txt
161 | ```
162 |
163 | * Start FastAPI server with Uvicorn
164 | ```shell
165 | uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
166 | ```
167 |
168 | ### If you want to run the frontend (Next.js)
169 | You need to have Node.js installed on your machine.
170 | Visit https://nodejs.org/ to download and install the latest version.
171 |
172 |
173 | Create a .env File If Necessary; Otherwise, Default Settings Are Loaded
174 |
175 |
176 | ```text
177 | # Set the URL for your backend here
178 | NEXT_PUBLIC_API_BASE_URL=http://localhost:8000/api/v1
179 |
180 | # The default value is 'bringthemhome'. Ensure this matches the 'static_bearer_secret_key' set in your backend.
181 | NEXT_PUBLIC_API_KEY=static_bearer_secret_key
182 | ```
183 |
184 | Please note: If you are running MongoDB locally, the URI should be set to "localhost". If you are running MongoDB inside a Docker container, the URI should be set to "mongodb".
185 |
186 |
187 |
188 | * For Development:
189 | ```shell
190 | npm install
191 | ```
192 | ```shell
193 | npm run dev
194 | ```
195 |
196 | * For Production:
197 | ```shell
198 | npm run build
199 | ```
200 | ```shell
201 | npm start
202 | ```
203 |
204 | ## Usage
205 |
206 | - Frontend: [http://localhost:3000](http://localhost:3000)
207 | ```
208 | Username: root
209 | Password: bringthemhome
210 | ```
211 | * Access the API documentation at http://localhost:8000/docs. You can obtain a token by entering your username and password from the text box above.
212 | * Please note that the Swagger UI is also password-protected, and it will temporarily block access if the password is entered incorrectly more than five times, for a duration of five minutes.
213 | ```
214 | Username: bringthemhome
215 | Password: bringthemhome
216 | ```
217 |
218 | ## Security Practices
219 | This application implements advanced security practices including password hashing, token validation, rate limiting, and secure API documentation access.
220 |
221 | ## License
222 | This project is licensed under the MIT License - see the LICENSE.md file for details.
223 |
224 | ## Credits
225 | - Developed by George Khananaev.
226 | - Thanks to the FastAPI, MongoDB, Redis, and Docker communities for support and resources.
227 |
228 | ## Additional Requests
229 |
230 | If you have ideas on how we can grow it into something greater, please share your suggestions. Additionally, if you have any special requests or unique features you'd like to see implemented, feel free to include those as well.
231 |
232 | ## Goals
233 |
234 | **Upon reaching 100 stars, the project will incorporate the following features:**
235 |
236 | 1. Firebase authentication support will be integrated.
237 | 2. Two-Factor Authentication will be implemented.
238 | 3. SMS and WhatsApp support will be added to the backend with TWILIO.
239 | 4. File handling capabilities, utilizing either Google Storage or AWS.
240 | 5. Enhanced email handling, including functionalities such as Inbox, Outgoing, Draft, etc.
241 |
242 | ## Support Me
243 |
244 | If you find my work helpful, consider supporting me by buying me a coffee at [Buy Me A Coffee](https://www.buymeacoffee.com/georgekhananaev).
245 |
246 | Your support helps me continue to create and maintain useful projects.
247 |
248 | [](https://www.buymeacoffee.com/georgekhananaev)
249 |
250 | Thank you!
--------------------------------------------------------------------------------
/app/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/georgekhananaev/PyNextStack/7546865fa1cd8b46140194813d9dae3e7310d925/app/__init__.py
--------------------------------------------------------------------------------
/app/classes/Auth.py:
--------------------------------------------------------------------------------
1 | # from typing import Optional
2 | from pydantic import BaseModel
3 |
4 |
5 | class SimpleAuthForm(BaseModel):
6 | username: str
7 | password: str
8 | # scope: Optional[str] = None # Make scope optional
9 |
--------------------------------------------------------------------------------
/app/classes/Chatgpt.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 |
4 | class ChatGptModelEnum(str, Enum):
5 | gpt_3_5_turbo = "gpt-3.5-turbo"
6 | gpt_4 = "gpt-4"
7 | # Add more models as needed
8 |
--------------------------------------------------------------------------------
/app/classes/Messages.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from pydantic import BaseModel, EmailStr
4 |
5 |
6 | class SmtpModel(BaseModel):
7 | active: Optional[bool] = None
8 | server: Optional[str] = None
9 | port: Optional[int] = None
10 | user: Optional[str] = None
11 | password: Optional[str] = None
12 | system_email: Optional[EmailStr] = None
13 |
14 |
15 | class WhatsappModel(BaseModel):
16 | active: Optional[bool] = None
17 | account_sid: Optional[str] = None
18 | auth_token: Optional[str] = None
19 | from_number: Optional[str] = None
20 |
21 |
22 | class SmsModel(BaseModel):
23 | active: Optional[bool] = None
24 | provider: Optional[str] = None
25 | api_key: Optional[str] = None
26 | from_number: Optional[str] = None
27 |
28 |
29 | class MessagesConfigModel(BaseModel):
30 | smtp: Optional[SmtpModel] = None
31 | whatsapp: Optional[WhatsappModel] = None
32 | sms: Optional[SmsModel] = None
33 |
--------------------------------------------------------------------------------
/app/classes/Permissions.py:
--------------------------------------------------------------------------------
1 | # permissions.py
2 |
3 | class RolePermissions:
4 | role_permissions_map = {
5 | "owner": ["read", "write", "delete", "edit"],
6 | "admin": ["read", "write", "edit"],
7 | "user": ["read"]
8 | }
9 |
10 |
11 | class HTTPMethodPermissions:
12 | method_permission_map = {
13 | "GET": "read",
14 | "POST": "write",
15 | "PUT": "edit",
16 | "DELETE": "delete",
17 | }
18 |
19 | @staticmethod
20 | def get_permission_for_method(method: str) -> str:
21 | """Retrieve the permission required for a given HTTP method."""
22 | return HTTPMethodPermissions.method_permission_map.get(method, "")
23 |
--------------------------------------------------------------------------------
/app/classes/User.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 | from typing import Optional
3 |
4 | from pydantic import BaseModel, Field, EmailStr
5 |
6 |
7 | # Define roles
8 | class Role(str, Enum):
9 | owner = "owner"
10 | admin = "admin"
11 | user = "user"
12 |
13 |
14 | class UserBase(BaseModel):
15 | username: str # User's username
16 | email: EmailStr # User's email, validated to be a proper email format
17 | full_name: str # User's full name
18 | disabled: Optional[bool] = None # Optional field to disable the user
19 | role: Role = Role.user # Default role
20 |
21 | class UpdateUser(BaseModel):
22 | username: Optional[str] = None # Now optional
23 | email: Optional[EmailStr] = None # Now optional, but still validated if provided
24 | full_name: Optional[str] = None # Now optional
25 | disabled: Optional[bool] = None # Remains optional
26 | role: Optional[Role] = Role.user # Optional, with a default value if not provided
27 | password: Optional[str] = None
28 |
29 | class UserCreate(UserBase):
30 | password: Optional[str] # Password field for user creation
31 |
32 |
33 | class User(UserBase):
34 | id: Optional[str] = Field(default=None, alias="_id") # User ID, mapping MongoDB's '_id'
35 |
36 | @classmethod
37 | def from_mongo(cls, data: dict):
38 | if "_id" in data:
39 | data["_id"] = str(data["_id"]) # Convert MongoDB ObjectId to string
40 | return cls(**data) # Create User instance with modified data
41 |
42 | class Config:
43 | json_schema_extra = {
44 | "example": {
45 | "username": "israel",
46 | "email": "israel@example.com",
47 | "full_name": "israel israeli",
48 | "disabled": False,
49 | }
50 | }
51 | from_attributes = True # Enable ORM mode for compatibility with databases
52 | populate_by_name = True # Allows field population by name, useful for fields with aliases
53 |
54 |
55 | class UserRegistration(BaseModel):
56 | username: str
57 | email: EmailStr
58 | full_name: str
59 | password: str
60 |
61 | class Config:
62 | json_schema_extra = {
63 | "example": {
64 | "username": "newuser",
65 | "email": "newuser@example.com",
66 | "full_name": "New User",
67 | "password": "securepassword123",
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/app/classes/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/georgekhananaev/PyNextStack/7546865fa1cd8b46140194813d9dae3e7310d925/app/classes/__init__.py
--------------------------------------------------------------------------------
/app/components/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/georgekhananaev/PyNextStack/7546865fa1cd8b46140194813d9dae3e7310d925/app/components/__init__.py
--------------------------------------------------------------------------------
/app/components/auth/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/georgekhananaev/PyNextStack/7546865fa1cd8b46140194813d9dae3e7310d925/app/components/auth/__init__.py
--------------------------------------------------------------------------------
/app/components/auth/check_permissions.py:
--------------------------------------------------------------------------------
1 | from fastapi import Depends, HTTPException, Request
2 | from starlette.status import HTTP_403_FORBIDDEN, HTTP_401_UNAUTHORIZED
3 |
4 | from app.classes.Permissions import RolePermissions, HTTPMethodPermissions
5 | from app.components.auth.jwt_token_handler import get_jwt_username
6 | from app.components.logger import logger
7 | from app.db.mongoClient import async_database
8 |
9 |
10 | async def check_permissions(request: Request, username: str = Depends(get_jwt_username)):
11 | """
12 | Check if the user has the required permissions to access the route.
13 | """
14 | try:
15 | user_collection = async_database.users
16 | user = await user_collection.find_one({"username": username})
17 | if not user:
18 | logger.error(f"User not found: {username}")
19 | raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="User not found.")
20 |
21 | if user.get("disabled", False):
22 | logger.warning(f"Access denied for disabled account: {username}")
23 | raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Account is disabled.")
24 |
25 | user_role = user.get("role")
26 |
27 | # Use the new classes for permission checking
28 | required_permission = HTTPMethodPermissions.get_permission_for_method(request.method)
29 |
30 | # Check if the user's role includes the required permission
31 | user_permissions = RolePermissions.role_permissions_map.get(user_role, [])
32 | if required_permission not in user_permissions:
33 | logger.warning(f"Permission denied: {username} attempted to {request.method}")
34 | raise HTTPException(status_code=HTTP_403_FORBIDDEN,
35 | detail="You don't have permission to perform this action.")
36 | logger.info(f"Permission granted: {username} accessed {request.url.path} with method {request.method}")
37 | except HTTPException as e:
38 | # Log the error and re-raise the exception
39 | logger.error(f"Error during permission check for user {username}: {str(e.detail)}")
40 | raise
41 |
--------------------------------------------------------------------------------
/app/components/auth/fastapi_auth.py:
--------------------------------------------------------------------------------
1 | import os
2 | import secrets
3 | from datetime import datetime, timedelta
4 |
5 | from dotenv import load_dotenv
6 | from fastapi import HTTPException, Depends, security, status, Request
7 | from fastapi.security import HTTPBasicCredentials, HTTPBasic
8 |
9 | # Load environment variables from .env file
10 | load_dotenv()
11 | SECRET_KEY = os.environ["static_bearer_secret_key"] # loading bearer_secret_key from.env file
12 |
13 | # Create an instance of HTTPBearer
14 | http_bearer = security.HTTPBearer()
15 | security_basic = HTTPBasic()
16 |
17 |
18 | async def get_secret_key(security_payload: security.HTTPAuthorizationCredentials = Depends(http_bearer)):
19 | """
20 | This function is used to get the secret key from the authorization header.
21 | :param security_payload:
22 | :return:
23 | """
24 | authorization = security_payload.credentials
25 | if not authorization or SECRET_KEY not in authorization:
26 | raise HTTPException(status_code=403, detail="Unauthorized")
27 | return authorization
28 |
29 |
30 | async def get_login_attempts(username: str, request: Request):
31 | redis_client = request.app.state.redis
32 | attempts = await redis_client.get(f"{username}:attempts")
33 | return int(attempts) if attempts else 0
34 |
35 |
36 | async def get_last_attempt_time(username: str, request: Request):
37 | """
38 | This function is used to get the last login attempt time.
39 | """
40 | redis_client = request.app.state.redis
41 |
42 | last_time = await redis_client.get(f"{username}:last_attempt") # This should be awaited
43 | if last_time:
44 | return datetime.fromtimestamp(float(last_time))
45 | return None
46 |
47 |
48 | async def set_failed_login(username: str, attempts: int, last_attempt_time: datetime, request: Request):
49 | """
50 | This function is used to set the number of failed login attempts and the last login attempt time.
51 | """
52 | redis_client = request.app.state.redis
53 |
54 | await redis_client.set(f"{username}:attempts", attempts, ex=300) # 5 minutes expiration
55 | await redis_client.set(f"{username}:last_attempt", last_attempt_time.timestamp(), ex=300) # 5 minutes expiration
56 |
57 |
58 | async def reset_login_attempts(username: str, request: Request):
59 | """
60 | This function is used to reset the number of failed login attempts and the last login attempt time.
61 | """
62 | redis_client = request.app.state.redis
63 | await redis_client.delete(f"{username}:attempts", f"{username}:last_attempt")
64 |
65 |
66 | async def verify_credentials(request: Request, credentials: HTTPBasicCredentials = Depends(security_basic)):
67 | username = credentials.username
68 | current_time = datetime.now()
69 |
70 | attempts = await get_login_attempts(username, request)
71 | last_attempt_time = await get_last_attempt_time(username, request)
72 | if attempts >= 5 and last_attempt_time and (current_time - last_attempt_time) < timedelta(minutes=5):
73 | raise HTTPException(status_code=status.HTTP_429_TOO_MANY_REQUESTS,
74 | detail="Too many login attempts. Please try again later.")
75 |
76 | correct_username = secrets.compare_digest(credentials.username, os.environ["fastapi_ui_username"])
77 | correct_password = secrets.compare_digest(credentials.password, os.environ["fastapi_ui_password"])
78 |
79 | if not (correct_username and correct_password):
80 | await set_failed_login(username, attempts + 1, current_time, request)
81 | raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password")
82 |
83 | await reset_login_attempts(username, request)
84 |
85 | return credentials
--------------------------------------------------------------------------------
/app/components/auth/jwt_token_handler.py:
--------------------------------------------------------------------------------
1 | import os
2 | from datetime import datetime, timedelta
3 |
4 | import jwt
5 | from dotenv import load_dotenv
6 | from fastapi import HTTPException, Header, Request
7 |
8 | # from app.db.mongoClient import database
9 |
10 | load_dotenv()
11 |
12 | # Secret key for JWT encoding and decoding. In a real app, keep this secret and safe!
13 | SECRET_KEY = os.environ["jwt_secret_key"] # Ensure this is corrected
14 | ALGORITHM = os.environ["algorithm"]
15 |
16 |
17 | async def create_jwt_access_token(request: Request, data: dict, expires_delta: timedelta = None):
18 | to_encode = data.copy() # data copy is required for jwt.encode() to work
19 | user_id = data["sub"] # Get the user_id from the payload
20 | redis_client = request.app.state.redis
21 |
22 | if expires_delta:
23 | expire = datetime.utcnow() + expires_delta
24 | else:
25 | expire = datetime.utcnow() + timedelta(minutes=120) # Token expires in 120 minutes by default
26 | to_encode.update({"exp": expire})
27 |
28 | encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
29 |
30 | # Token expiration in seconds (2 days)
31 | token_expiration_seconds = 48 * 60 * 60
32 |
33 | # Key for storing the mapping of user_id to their token
34 | user_key = f"USER_{user_id}_API_KEY"
35 |
36 | # Check if this user already has a token
37 | existing_api_key = await redis_client.get(user_key)
38 | if existing_api_key:
39 | # Delete the old token to invalidate it
40 | await redis_client.delete(f"API_KEY_{existing_api_key}")
41 |
42 | # Store the new token in Redis
43 | api_key = f"API_KEY_{encoded_jwt}" # Unique key for each user's token
44 | await redis_client.setex(api_key, token_expiration_seconds, user_id) # Storing user_id for reference, if needed
45 |
46 | # Update the user's current active token mapping
47 | await redis_client.setex(user_key, token_expiration_seconds, encoded_jwt)
48 |
49 | return encoded_jwt
50 |
51 |
52 | async def get_jwt_secret_key(request: Request, api_key: str = Header(...)):
53 | redis_client = request.app.state.redis
54 |
55 | token_key = f"API_KEY_{api_key}"
56 | token_exists = await redis_client.exists(token_key)
57 | if not token_exists:
58 | raise HTTPException(status_code=401, detail="API key is invalid or has expired")
59 | # If the key exists, you might want to return something or just let the request pass
60 | return api_key
61 |
62 |
63 | async def get_user_id_from_jwt(token: str):
64 | """
65 | Extracts the user ID from the provided JWT token.
66 | :param token:
67 | :return:
68 | """
69 | try:
70 | # Decode the JWT without verification just to extract user ID
71 | payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM], options={"verify_signature": False})
72 | user_id = payload.get("sub")
73 | if not user_id:
74 | raise HTTPException(status_code=400, detail="Invalid JWT token: No user ID (sub) present.")
75 | return user_id
76 | except jwt.PyJWTError as e:
77 | # Handle decoding errors (e.g., token is malformed)
78 | raise HTTPException(status_code=400, detail=f"Invalid JWT token: {e}")
79 |
80 |
81 | async def get_username_from_api_key(request: Request, api_key: str = Header(...)):
82 | """
83 | Extracts the username (user_id) from the provided API key (JWT token).
84 | Optionally verifies if the token is still valid and not expired.
85 | """
86 | redis_client = request.app.state.redis
87 |
88 | # Extract user_id from the JWT token
89 | user_id = get_user_id_from_jwt(api_key)
90 |
91 | # Here you might want to verify if the token is still valid by checking
92 | # if it exists in Redis or if it's not expired, depending on your application logic.
93 |
94 | # Assuming redisClient is already connected and setup
95 | token_exists = await redis_client.exists(f"API_KEY_{api_key}")
96 | if not token_exists:
97 | raise HTTPException(status_code=401, detail="API key is invalid or has expired")
98 |
99 | # Return the user_id if everything is valid
100 | return user_id
101 |
102 |
103 | async def get_jwt_username(api_key: str = Header(...)):
104 | try:
105 | payload = jwt.decode(api_key, SECRET_KEY, algorithms=[ALGORITHM])
106 | username = payload.get("sub", None)
107 | if not username:
108 | raise HTTPException(status_code=401, detail="Invalid JWT token: Username missing")
109 | return username
110 | except jwt.PyJWTError:
111 | raise HTTPException(status_code=401, detail="Invalid JWT token")
--------------------------------------------------------------------------------
/app/components/chat_gpt/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/georgekhananaev/PyNextStack/7546865fa1cd8b46140194813d9dae3e7310d925/app/components/chat_gpt/__init__.py
--------------------------------------------------------------------------------
/app/components/chat_gpt/chatgpt_service.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 |
4 | import httpx
5 | from dotenv import load_dotenv
6 |
7 | load_dotenv()
8 | API_KEY = os.getenv("open_ai_secret_key")
9 |
10 | logging.basicConfig(level=logging.INFO)
11 | logger = logging.getLogger(__name__)
12 |
13 |
14 | def ask_chatgpt_with_context(gpt_question: str, model: str = "gpt-3.5-turbo"):
15 | headers = {
16 | "Authorization": f"Bearer {API_KEY}"
17 | }
18 |
19 | payload = {
20 | "model": model,
21 | "messages": [
22 | {"role": "user", "content": gpt_question}
23 | ]
24 | }
25 |
26 | try:
27 | with httpx.Client() as client:
28 | response = client.post("https://api.openai.com/v1/chat/completions", headers=headers, json=payload)
29 | response.raise_for_status()
30 | data = response.json()
31 | return ''.join(choice['message']['content'] for choice in data['choices'] if
32 | 'message' in choice and 'content' in choice['message'])
33 | except httpx.HTTPStatusError as e:
34 | logger.error(f"HTTP error occurred: {e}")
35 | except httpx.RequestError as e:
36 | logger.error(f"Request error occurred: {e}")
37 | except Exception as e:
38 | logger.error(f"An unexpected error occurred: {e}")
39 | return "Error: Unable to fetch response."
40 |
41 | # Example usage
42 | if __name__ == "__main__":
43 | question = "Which nation founded Jerusalem and has the strongest connection with it throughout known history?"
44 | print(ask_chatgpt_with_context(question))
45 |
--------------------------------------------------------------------------------
/app/components/hash_password.py:
--------------------------------------------------------------------------------
1 | from passlib.context import CryptContext
2 |
3 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
4 |
5 |
6 | def hash_password(password: str) -> str:
7 | """
8 | Hash the password
9 | :param password:
10 | :return:
11 | """
12 | return pwd_context.hash(password)
13 |
--------------------------------------------------------------------------------
/app/components/initial_settings.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import pymongo
4 | from bson import ObjectId
5 | from pymongo.errors import DuplicateKeyError
6 | from dotenv import load_dotenv
7 |
8 | from app.classes.User import UserCreate
9 | from app.components.logger import logger
10 | from app.db.mongoClient import async_database
11 | from app.routers.users import create_user
12 |
13 | load_dotenv() # loading environment variables
14 |
15 |
16 | async def check_key_not_expired(redis_client, key):
17 | """
18 | Check if a given key exists and thus has not expired.
19 | """
20 | # Attempt to get the value for the specified key
21 | value = redis_client.get(key)
22 |
23 | # If the value exists, the key has not expired
24 | if value is not None:
25 | return True
26 | else:
27 | return False
28 |
29 |
30 | async def create_indexes():
31 | await async_database.users.create_index("email", unique=True)
32 | await async_database.users.create_index("username", unique=True)
33 |
34 | async def create_owner():
35 | # Fetch owner's email and username from environment variables
36 | owner_email = os.getenv("owner_email")
37 | owner_username = os.getenv("owner_username")
38 |
39 | # Assuming async_database.users is your collection from an async database
40 | user_collection = async_database.users
41 |
42 | # Check if an owner with the same email already exists
43 | if await user_collection.find_one({"email": owner_email}):
44 | logger.info(f"An owner with email {owner_email} already exists, skipping creation.")
45 | return
46 |
47 | # Check if an owner with the same username already exists
48 | if await user_collection.find_one({"username": owner_username}):
49 | logger.info(f"An owner with username {owner_username} already exists, skipping creation.")
50 | return
51 |
52 | # Define the admin user with details from environment variables
53 | admin_user = {
54 | "username": owner_username,
55 | "email": owner_email,
56 | "full_name": "Site Owner",
57 | "disabled": False,
58 | "password": os.getenv("owner_password"), # Assuming the password is also stored in env variables
59 | "role": "owner"
60 | }
61 |
62 | # Create the user with the specified admin user details
63 | try:
64 | await create_user(UserCreate(**admin_user), "system_init")
65 | logger.info("Owner created successfully.")
66 | except pymongo.errors.DuplicateKeyError:
67 | logger.info("Owner already exists, skipping creation.")
68 |
69 | logger.info("Owner created successfully.")
70 |
71 |
72 | async def initialize_message_settings():
73 | settings_collection = async_database.settings # Assuming a collection named 'settings'
74 |
75 | # Convert the string ID to an ObjectId
76 | settings_id = ObjectId("65fdaaca4f94194ff730d3be")
77 |
78 | # Check if settings already exist to avoid duplication
79 | existing_settings = await settings_collection.find_one({"_id": settings_id})
80 | if existing_settings:
81 | logger.info("Message settings already initialized, skipping.")
82 | return
83 |
84 | # Define the initial settings document with ObjectId for _id
85 | initial_settings = {
86 | "_id": settings_id,
87 | "smtp": {
88 | "active": False,
89 | "server": "smtp.gmail.com",
90 | "port": 587,
91 | "user": "your_email@gmail.com",
92 | "password": "your_password",
93 | "system_email": "your_email@gmail.com"
94 | },
95 | "whatsapp": {
96 | "active": False,
97 | "account_sid": "your_account_sid",
98 | "auth_token": "your_auth_token",
99 | "from_number": "+1234567890"
100 | },
101 | "sms": {
102 | "active": False,
103 | "provider": "Twilio",
104 | "api_key": "your_api_key",
105 | "from_number": "+1234567890"
106 | }
107 | }
108 |
109 | # Insert the initial settings document into the database
110 | await settings_collection.insert_one(initial_settings)
111 | logger.info("Initial settings have been successfully set up.")
112 |
--------------------------------------------------------------------------------
/app/components/logger.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | from datetime import datetime
4 | from logging.handlers import BaseRotatingHandler
5 |
6 |
7 | class DailyRotatingFileHandler(BaseRotatingHandler):
8 | """Rotating file handler that rotates the file every day."""
9 |
10 | def __init__(self, dir_name, base_filename, encoding=None):
11 | """
12 | :param dir_name: Directory to store monthly folders
13 | """
14 | self.dir_name = dir_name # Directory to store monthly folders
15 | self.base_filename = base_filename # Base name for log files
16 | self.encoding = encoding
17 | self.current_date = None
18 | filename = self._get_filename() # Initial filename
19 | os.makedirs(os.path.dirname(filename), exist_ok=True) # Ensure directory exists
20 | BaseRotatingHandler.__init__(self, filename, 'a', encoding, delay=True)
21 |
22 | def _get_filename(self):
23 | """Generate a filename based on the current date."""
24 | now = datetime.now()
25 | month_dir = now.strftime('%Y-%m') # Format: YYYY-MM
26 | date_str = now.strftime('%Y-%m-%d') # Format: YYYY-MM-DD
27 | self.current_date = date_str
28 | filename = f"{self.base_filename}_{date_str}.log"
29 | full_path = os.path.join(self.dir_name, month_dir, filename)
30 | return full_path
31 |
32 | def shouldRollover(self, record): # noqa
33 | """Determine if we should rollover to a new file."""
34 | now = datetime.now().strftime('%Y-%m-%d')
35 | return self.current_date != now # True if the date has changed
36 |
37 | def doRollover(self): # noqa
38 | """Roll over to a new log file."""
39 | self.stream.close()
40 | self.baseFilename = self._get_filename()
41 | self.stream = self._open()
42 |
43 |
44 | # Create base 'logs' directory if it doesn't exist
45 | base_log_dir = 'logs'
46 | if not os.path.exists(base_log_dir):
47 | os.makedirs(base_log_dir)
48 |
49 | # Configure logging
50 | logging.basicConfig(level=logging.INFO)
51 | logger = logging.getLogger(__name__)
52 |
53 | # Use the custom handler
54 | handler = DailyRotatingFileHandler(base_log_dir, 'fastapi', encoding='utf-8')
55 | formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') # noqa
56 | handler.setFormatter(formatter)
57 | logger.addHandler(handler)
58 |
59 | # Ensure that logging of each log level gets propagated to the root logger
60 | logger.propagate = True
61 |
--------------------------------------------------------------------------------
/app/components/message_dispatcher/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/georgekhananaev/PyNextStack/7546865fa1cd8b46140194813d9dae3e7310d925/app/components/message_dispatcher/__init__.py
--------------------------------------------------------------------------------
/app/components/message_dispatcher/mail.py:
--------------------------------------------------------------------------------
1 | import smtplib
2 | from email import encoders
3 | from email.mime.base import MIMEBase
4 | from email.mime.multipart import MIMEMultipart
5 | from email.mime.text import MIMEText
6 | from typing import List
7 |
8 | from aiosmtplib import SMTP, SMTPException
9 | from bson import ObjectId
10 | from fastapi import UploadFile, HTTPException
11 |
12 | from app.components.logger import logger
13 | from app.db.mongoClient import async_mdb_client, async_database
14 |
15 | # mongo connection
16 | config_collection = async_database.settings
17 | emails_collection = async_mdb_client.messages.emails_sent
18 |
19 |
20 | async def get_config_data(config_key: str):
21 | config_id = "65fdaaca4f94194ff730d3be"
22 | config = await config_collection.find_one({"_id": ObjectId(config_id)})
23 | if not config:
24 | raise HTTPException(status_code=404, detail="Config not found")
25 | return config.get(config_key)
26 |
27 |
28 | async def send_email_and_save(subject: str, body: str, to_emails: List[str], files: List[UploadFile] = []): # noqa
29 | smtp_config = await get_config_data("smtp")
30 | smtp = None # Declare smtp here
31 |
32 | # Prepare the email message
33 | msg = MIMEMultipart()
34 | msg['Subject'] = subject
35 | msg['From'] = smtp_config["system_email"]
36 | msg['To'] = ', '.join(to_emails)
37 | msg.attach(MIMEText(body, 'plain'))
38 |
39 | # Attach files
40 | for file in files:
41 | part = MIMEBase('application', "octet-stream")
42 | content = await file.read() # Ensure you read the content here
43 | part.set_payload(content)
44 | encoders.encode_base64(part)
45 | part.add_header('Content-Disposition', f'attachment; filename={file.filename}')
46 | msg.attach(part)
47 |
48 | try:
49 | smtp = SMTP(hostname=smtp_config["server"], port=smtp_config["port"])
50 | if smtp_config["port"] == 587:
51 | await smtp.connect(start_tls=True)
52 | else:
53 | # Assuming SSL from the start for port 465 or other SSL ports
54 | await smtp.connect(use_tls=True)
55 |
56 | await smtp.login(smtp_config["user"], smtp_config["password"])
57 | await smtp.send_message(msg)
58 |
59 | logger.info("Email sent successfully.")
60 | except SMTPException as e:
61 | logger.error(f"Failed to send email. Error: {e}")
62 | raise
63 | finally:
64 | await smtp.quit()
65 |
66 | # Save email details in MongoDB after successful sending
67 | email_data = {
68 | "subject": subject,
69 | "body": body,
70 | "to_emails": to_emails,
71 | "attachments": [file.filename for file in files],
72 | }
73 | await emails_collection.insert_one(email_data)
74 | logger.info("Email data saved successfully in MongoDB.")
75 | return {"message": "Email sent and saved successfully"}
76 |
77 |
78 | async def test_email_connection():
79 | smtp_config = await get_config_data("smtp")
80 | smtp = None # Declare smtp here
81 |
82 | msg = MIMEMultipart()
83 | msg['Subject'] = "Test Email Connection"
84 | msg['From'] = smtp_config["system_email"]
85 | msg['To'] = "your_email@gmail.com"
86 | msg.attach(MIMEText("This is a test email to verify SMTP configuration.", 'plain'))
87 |
88 | try:
89 | smtp = SMTP(hostname=smtp_config["server"], port=smtp_config["port"])
90 | if smtp_config["port"] == 587:
91 | await smtp.connect(start_tls=True)
92 | else:
93 | await smtp.connect(use_tls=True) # For SSL from the beginning without STARTTLS
94 | await smtp.login(smtp_config["user"], smtp_config["password"])
95 | await smtp.send_message(msg)
96 |
97 | # If email sent successfully, update 'active' to True
98 | await config_collection.update_one(
99 | {"_id": ObjectId("65fdaaca4f94194ff730d3be")},
100 | {"$set": {"smtp.active": True}}
101 | )
102 | logger.info("Test email sent successfully. SMTP status set to active.")
103 | return {"message": "Test email sent successfully. SMTP status set to active."}
104 |
105 | except SMTPException as e:
106 | # If sending fails, update 'active' to False
107 | await config_collection.update_one(
108 | {"_id": ObjectId("65fdaaca4f94194ff730d3be")},
109 | {"$set": {"smtp.active": False}}
110 | )
111 | logger.error(f"Failed to send test email. SMTP status set to inactive. Error: {str(e)}")
112 | return {"message": f"Failed to send test email. SMTP status set to inactive. Error: {str(e)}"}
113 |
114 | finally:
115 | await smtp.quit()
116 |
117 |
118 | async def main():
119 | await test_email_connection()
120 |
121 |
122 | def manual_send_test_email(smtp_server, port, sender_email, receiver_email, password, subject, body):
123 | """
124 | Send a test email using specified SMTP server settings.
125 |
126 | Parameters:
127 | smtp_server (str): SMTP server host name.
128 | port (int): SMTP server port number.
129 | sender_email (str): The email address sending the email.
130 | receiver_email (str): The email address receiving the email.
131 | password (str): Password or app-specific password for the sender's email account.
132 | subject (str): Subject line of the email.
133 | body (str): Body content of the email.
134 | """
135 |
136 | # Create a MIMEText object to represent the email.
137 | msg = MIMEMultipart()
138 | msg['From'] = sender_email
139 | msg['To'] = receiver_email
140 | msg['Subject'] = subject
141 | msg.attach(MIMEText(body, 'plain'))
142 |
143 | try:
144 | # Choose the right connection type based on the port
145 | if port == 465:
146 | # Use SMTP_SSL for a direct SSL connection
147 | server = smtplib.SMTP_SSL(smtp_server, port)
148 | else:
149 | # Use SMTP and upgrade to SSL with starttls
150 | server = smtplib.SMTP(smtp_server, port)
151 | server.starttls() # Upgrade the connection to encrypted TLS
152 |
153 | # Log in to the server and send the email
154 | server.login(sender_email, password)
155 | server.send_message(msg) # Using send_message simplifies the API usage
156 | server.quit()
157 |
158 | logger.info("Email sent successfully!")
159 | except Exception as e:
160 | logger.error(f"Failed to send email. Error: {e}")
161 |
162 |
163 | if __name__ == "__main__":
164 | manual_send_test_email("smtp.gmail.com", "465", "mhgfallback@gmail.com", "mhgfallback@gmail.com", "password", "Test Email",
165 | "This is a test email.")
--------------------------------------------------------------------------------
/app/db/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/georgekhananaev/PyNextStack/7546865fa1cd8b46140194813d9dae3e7310d925/app/db/__init__.py
--------------------------------------------------------------------------------
/app/db/mongoClient.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import os
3 |
4 | from dotenv import load_dotenv
5 | from motor.motor_asyncio import AsyncIOMotorClient
6 |
7 | from app.components.logger import logger
8 |
9 | load_dotenv() # loading environment variables
10 |
11 | MONGODB_USER = os.getenv("mongodb_username")
12 | MONGODB_PASS = os.getenv("mongodb_password")
13 | MONGODB_SERVER = os.getenv("mongodb_server")
14 | MONGODB_PORT = os.getenv("mongodb_port")
15 |
16 | MONGODB_CONNECTION_STRING = f"mongodb://{MONGODB_USER}:{MONGODB_PASS}@{MONGODB_SERVER}:{MONGODB_PORT}"
17 |
18 | # Asynchronous MongoDB connection
19 | async_connection_string = f'{MONGODB_CONNECTION_STRING}' # This can be the same as the synchronous connection string
20 | async_mdb_client = AsyncIOMotorClient(async_connection_string) # setting mongodb client for asynchronous operations
21 | async_database = async_mdb_client['fastapi_db'] # database name in mongodb for asynchronous operations
22 |
23 |
24 | # Function to validate connection
25 | async def validate_mongodb_connection():
26 | try:
27 | # Attempt to count documents in a specific collection, e.g., 'test_collection'
28 | count = await async_database['test_collection'].count_documents({})
29 | print(f"Connection Successful! Found {count} documents in 'test_collection'.")
30 | except Exception as e:
31 | logger.info(f"Connection to MongoDB failed: {e}")
32 |
33 | # Running the validation function
34 | if __name__ == "__main__":
35 | loop = asyncio.get_event_loop()
36 | loop.run_until_complete(validate_mongodb_connection())
--------------------------------------------------------------------------------
/app/db/redisClient.py:
--------------------------------------------------------------------------------
1 | import redis.asyncio as aioredis
2 |
3 | from app.components.logger import logger
4 |
5 |
6 | class AsyncRedisClient:
7 | _instance = None
8 |
9 | @classmethod
10 | async def get_instance(cls):
11 | """
12 | Asynchronously get the singleton instance of the Redis client.
13 | """
14 | if cls._instance is None:
15 | cls._instance = await cls.create_redis_client()
16 | return cls._instance
17 |
18 | @staticmethod
19 | async def create_redis_client():
20 | """
21 | Asynchronously create a Redis client. Tries to connect to localhost, redis, and 0.0.0.0.
22 | """
23 | hosts = ['localhost', 'redis', '0.0.0.0']
24 | for host in hosts:
25 | try:
26 | client = aioredis.StrictRedis(host=host, port=6379, db=0, decode_responses=True)
27 | # The ping command is now an awaitable coroutine
28 | if await client.ping():
29 | logger.info(f"Successfully connected to Redis server at {host}")
30 | return client
31 | except aioredis.ConnectionError as e:
32 | logger.error(f"Could not connect to Redis server at {host}: {e}.")
33 | raise Exception("Could not connect to any Redis server.")
34 |
35 | # # An Redis Lock Example for Future Implementations When Running Multiple Workers
36 | # async def create_initial_users(redis_client):
37 | # lock_key = "lock_users_creation" # Unique key for locking
38 | # expiry_seconds = 120 # Lock expiration time
39 | #
40 | # try:
41 | # lock_acquired = await redis_client.set(lock_key, "1", ex=expiry_seconds, nx=True)
42 | # if lock_acquired:
43 | # try:
44 | # await create_owner()
45 | # except Exception as e:
46 | # logger.error(f"Error during schedule contact update: {e}")
47 | # await asyncio.sleep(expiry_seconds)
48 | # else:
49 | # await asyncio.sleep(30)
50 | # finally:
51 | # await redis_client.close()
52 | # await redis_client.connection_pool.disconnect()
53 |
--------------------------------------------------------------------------------
/app/routers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/georgekhananaev/PyNextStack/7546865fa1cd8b46140194813d9dae3e7310d925/app/routers/__init__.py
--------------------------------------------------------------------------------
/app/routers/auth.py:
--------------------------------------------------------------------------------
1 | from datetime import timedelta
2 | from typing import Any
3 |
4 | from fastapi import APIRouter, HTTPException, Depends, status, Header, Request
5 | from fastapi.security import OAuth2PasswordBearer
6 | from passlib.context import CryptContext
7 |
8 | from app.classes.Auth import SimpleAuthForm
9 | from app.components.auth.jwt_token_handler import create_jwt_access_token
10 | from app.components.logger import logger
11 | from app.db.mongoClient import async_database
12 |
13 | router = APIRouter()
14 |
15 | user_collection = async_database.users # Get the collection from the database
16 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") # setup password hashing context
17 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") # Dependency
18 |
19 |
20 | async def authenticate_user(username: str, password: str):
21 | """
22 | Authenticate the user by connecting to MongoDB asynchronously and checking the password.
23 | :param username:
24 | :param password:
25 | :return:
26 | """
27 | user = await user_collection.find_one({"username": username}) # Use `await` for async operation
28 | if not user:
29 | return False
30 | if not pwd_context.verify(password, user.get("hashed_password")): # Verify the password
31 | return False
32 | return user
33 |
34 |
35 | @router.post("/token")
36 | async def login_for_access_token(request: Request, form_data: SimpleAuthForm = Depends()):
37 | """
38 | Log-in and generate access token.
39 |
40 | :param request: The request object, providing access to HTTP request properties.
41 | :param form_data: The form data from the login request, containing the username and password.
42 | :return: A JSON object containing the access token and token type.
43 |
44 | This endpoint verifies the user's credentials. If valid, it generates a JWT access token that the user
45 | can use for authenticated requests. The token includes a 'sub' claim containing the username, and it
46 | has a default expiration time. This token is also stored in Redis with an expiration time for validation
47 | on subsequent requests.
48 | """
49 | try:
50 | # Authenticate the user
51 | user: dict | Any = await authenticate_user(form_data.username, form_data.password)
52 | if not user:
53 | logger.warning(f"Login attempt failed for user: {form_data.username}")
54 | raise HTTPException(
55 | status_code=status.HTTP_401_UNAUTHORIZED,
56 | detail="Incorrect username or password",
57 | )
58 |
59 | # Specify token expiration duration
60 | access_token_expires = timedelta(minutes=30) # Token validity can be adjusted
61 |
62 | # Create JWT access token
63 | access_token = await create_jwt_access_token(
64 | request=request,
65 | data={"sub": user["username"]}, # 'sub' claim to include the username
66 | expires_delta=access_token_expires
67 | )
68 |
69 | logger.info(f"Login successful for user: {form_data.username}")
70 | return {"access_token": access_token, "token_type": "bearer"}
71 |
72 | except HTTPException: # Catch and re-raise HTTPException to ensure it stops execution
73 | raise
74 |
75 | except Exception as e:
76 | logger.error(f"An error occurred during login attempt: {str(e)}")
77 | raise HTTPException(
78 | status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
79 | detail="An error occurred during the login process."
80 | )
81 |
82 |
83 | @router.post("/logout")
84 | async def logout(request: Request, api_key: str = Header(...)):
85 | """
86 | Invalidate the user's current JWT token to log them out.
87 | """
88 | try:
89 | token_key = f"API_KEY_{api_key}"
90 | # Use the async get_instance method to get the Redis client
91 | redis_client = request.app.state.redis
92 |
93 | # Async call to check if the token exists
94 | token_exists = await redis_client.exists(token_key)
95 | if not token_exists:
96 | raise HTTPException(status_code=404, detail="Token not found or already invalidated")
97 |
98 | # Invalidate the token by deleting it from Redis asynchronously
99 | await redis_client.delete(token_key)
100 | return {"message": "Logged out successfully."}
101 | except Exception as e:
102 | raise HTTPException(
103 | status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
104 | detail=f"An error occurred: {str(e)}"
105 | )
106 |
--------------------------------------------------------------------------------
/app/routers/chatgpt.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter, Depends
2 | from starlette.responses import JSONResponse
3 |
4 | from app.classes.Chatgpt import ChatGptModelEnum
5 | from app.components.auth.check_permissions import check_permissions
6 | from app.components.chat_gpt.chatgpt_service import ask_chatgpt_with_context
7 |
8 | router = APIRouter() # loading the FastAPI app
9 |
10 |
11 | @router.get("/chat/", dependencies=[Depends(check_permissions)])
12 | async def chat_with_gpt(question: str, model: ChatGptModelEnum):
13 | """
14 | Chat with ChatGPT
15 | """
16 | answer = ask_chatgpt_with_context(question, model)
17 | return JSONResponse(content={"message": answer})
18 |
--------------------------------------------------------------------------------
/app/routers/register.py:
--------------------------------------------------------------------------------
1 | import secrets
2 | from datetime import timedelta
3 |
4 | from bson import ObjectId
5 | from fastapi import status, Request, HTTPException, APIRouter
6 |
7 | from app.classes.User import User, Role, UserRegistration
8 | from app.components.hash_password import hash_password
9 | from app.components.logger import logger
10 | from app.components.message_dispatcher.mail import send_email_and_save
11 | from app.db.mongoClient import async_database
12 | from app.routers.users import user_exists
13 |
14 | router = APIRouter() # router instance
15 | user_collection = async_database.users # Get the collection from the database
16 |
17 |
18 | @router.post("/register/", response_model=User)
19 | async def create_user(user_registration: UserRegistration):
20 | """
21 | User registration endpoint.
22 | This function handles the registration of a new user.
23 | It checks for uniqueness of the email and username, hashes the password, and inserts a new user into the database.
24 | """
25 |
26 | try:
27 | # Check if the user with the given email or username already exists.
28 | if await user_exists(email=user_registration.email, username=user_registration.username):
29 | detail_msg = "The email or username is already in use."
30 | logger.warning(
31 | f"Attempt to create a user with an existing email or username by {user_registration.username}")
32 | # If the user exists, raise an HTTP exception with a 400 status code.
33 | raise HTTPException(
34 | status_code=status.HTTP_400_BAD_REQUEST,
35 | detail=detail_msg
36 | )
37 |
38 | # Hash the plaintext password provided in the registration.
39 | hashed_password = hash_password(user_registration.password)
40 |
41 | # Prepare the user document for insertion into the database.
42 | # This includes removing the plaintext password and adding hashed_password, role, and disabled status.
43 | user_dict = user_registration.dict()
44 | user_dict["hashed_password"] = hashed_password
45 | del user_dict["password"] # Remove plaintext password from the document
46 | user_dict["role"] = Role.user.value # Explicitly set the user's role to 'user'
47 | user_dict["disabled"] = False # New users are not disabled by default
48 |
49 | # Insert the new user document into the database collection.
50 | new_user = await user_collection.insert_one(user_dict)
51 |
52 | # Retrieve the newly created user document from the database to return it.
53 | created_user = await user_collection.find_one({"_id": new_user.inserted_id})
54 | created_user['_id'] = str(created_user['_id']) # Convert ObjectId to string for JSON serialization
55 | del created_user["hashed_password"] # Remove hashed password from the returned user document
56 |
57 | logger.info(f"Registered new user {user_registration.username} successfully.")
58 |
59 | # Return the created user, converting the MongoDB document to the User model.
60 | return User.from_mongo(created_user)
61 | except Exception as e:
62 | logger.error(f"Error registering new user {user_registration.username}: {str(e)}")
63 | # Raise a 500 status code HTTP exception if an unexpected error occurs during registration.
64 | raise HTTPException(status_code=500, detail="An error occurred while creating the user.")
65 |
66 |
67 | @router.post("/users/forgot-password/")
68 | async def forgot_password(email: str, request: Request):
69 | """
70 | Initiates the password reset process for a user identified by their email.
71 |
72 | This endpoint will:
73 | - Verify if a user with the provided email exists in the database.
74 | - Generate a secure, random token to be used as a password reset token.
75 | - Store this token in Redis with an expiration time, linking it to the user's ID.
76 | - Email the user with a link containing the password reset token.
77 |
78 | Args:
79 | - email (str): The email address of the user requesting a password reset.
80 | - request (Request): The request object, used to access app state like the Redis client.
81 |
82 | Returns:
83 | - A message indicating that a password reset link has been sent if a user with the provided email exists.
84 | """
85 |
86 | # Look for the user in the database using the provided email address.
87 | user = await user_collection.find_one({"email": email})
88 | redis_client = request.app.state.redis
89 |
90 | # If no user is found with the provided email, return a 404 error.
91 | if not user:
92 | raise HTTPException(status_code=404, detail="User with this email does not exist.")
93 |
94 | # Generate a secure, random token for the password reset
95 | reset_token = secrets.token_urlsafe(64)
96 | # reset_token_expires = int((datetime.utcnow() + timedelta(hours=1)).timestamp())
97 |
98 | # Use Redis to store the token with an expiration time
99 | await redis_client.setex(f"reset_token:{reset_token}", timedelta(hours=1), value=str(user["_id"]))
100 |
101 | # Email the user with the reset token
102 | reset_link = f"http://localhost:3000/reset-password/{reset_token}"
103 | await send_email_and_save(
104 | subject="Password Reset Request",
105 | body=f"Please click on the link to reset your password: {reset_link}, the URL is valid for one hour.",
106 | to_emails=[email],
107 | files=[]
108 | )
109 | return {"message": "If an account with this email was found, a password reset link has been sent."}
110 |
111 |
112 | @router.post("/users/reset-password/")
113 | async def reset_password(token: str, new_password: str, request: Request):
114 | """Reset user password given a valid token and new password.
115 |
116 | This endpoint retrieves the user ID associated with the provided token from Redis,
117 | verifies that the token is valid (i.e., has not expired and exists), hashes the new password,
118 | updates the user's password in the database, and then optionally deletes the token from Redis.
119 | """
120 |
121 | # Access the Redis client from the application state
122 | redis_client = request.app.state.redis
123 |
124 | # Attempt to retrieve the user_id associated with the provided token
125 | user_id_str = await redis_client.get(f"reset_token:{token}")
126 | if not user_id_str:
127 | # If no user_id is found, the token is invalid or expired
128 | raise HTTPException(status_code=400, detail="Invalid or expired password reset token.")
129 |
130 | # Convert the user_id string to ObjectId for MongoDB.
131 | # No need to decode since it's already a string.
132 | user_id = ObjectId(user_id_str)
133 |
134 | # Hash the new password before storing it
135 | hashed_password = hash_password(new_password)
136 |
137 | # Update the user's password in the database
138 | result = await user_collection.update_one(
139 | {"_id": user_id},
140 | {"$set": {"hashed_password": hashed_password}}
141 | )
142 |
143 | # Check if the password was actually updated
144 | if result.modified_count == 0:
145 | # This means the user ID did not match any document in the database
146 | raise HTTPException(status_code=500, detail="Failed to reset the password.")
147 |
148 | # Optionally, delete the token from Redis after successful password reset
149 | await redis_client.delete(f"reset_token:{token}")
150 |
151 | # Return a success message
152 | return {"message": "Password has been reset successfully."}
--------------------------------------------------------------------------------
/app/routers/senders.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 | from fastapi import APIRouter, UploadFile, File, Form
3 | from app.components.message_dispatcher.mail import send_email_and_save
4 |
5 | router = APIRouter()
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/routers/settings/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/georgekhananaev/PyNextStack/7546865fa1cd8b46140194813d9dae3e7310d925/app/routers/settings/__init__.py
--------------------------------------------------------------------------------
/app/routers/settings/messages.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | from bson import ObjectId
4 | from fastapi import APIRouter, UploadFile, File, Form, HTTPException
5 |
6 | from app.classes.Messages import MessagesConfigModel
7 | from app.components.message_dispatcher.mail import send_email_and_save, test_email_connection
8 | from app.db.mongoClient import async_mdb_client, async_database
9 |
10 | router = APIRouter()
11 |
12 | # mongo connection
13 | config_collection = async_database.settings
14 | emails_collection = async_mdb_client.messages.emails_sent
15 |
16 |
17 | @router.get("/config", response_model=MessagesConfigModel)
18 | async def get_config():
19 | """Fetch a specific configuration object from the database.
20 |
21 | Raises a 404 error if the configuration cannot be found.
22 | """
23 |
24 | config_id = "65fdaaca4f94194ff730d3be" # noqa
25 | config = await config_collection.find_one({"_id": ObjectId(config_id)}) # Use ObjectId to convert the config_id
26 | if config:
27 | return config
28 | else:
29 | raise HTTPException(status_code=404, detail="Configuration not found")
30 |
31 |
32 | @router.put("/config", response_model=MessagesConfigModel)
33 | async def update_config(config: MessagesConfigModel):
34 | """Update an existing configuration object in the database.
35 |
36 | If the specified configuration does not exist, a 404 error is raised.
37 | This method ensures that the MongoDB document structure is respected during the update.
38 | """
39 | config_id = "65fdaaca4f94194ff730d3be" # noqa
40 | # Use ObjectId to convert the config_id
41 | existing_config = await config_collection.find_one({"_id": ObjectId(config_id)})
42 | if existing_config is None:
43 | raise HTTPException(status_code=404, detail="Configuration not found")
44 | # Need to ensure that the conversion to dictionary (if necessary) and merging with updates respects MongoDB's document structure and idiosyncrasies
45 | updated_config = {**existing_config, **config.dict(exclude_unset=True)}
46 | # When replacing, you also need to make sure the _id is not altered. Since ObjectId is not JSON serializable, exclude it from the update.
47 | updated_config.pop('_id', None) # Remove _id if present, to avoid issues with MongoDB's _id immutability
48 | await config_collection.replace_one({"_id": ObjectId(config_id)}, updated_config)
49 | return updated_config
50 |
51 |
52 | @router.post("/test-email/")
53 | async def test_email_route():
54 | """Tests the email system connectivity and configuration.
55 |
56 | If the test fails, a 500 internal server error is returned with the error details.
57 | """
58 | try:
59 | result = await test_email_connection()
60 | return result
61 | except Exception as e:
62 | raise HTTPException(status_code=500, detail=str(e))
63 |
64 |
65 |
66 | @router.post("/send-email/")
67 | async def send_email(
68 | subject: str = Form(...),
69 | body: str = Form(...),
70 | to_emails: List[str] = Form(...),
71 | files: List[UploadFile] = File([])
72 | ):
73 | """Sends an email and saves its details to the database.
74 |
75 | The email is sent to the specified recipient(s) with optional file attachments.
76 | """
77 | return await send_email_and_save(subject, body, to_emails, files)
78 |
79 |
--------------------------------------------------------------------------------
/app/routers/users.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 |
3 | from bson import ObjectId
4 | from fastapi import APIRouter, HTTPException, Depends, Query
5 | from starlette import status
6 |
7 | from app.classes.User import UserCreate, User, UpdateUser
8 | from app.components.auth.check_permissions import check_permissions
9 | from app.components.auth.jwt_token_handler import get_jwt_username
10 | from app.components.hash_password import hash_password
11 | from app.components.logger import logger
12 | from app.db.mongoClient import async_database
13 |
14 | router = APIRouter() # router instance
15 | user_collection = async_database.users # Get the collection from the database
16 |
17 |
18 | async def user_exists(email: str = None, username: str = None) -> bool:
19 | query = {}
20 | if email:
21 | query["email"] = email
22 | if username:
23 | query["username"] = username
24 | if query:
25 | return await user_collection.find_one({"$or": [query]}) is not None
26 | return False
27 |
28 | @router.get("/users/", response_model=List[User], dependencies=[Depends(check_permissions)])
29 | async def get_users(username: str = Depends(get_jwt_username)):
30 | try:
31 | users = []
32 | async for user in user_collection.find({}):
33 | users.append(User.from_mongo(user))
34 | logger.info(f"User list requested by {username} - Success")
35 | return users
36 | except Exception as e:
37 | logger.error(f"User list requested by {username} - Failed: {str(e)}")
38 | raise HTTPException(
39 | status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
40 | detail="An error occurred while fetching users."
41 | )
42 |
43 | @router.get("/users/profile", response_model=User, dependencies=[Depends(check_permissions)])
44 | async def get_profile(current_username: str = Depends(get_jwt_username)):
45 | try:
46 | # Find the current user based on the username obtained from the JWT token
47 | user = await user_collection.find_one({"username": current_username})
48 | if not user:
49 | logger.warning(f"Profile not found for {current_username}")
50 | raise HTTPException(status_code=404, detail="Profile not found")
51 | user["_id"] = str(user["_id"])
52 | logger.info(f"Profile fetched for user {current_username} successfully.")
53 | return user
54 | except Exception as e:
55 | logger.error(f"Error fetching profile for {current_username}: {str(e)}")
56 | raise HTTPException(status_code=500, detail="An error occurred while fetching the profile.")
57 |
58 |
59 | @router.get("/users/{id}", response_model=User, dependencies=[Depends(check_permissions)])
60 | async def get_user(id: str, username: str = Depends(get_jwt_username)): # noqa
61 | try:
62 | user = await user_collection.find_one({"_id": ObjectId(id)})
63 | if not user:
64 | logger.warning(f"User with ID {id} not found by {username}")
65 | raise HTTPException(status_code=404, detail="User with ID {id} not found")
66 | user["_id"] = str(user["_id"])
67 | logger.info(f"User {username} fetched user {id} successfully.")
68 | return user
69 | except Exception as e:
70 | logger.error(f"Error fetching user by {username}: {str(e)}")
71 | raise HTTPException(status_code=500, detail="An error occurred while fetching the user.")
72 |
73 |
74 | @router.get("/check_user_exists", response_model=bool)
75 | async def check_user_exists(email: str = Query(None), username: str = Query(None)):
76 | # Validate input
77 | if not email and not username:
78 | raise HTTPException(
79 | status_code=status.HTTP_400_BAD_REQUEST,
80 | detail="Either an email or a username must be provided."
81 | )
82 |
83 | # Use the utility function to check if the user exists
84 | exists = await user_exists(email=email, username=username)
85 |
86 | return exists
87 |
88 |
89 | @router.post("/users/", response_model=User, dependencies=[Depends(check_permissions)])
90 | async def create_user(user: UserCreate, username: str = Depends(get_jwt_username)):
91 | try:
92 | # Check if the email or username already exists
93 | if await user_exists(email=user.email, username=user.username):
94 | detail_msg = "The email or username is already in use."
95 | logger.warning(f"Attempt to create a user with an existing email or username by {username}")
96 | raise HTTPException(
97 | status_code=status.HTTP_400_BAD_REQUEST,
98 | detail=detail_msg
99 | )
100 |
101 | # Hash the user's password for storage
102 | hashed_password = hash_password(user.password)
103 | user_dict = user.dict()
104 | user_dict["hashed_password"] = hashed_password
105 | del user_dict["password"]
106 |
107 | # Insert the new user into the database
108 | new_user = await user_collection.insert_one(user_dict)
109 | created_user = await user_collection.find_one({"_id": new_user.inserted_id})
110 | created_user['_id'] = str(created_user['_id'])
111 | del created_user["hashed_password"]
112 |
113 | logger.info(f"User {username} created new user {user.username} successfully.")
114 | return created_user
115 | except Exception as e:
116 | logger.error(f"Error creating user by {username}: {str(e)}")
117 | raise HTTPException(status_code=500, detail="An error occurred while creating the user.")
118 |
119 | @router.put("/users/{id}", dependencies=[Depends(check_permissions)])
120 | async def update_user(id: str, update_data: UpdateUser, username: str = Depends(get_jwt_username)): # noqa
121 | try:
122 | update_data_dict = update_data.dict(exclude_unset=True)
123 | if "password" in update_data_dict:
124 | update_data_dict["hashed_password"] = hash_password(update_data_dict.pop("password"))
125 |
126 | updated_user = await user_collection.find_one_and_update(
127 | {"_id": ObjectId(id)},
128 | {"$set": update_data_dict},
129 | return_document=True
130 | )
131 | if not updated_user:
132 | logger.warning(f"User with ID {id} not found by {username}")
133 | raise HTTPException(status_code=404, detail="User with ID {id} not found")
134 |
135 | updated_user['_id'] = str(updated_user['_id'])
136 | del updated_user["hashed_password"]
137 |
138 | logger.info(f"User {username} updated user {id} successfully.")
139 | return updated_user
140 | except Exception as e:
141 | logger.error(f"Error updating user by {username}: {str(e)}")
142 | raise HTTPException(status_code=500, detail="An error occurred while updating the user.")
143 |
144 |
145 | @router.delete("/users/{id}", dependencies=[Depends(check_permissions)])
146 | async def delete_user(id: str, username: str = Depends(get_jwt_username)): # noqa
147 | """
148 | Delete user by ID, the site owner cannot be deleted.
149 | """
150 | try:
151 | # Retrieve user data from MongoDB based on the provided ID
152 | user_data = await user_collection.find_one({"_id": ObjectId(id)})
153 |
154 | # Check if user data is found
155 | if not user_data:
156 | logger.warning(f"Attempt to delete non-existing user with ID {id} by {username}")
157 | raise HTTPException(status_code=404, detail=f"User with ID {id} not found")
158 |
159 | # Assuming user data includes a field for the user's role, such as 'role'
160 | user_role = user_data.get('role', None)
161 |
162 | # Check if the user is an owner (or has the permission to delete)
163 | if user_role == "owner":
164 | logger.warning(f"Attempt to delete user with ID {id} who is an owner by {username}")
165 | raise HTTPException(status_code=403, detail="Owners are not allowed to be deleted.")
166 |
167 | # Proceed with user deletion if the user is not an owner
168 | delete_result = await user_collection.delete_one({"_id": ObjectId(id)})
169 |
170 | if delete_result.deleted_count == 0:
171 | logger.warning(f"Attempt to delete non-existing user with ID {id} by {username}")
172 | raise HTTPException(status_code=404, detail=f"User with ID {id} not found")
173 |
174 | logger.info(f"User {username} deleted user {id} successfully.")
175 | return {"message": "User deleted successfully."}
176 |
177 | except HTTPException:
178 | raise # Re-raise HTTPException to let FastAPI handle it
179 |
180 | except Exception as e:
181 | logger.error(f"Error deleting user by {username}: {str(e)}")
182 | raise HTTPException(status_code=500, detail="An error occurred while deleting the user.")
183 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.12-slim'
2 |
3 | services:
4 | fastapi:
5 | build:
6 | context: .
7 | ports:
8 | - "8000:8000"
9 | environment:
10 | - REDIS_URL=redis://redis:6379/0
11 | - NEXT_PUBLIC_API_KEY=${static_bearer_secret_key}
12 | depends_on:
13 | - mongodb
14 | - redis
15 |
16 | nextjs:
17 | build:
18 | context: ./nextjs
19 | dockerfile: Dockerfile
20 | volumes:
21 | - ./nextjs:/app
22 | - /app/node_modules
23 | ports:
24 | - "3000:3000"
25 |
26 | mongodb:
27 | image: mongo:latest
28 | environment:
29 | - MONGO_INITDB_ROOT_USERNAME=bringthemhome
30 | - MONGO_INITDB_ROOT_PASSWORD=${mongodb_password}
31 | ports:
32 | - "27017:27017" # for testing purposes you can expose the MongoDB port
33 | volumes:
34 | - mongodb_data:/data/db
35 |
36 | redis:
37 | image: redis:latest
38 | volumes:
39 | - redis_data:/data
40 |
41 | volumes:
42 | mongodb_data:
43 | redis_data:
44 |
--------------------------------------------------------------------------------
/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # script.sh
4 |
5 | # Replace 'mongodb_server=localhost' with 'mongodb_server=mongodb' in .env
6 | sed -i 's/mongodb_server=localhost/mongodb_server=mongodb/' /app/.env
7 |
8 | # Execute the main container command.
9 | exec "$@"
10 |
--------------------------------------------------------------------------------
/generate_env.py:
--------------------------------------------------------------------------------
1 | import os
2 | import secrets
3 | import string
4 |
5 |
6 | def generate_secret_key(length=32):
7 | # Define the character pool including numbers, uppercase, and lowercase letters
8 | characters = string.ascii_letters + string.digits # This includes [a-zA-Z0-9]
9 | # Generate a random secret key from the pool
10 | key = ''.join(secrets.choice(characters) for _ in range(length))
11 | return key
12 |
13 |
14 | def load_chatgpt_credentials(file_path='chatgpt_credentials.env'):
15 | # Initialize a dictionary to hold the credentials
16 | credentials = {}
17 | try:
18 | # Open the credentials file and read the contents
19 | with open(file_path, 'r') as file:
20 | for line in file:
21 | # Split each line by the equals sign into key and value
22 | key, value = line.strip().split('=', 1)
23 | credentials[key] = value
24 | except FileNotFoundError:
25 | print(f"Warning: {file_path} not found. Skipping ChatGPT credentials.")
26 | return credentials
27 |
28 |
29 | def generate_nextjs_env_file(static_bearer_secret_key): # noqa
30 | nextjs_env_content = f"""
31 | NEXT_PUBLIC_API_BASE_URL=http://localhost:8000/api/v1
32 | NEXT_PUBLIC_API_KEY={static_bearer_secret_key}
33 | """
34 | nextjs_folder_path = "./nextjs"
35 | if not os.path.exists(nextjs_folder_path):
36 | os.makedirs(nextjs_folder_path)
37 |
38 | with open(os.path.join(nextjs_folder_path, '.env'), 'w') as file:
39 | file.write(nextjs_env_content)
40 |
41 |
42 | def main():
43 | mongodb_fastapi_password = generate_secret_key(length=16) # New password for MongoDB and FastAPI UI
44 | chatgpt_credentials = load_chatgpt_credentials()
45 | static_bearer_secret_key = generate_secret_key(length=32)
46 |
47 | # Prepare the settings with the generated static_bearer_secret_key
48 | env_content = f"""
49 | # You can set your backend settings here.
50 |
51 | # mongodb connection
52 | mongodb_server=mongodb
53 | mongodb_port=27017
54 | mongodb_username=bringthemhome
55 | mongodb_password={mongodb_fastapi_password}
56 |
57 | # fastapi
58 | fastapi_ui_username=bringthemhome
59 | fastapi_ui_password=bringthemhome
60 |
61 | # static_bearer_secret_key to protect your register from unauthorized access
62 | static_bearer_secret_key={static_bearer_secret_key}
63 | jwt_secret_key=bringthemhome
64 | algorithm=HS256
65 |
66 | # chatgpt
67 | open_ai_organization={chatgpt_credentials.get('open_ai_organization', 'this')}
68 | open_ai_secret_key={chatgpt_credentials.get('open_ai_secret_key', 'this')}
69 |
70 | # default admin user
71 | owner_username=root
72 | owner_password=bringthemhome
73 | owner_email=israel@israeli.com
74 | """.strip()
75 |
76 | # Write the combined content to an .env file
77 | with open('.env', 'w') as file:
78 | file.write(env_content)
79 |
80 | generate_nextjs_env_file(static_bearer_secret_key)
81 |
82 | print("Generated .env with keys and additional settings")
83 | print("Generated .env for Next.js")
84 |
85 |
86 | if __name__ == "__main__":
87 | main()
88 |
--------------------------------------------------------------------------------
/nextjs/.dockerignore:
--------------------------------------------------------------------------------
1 | ### NextJS template
2 | # dependencies
3 | /node_modules
4 | /.pnp
5 | .pnp.js
6 |
7 | # testing
8 | /coverage
9 |
10 | # next.js
11 | /.next/
12 | /out/
13 |
14 | # production
15 | /build
16 |
17 | # misc
18 | .DS_Store
19 | *.pem
20 |
21 | # debug
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 | .pnpm-debug.log*
26 |
27 | # vercel
28 | .vercel
29 |
30 | # typescript
31 | *.tsbuildinfo
32 | next-env.d.ts
33 |
34 |
--------------------------------------------------------------------------------
/nextjs/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/nextjs/Dockerfile:
--------------------------------------------------------------------------------
1 | # Assuming you're using Node.js
2 | FROM node:21.7.1-slim
3 |
4 | # Set the working directory in the container
5 | WORKDIR /nextjs
6 |
7 | ## Set environment variables
8 | #ENV API_BASE_URL="http://localhost:8000/api/v1"
9 | #ENV NEXT_PUBLIC_API_KEY="bringthemhome"
10 |
11 | # Copy the current directory contents into the container at /app
12 | COPY . .
13 |
14 | # Install any dependencies
15 | RUN npm install
16 |
17 | # If you're building a static site or need to run a build script
18 | RUN npm run build
19 |
20 | # Expose the port your app runs on
21 | EXPOSE 3000
22 |
23 | # Define the command to run your app
24 | CMD ["npm", "start"]
25 |
--------------------------------------------------------------------------------
/nextjs/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "paths": {
4 | "@/*": ["./src/*"]
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/nextjs/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {};
3 |
4 | export default nextConfig;
5 |
--------------------------------------------------------------------------------
/nextjs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "py-next-stack",
3 | "version": "v31.03.2024",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@emotion/react": "^11.11.4",
13 | "@emotion/styled": "^11.11.0",
14 | "@mui/icons-material": "^5.15.13",
15 | "@mui/material": "^5.15.13",
16 | "axios": "^1.6.8",
17 | "js-cookie": "^3.0.5",
18 | "next": "14.1.3",
19 | "react": "^18.2.0",
20 | "react-dom": "^18.2.0",
21 | "react-hot-toast": "^2.4.1",
22 | "react-query": "^3.39.3",
23 | "react-tsparticles": "^2.12.2",
24 | "recharts": "^2.12.3",
25 | "tsparticles": "^3.3.0",
26 | "tsparticles-slim": "^2.12.0"
27 | },
28 | "devDependencies": {
29 | "@types/node": "20.11.28",
30 | "typescript": "5.4.2"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/nextjs/src/api/auth/auth-context.js:
--------------------------------------------------------------------------------
1 | import React, {createContext, useContext, useEffect, useState} from 'react';
2 | import Cookies from 'js-cookie';
3 | import {getProfile, logout as apiLogout} from "@/api/endpoints";
4 |
5 | // Create a context for auth-related data and operations.
6 | const AuthContext = createContext();
7 |
8 | // Custom hook to use the auth context in other components.
9 | export const useAuth = () => useContext(AuthContext);
10 |
11 | // AuthProvider component that wraps the application and provides auth state.
12 | export const AuthProvider = ({children}) => {
13 | // State for the access token.
14 | const [accessToken, setAccessToken] = useState(null);
15 | // State for the user profile object.
16 | const [userProfile, setUserProfile] = useState(null);
17 | // State to track if the user is authenticated.
18 | const [isAuthenticated, setIsAuthenticated] = useState(false);
19 | // State to track if the auth-related data is still loading.
20 | const [isLoading, setIsLoading] = useState(true);
21 |
22 | // Effect that runs once on component mount.
23 | useEffect(() => {
24 | // Function to check for an existing session and set auth state.
25 | const checkForExistingSession = async () => {
26 | // Attempt to retrieve the access token from cookies.
27 | const storedToken = Cookies.get('accessToken');
28 | if (storedToken) {
29 | // If a token is found, attempt to fetch the user profile.
30 | setAccessToken(storedToken);
31 | try {
32 | const profile = await getProfile(storedToken);
33 | if (profile) {
34 | // If a profile is successfully fetched, update auth state.
35 | setUserProfile(profile);
36 | setIsAuthenticated(true);
37 | } else {
38 | // If no profile is returned, consider the user unauthenticated.
39 | setIsAuthenticated(false);
40 | Cookies.remove('accessToken');
41 | }
42 | } catch (error) {
43 | // Handle any errors during profile fetch.
44 | console.error("Error fetching user profile:", error);
45 | setIsAuthenticated(false);
46 | Cookies.remove('accessToken');
47 | }
48 | } else {
49 | // If no token is found, set the user as unauthenticated.
50 | setIsAuthenticated(false);
51 | }
52 | // Indicate that loading of auth data is complete.
53 | setIsLoading(false);
54 | };
55 |
56 | // Invoke the session check function.
57 | checkForExistingSession();
58 | }, []);
59 |
60 | // Function to handle user logout.
61 | const logout = async () => {
62 | if (accessToken) {
63 | try {
64 | // Attempt to call the API's logout endpoint.
65 | await apiLogout(accessToken);
66 | } catch (error) {
67 | // Handle any errors during logout.
68 | console.error('Error during logout:', error);
69 | }
70 | }
71 | // Clear auth state and remove the token cookie.
72 | setIsAuthenticated(false);
73 | setAccessToken(null);
74 | setUserProfile(null);
75 | Cookies.remove('accessToken');
76 | setIsLoading(false);
77 | };
78 |
79 | // Function to handle setting the access token.
80 | const value = {
81 | accessToken,
82 | setAccessToken: (token) => {
83 | // Store the token in cookies.
84 | Cookies.set('accessToken', token, {expires: 1, secure: true, sameSite: 'strict'});
85 | // Update the access token state.
86 | setAccessToken(token);
87 | setIsAuthenticated(true); // Assume authentication is successful.
88 | // Attempt to fetch the user profile with the new token.
89 | getProfile(token).then(profile => {
90 | setUserProfile(profile);
91 | setIsAuthenticated(true); // Confirm authentication.
92 | }).catch(error => {
93 | // Handle any errors during profile fetch.
94 | console.error("Error setting access token and fetching profile:", error);
95 | setIsAuthenticated(false);
96 | setUserProfile(null); // Clear user profile on error.
97 | });
98 | },
99 | isAuthenticated,
100 | setIsAuthenticated,
101 | userProfile,
102 | isLoading,
103 | logout,
104 | };
105 |
106 | // Provide the auth context to child components.
107 | return {children};
108 | };
109 |
--------------------------------------------------------------------------------
/nextjs/src/api/endpoints.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import {jsonHeader, staticBearerHeader} from "@/api/headers";
3 |
4 | const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL;
5 |
6 | export const apiClient = axios.create({
7 | baseURL: API_BASE_URL,
8 | });
9 |
10 | // Function to log-in and get token
11 | export const getToken = async (username, password) => {
12 | const response = await axios.post(`${API_BASE_URL}/token?username=${username}&password=${password}`, null, {
13 | headers: jsonHeader
14 | });
15 | return response.data;
16 | };
17 |
18 | // Function to get user profile
19 | export const getProfile = async (accessToken) => {
20 | const response = await axios.get(`${API_BASE_URL}/users/profile`, {
21 | headers: jsonHeader(accessToken),
22 | });
23 | return response.data;
24 | };
25 |
26 | // log out function
27 | export const logout = async (accessToken) => {
28 | await axios.post(`${API_BASE_URL}/logout`, {}, {headers: jsonHeader(accessToken)});
29 |
30 | };
31 |
32 |
33 | // Users related functions, get, post, delete, put
34 | export const fetchUsers = async (accessToken) => {
35 | const {data} = await axios.get(`${API_BASE_URL}/users/`, {
36 | headers: jsonHeader(accessToken)
37 | });
38 | return data;
39 | };
40 |
41 | export const updateUser = async (user, accessToken) => {
42 | // noinspection JSUnresolvedReference
43 | const {data} = await axios.put(`${API_BASE_URL}/users/${user._id}`, user, {
44 | headers: jsonHeader(accessToken)
45 | });
46 | return data;
47 | };
48 |
49 | export const deleteUser = async (id, accessToken) => {
50 | await axios.delete(`${API_BASE_URL}/users/${id}`, {
51 | headers: jsonHeader(accessToken)
52 | });
53 | return id;
54 | };
55 |
56 | export const createUser = async (user, accessToken) => {
57 | console.log(user)
58 | const {data} = await axios.post(`${API_BASE_URL}/users/`, user, {
59 | headers: jsonHeader(accessToken)
60 | });
61 | return data;
62 | };
63 |
64 |
65 | // ChatGPT
66 | export const fetchChatResponse = async (question, accessToken, model) => {
67 | // Simulated API call
68 | console.log("Sending question to the API:", question, "with model", model);
69 |
70 | const url = new URL(`${API_BASE_URL}/chat/`);
71 | url.searchParams.append('question', question);
72 | url.searchParams.append('model', model);
73 |
74 | const response = await fetch(url, {
75 | method: 'GET',
76 | headers: {
77 | 'accept': 'application/json',
78 | 'api-key': accessToken,
79 | },
80 | });
81 |
82 | if (!response.ok) {
83 | throw new Error('Network response was not ok');
84 | }
85 | return response.json();
86 | };
87 |
88 |
89 | export const forgotPassword = async (email) => {
90 | await axios.post(`${API_BASE_URL}/users/forgot-password/`, {}, {
91 | params: {email},
92 | headers: staticBearerHeader,
93 | });
94 | };
95 |
96 |
97 | export const postResetPassword = async ({token, newPassword}) => {
98 | const response = await axios.post(`${API_BASE_URL}/users/reset-password/`, {}, {
99 | params: {token, new_password: newPassword},
100 | headers: staticBearerHeader,
101 | });
102 | return response.data;
103 | };
104 |
105 |
106 | export const postRegister = async (userData) => {
107 | const response = await axios.post(`${API_BASE_URL}/register/`, {
108 | email: userData.email,
109 | full_name: userData.fullName,
110 | password: userData.password,
111 | username: userData.username,
112 | }, {
113 | headers: staticBearerHeader,
114 | });
115 |
116 | return response.data;
117 | };
118 |
119 | // mail settings
120 | export const fetchMessageSettings = async (accessToken) => {
121 | // Pass the accessToken directly to use it in the request
122 | const {data} = await apiClient.get('config', {
123 | headers: jsonHeader(accessToken)
124 | });
125 | return data;
126 | };
127 |
128 | export const updateMessageSettings = async ({section, settings, accessToken}) => {
129 | // Include accessToken in the request
130 | await apiClient.put(`config`, {[section]: settings}, {
131 | headers: jsonHeader(accessToken)
132 | });
133 | return settings;
134 | };
--------------------------------------------------------------------------------
/nextjs/src/api/headers.js:
--------------------------------------------------------------------------------
1 | export const jsonHeader = (token) => ({
2 | 'Accept': 'application/json',
3 | ...(token ? {'api-key': token} : {}),
4 | });
5 |
6 | export const staticBearerHeader = {
7 | 'accept': 'application/json',
8 | 'Authorization': `Bearer ${process.env.NEXT_PUBLIC_API_KEY}`,
9 | };
--------------------------------------------------------------------------------
/nextjs/src/components/confirmation-modal.js:
--------------------------------------------------------------------------------
1 | import React, {useState} from 'react';
2 | import Modal from '@mui/material/Modal';
3 | import Button from '@mui/material/Button';
4 | import Box from '@mui/material/Box';
5 | import Typography from '@mui/material/Typography';
6 |
7 | const keyframes = `
8 | @keyframes gentleZoomIn {
9 | from { transform: translate(-50%, -50%) scale(0.5); }
10 | to { transform: translate(-50%, -50%) scale(1); }
11 | }
12 |
13 | @keyframes gentleZoomOut {
14 | from { transform: translate(-50%, -50%) scale(1); }
15 | to { transform: translate(-50%, -50%) scale(0.5); opacity: 0; }
16 | }
17 | `;
18 |
19 | const modalStyle = (animation) => ({
20 | position: 'absolute',
21 | top: '30%',
22 | left: '50%',
23 | transform: 'translate(-50%, -50%)',
24 | width: 340,
25 | bgcolor: 'background.paper',
26 | borderRadius: '15px',
27 | boxShadow: 24,
28 | p: 4,
29 | transformOrigin: 'center',
30 | animation: animation,
31 | });
32 |
33 | const ConfirmModal = ({
34 | onConfirm,
35 | confirmButtonName,
36 | buttonName,
37 | buttonSize = 'auto',
38 | buttonVariant = 'contained',
39 | confirmationText,
40 | title,
41 | margin,
42 | color,
43 | disabledButton = false,
44 | icon = null, // New prop for the icon
45 | iconPosition = 'start' // New prop for the icon position
46 | }) => {
47 | const [isOpen, setIsOpen] = useState(false);
48 | const [animation, setAnimation] = useState('');
49 |
50 | const handleClose = () => {
51 | // Start the zoom-out animation
52 | setAnimation('gentleZoomOut 0.3s ease-out forwards');
53 | // Close the modal after the animation finishes
54 | setTimeout(() => {
55 | setIsOpen(false);
56 | // Reset animation to none
57 | setAnimation('');
58 | }, 500); // Duration of zoom-out animation
59 | };
60 |
61 | const handleOpen = () => {
62 | setIsOpen(true);
63 | // Apply the zoom-in effect when opening
64 | setAnimation('gentleZoomIn 0.3s ease-out forwards');
65 | };
66 |
67 | const buttonProps = {
68 | ...(iconPosition === 'start' && {startIcon: icon}),
69 | ...(iconPosition === 'end' && {endIcon: icon}),
70 | };
71 |
72 | return (
73 |
74 |
75 |
84 |
85 |
87 |
88 |
91 | {title}
92 |
93 |
96 | {confirmationText}
97 |
98 |
101 |
109 |
117 |
118 |
119 |
120 |
121 | );
122 | };
123 |
124 | export default ConfirmModal;
125 |
--------------------------------------------------------------------------------
/nextjs/src/components/error.js:
--------------------------------------------------------------------------------
1 | // components/ErrorDisplay.js
2 | import React from 'react';
3 | import {Alert, AlertTitle, Box, Container, Typography} from '@mui/material';
4 |
5 | const ErrorDisplay = ({message}) => {
6 | return (
7 |
8 |
9 | {/**/}
10 |
11 | Error
12 | {message}
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default ErrorDisplay;
20 |
--------------------------------------------------------------------------------
/nextjs/src/components/loading.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CircularProgress from '@mui/material/CircularProgress';
3 | import Box from '@mui/material/Box';
4 |
5 | const Loading = () => {
6 | return (
7 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default Loading;
21 |
--------------------------------------------------------------------------------
/nextjs/src/components/scroll-top-top.js:
--------------------------------------------------------------------------------
1 | import React, {useEffect} from 'react';
2 | import IconButton from '@mui/material/IconButton';
3 | import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
4 | import useScrollTrigger from '@mui/material/useScrollTrigger';
5 | import Zoom from '@mui/material/Zoom';
6 |
7 | const ScrollToTop = ({children}) => {
8 | // Function to check if the page is scrolled down
9 | // The threshold value has been set to 10 to make the button appear earlier than only at the bottom
10 | const trigger = useScrollTrigger({
11 | disableHysteresis: true,
12 | threshold: 300, // This value determines when the button becomes visible as you scroll down
13 | });
14 |
15 | // Function to handle the scroll to top action
16 | const handleClick = () => {
17 | window.scrollTo({
18 | top: 0,
19 | behavior: 'smooth',
20 | });
21 | };
22 |
23 | // Listen for scroll events to potentially show the button
24 | useEffect(() => {
25 | const handleScroll = () => {
26 | // This logic is handled by `useScrollTrigger`, so no additional logic is added here
27 | // Keeping this might hint at potential future use or modifications
28 | };
29 |
30 | window.addEventListener('scroll', handleScroll);
31 |
32 | return () => {
33 | window.removeEventListener('scroll', handleScroll);
34 | };
35 | }, []);
36 |
37 | return (
38 | <>
39 | {children}
40 |
41 |