yamilogic commited on
Commit
c425319
Β·
1 Parent(s): f54f330

Deploy full Auth-System-SMTP frontend and backend together in single space

Browse files
.dockerignore CHANGED
@@ -1,4 +1,24 @@
1
- __pycache__
2
- *.pyc
3
- *.db
4
  .git
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Exclude git repository details
 
 
2
  .git
3
+ .gitignore
4
+
5
+ # Exclude package metadata and docs
6
+ README.md
7
+ DOCKER_GUIDE.md
8
+
9
+ # Exclude local node modules and build output
10
+ **/node_modules
11
+ **/dist
12
+ frontend/dist
13
+
14
+ # Exclude local python venv and compiled cache files
15
+ **/.venv
16
+ **/.venv/
17
+ **/__pycache__
18
+ **/*.pyc
19
+ **/*.pyo
20
+ **/*.pyd
21
+
22
+ # Exclude databases and debug logs
23
+ **/users.db
24
+ **/requests.log
.gitattributes DELETED
@@ -1,35 +0,0 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Virtual environment
2
+ .venv/
3
+ venv/
4
+ ENV/
5
+
6
+ # Python cache files
7
+ __pycache__/
8
+ *.py[cod]
9
+ *$py.class
10
+
11
+ # Databases and Local logs
12
+ users.db
13
+ requests.log
14
+
15
+ # Node dependency modules
16
+ node_modules/
17
+ npm-debug.log*
18
+ yarn-debug.log*
19
+ yarn-error.log*
20
+
21
+ # Production build output
22
+ frontend/dist/
23
+ dist/
24
+
25
+ # OS files
26
+ .DS_Store
27
+ Thumbs.db
28
+
29
+ # Environment secret tokens
30
+ .env
31
+ .env*.local
DOCKER_GUIDE.md ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Docker Run Guide 🐳
2
+
3
+ Here is a quick, point-wise guide on how the SMTP-enabled Docker container stack works and how the components connect together.
4
+
5
+ ## 1. Access Links (Click to Open)
6
+ * πŸ–₯️ **React Frontend**: [http://localhost:5173](http://localhost:5173)
7
+ * βš™οΈ **FastAPI Backend (Swagger Docs)**: [http://localhost:8000/docs](http://localhost:8000/docs)
8
+ * 🏠 **Backend API Status**: [http://localhost:8000](http://localhost:8000)
9
+
10
+ ---
11
+
12
+ ## 2. How the Components Connect
13
+ * **Backend (`./backend`)**:
14
+ * Runs on port `8000`.
15
+ * Automatically loads your SMTP email settings (email address and Google App Password token) from the `./backend/.env` file.
16
+ * Captures registration details and emails a 6-digit OTP code to the registered email address.
17
+ * **Frontend (`./frontend`)**:
18
+ * Runs on port `5173`.
19
+ * Redirects users to the `/verify-email` screen after registration to enter their OTP.
20
+ * Connects directly to the backend at `http://localhost:8000` to process validations.
21
+ * **Docker Network & Persistence**:
22
+ * Docker Compose runs both components in a single network.
23
+ * The SQLite database is saved on your computer at `./backend/users.db` so accounts are kept safe.
24
+
25
+ ---
26
+
27
+ ## 3. How to Start and Stop
28
+ To run the entire project, execute this command in your terminal:
29
+ ```bash
30
+ docker compose up --build
31
+ ```
32
+
33
+ To stop the project, press `CTRL + C` or run:
34
+ ```bash
35
+ docker compose down
36
+ ```
Dockerfile CHANGED
@@ -1,8 +1,27 @@
1
- FROM python:3.10-slim
 
 
 
 
 
 
 
 
 
2
  WORKDIR /app
3
- COPY requirements.txt .
 
 
4
  RUN pip install --no-cache-dir -r requirements.txt
5
- COPY . .
6
- RUN mkdir -p /app/data
 
 
 
 
 
 
7
  EXPOSE 7860
8
- CMD sh -c "uvicorn main:app --host 0.0.0.0 --port ${PORT:-7860}"
 
 
 
1
+ # Stage 1: Build the React frontend
2
+ FROM node:18-slim AS frontend-builder
3
+ WORKDIR /frontend
4
+ COPY frontend/package.json ./
5
+ RUN npm install
6
+ COPY frontend/ ./
7
+ RUN npm run build
8
+
9
+ # Stage 2: Serve via FastAPI backend
10
+ FROM python:3.11-slim
11
  WORKDIR /app
12
+
13
+ # Install backend dependencies
14
+ COPY backend/requirements.txt .
15
  RUN pip install --no-cache-dir -r requirements.txt
16
+
17
+ # Copy backend source code
18
+ COPY backend/ .
19
+
20
+ # Copy static frontend bundle from Stage 1 into the "static" folder
21
+ COPY --from=frontend-builder /frontend/dist ./static
22
+
23
+ # Expose port 7860 (Hugging Face Spaces default port)
24
  EXPOSE 7860
25
+
26
+ # Run uvicorn on port 7860
27
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,12 +1,172 @@
1
  ---
2
- title: Auth Backend
3
- emoji: πŸ”
4
- colorFrom: blue
5
- colorTo: purple
6
  sdk: docker
7
  app_port: 7860
 
8
  ---
9
 
10
- # Auth Backend API
11
 
12
- FastAPI backend with JWT authentication and OTP email verification.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Secure Auth System
3
+ emoji: πŸ›‘οΈ
4
+ colorFrom: gray
5
+ colorTo: gray
6
  sdk: docker
7
  app_port: 7860
8
+ pinned: false
9
  ---
10
 
11
+ # Secure Authentication System with SMTP OTP Verification
12
 
13
+ A professional, modular authentication system built with **FastAPI** (Python) and **React** (Vite + JavaScript). This version implements secure signup and recovery flows using **Email OTP (One-Time Password) Verification** via Python's built-in SMTP protocols.
14
+
15
+ ---
16
+
17
+ ## πŸš€ Key Features
18
+
19
+ * **SMTP Email Verification**: Creates inactive users on signup, sends a 6-digit OTP to their email, and locks login attempts until the account is verified.
20
+ * **Console Fallback Mode**: If SMTP parameters are not set in `.env`, the server outputs verification codes to the console so you can test the system completely offline!
21
+ * **Secure Forgot Password Recovery**: A 2-step password recovery flow sending OTP codes to users before allowing a credential reset.
22
+ * **Modular Backend Architecture**: Clean separation of models, routes, database, schemas, configs, and helpers.
23
+ * **JWT Access & Refresh Token System**: Implements short-lived access tokens (30 minutes) and long-lived refresh tokens (7 days).
24
+ * **Automatic Token Rotation**: Refreshing your session issues a new access/refresh pair and revokes the old refresh token.
25
+ * **Server-Side Token Revocation (Logout)**: Invalides active tokens immediately on logout by storing them in a SQLite database blacklist.
26
+ * **Bcrypt Password Hashing**: Clean hashing using Python's direct `bcrypt` package to prevent dictionary attacks.
27
+ * **Request Logger & Rate Limiter**: Logs API logs to `requests.log` and restricts traffic per IP address in-memory.
28
+ * **Premium Glassmorphic UI**: Beautiful dark-mode dashboard styled with vanilla CSS and subtle micro-animations.
29
+
30
+ ---
31
+
32
+ ## πŸ› οΈ Technology Stack
33
+
34
+ * **Backend**: FastAPI, SQLite (Database), SQLAlchemy (ORM), Pydantic (Data Validation), PyJWT/Jose (JWT tokens), Bcrypt, built-in Smtplib.
35
+ * **Frontend**: React, React Router Dom (Routing), Axios (Interceptors for token injection & auto-refresh).
36
+
37
+ ---
38
+
39
+ ## πŸ“ Project Structure
40
+
41
+ ```
42
+ β”œβ”€β”€ backend/
43
+ β”‚ β”œβ”€β”€ config.py # Loads .env configurations
44
+ β”‚ β”œβ”€β”€ database.py # SQLAlchemy engine & SQLite session setup
45
+ β”‚ β”œβ”€β”€ models.py # User and RevokedToken database models
46
+ β”‚ β”œβ”€β”€ schemas.py # Pydantic input/output validation models
47
+ β”‚ β”œβ”€β”€ auth.py # Password hashing, JWT dependencies & SMTP mailers
48
+ β”‚ β”œβ”€β”€ routes/ # Authentication & user routing endpoints
49
+ β”‚ β”œβ”€β”€ .env # Secret configurations & SMTP details
50
+ β”‚ β”œβ”€β”€ requirements.txt # Python requirements
51
+ β”‚ └── main.py # FastAPI entry point, logger, & rate-limiting
52
+ └── frontend/
53
+ β”œβ”€β”€ package.json # Frontend dependencies
54
+ β”œβ”€β”€ vite.config.js # Vite dev server configuration
55
+ β”œβ”€β”€ index.html # HTML template
56
+ └── src/
57
+ β”œβ”€β”€ main.jsx # Renders React root
58
+ β”œβ”€β”€ App.jsx # App router & AuthProvider integration
59
+ β”œβ”€β”€ index.css # Gorgeous dark-glass UI styling
60
+ β”œβ”€β”€ api/axios.js # Axios client with interceptors
61
+ β”œβ”€β”€ components/ # Reusable components (Navbar, Guards)
62
+ β”œβ”€β”€ context/ # Auth Context provider managing global states
63
+ └── pages/ # Login, Register, VerifyEmail, and Dashboard views
64
+ ```
65
+
66
+ ---
67
+
68
+ ## βš™οΈ Setup and Installation
69
+
70
+ ### Option 1: Running with Docker Compose (Recommended)
71
+ You can start the entire stack (React + FastAPI + SQLite) with a single command. Docker Compose will automatically read your SMTP settings from `./backend/.env`.
72
+
73
+ In the root project folder, run:
74
+ ```bash
75
+ docker compose up --build
76
+ ```
77
+ * **React Frontend URL**: [http://localhost:5173](http://localhost:5173)
78
+ * **FastAPI Docs URL**: [http://localhost:8000/docs](http://localhost:8000/docs)
79
+
80
+ To stop the containers:
81
+ ```bash
82
+ docker compose down
83
+ ```
84
+
85
+ ---
86
+
87
+ ### Option 2: Running Locally
88
+
89
+ #### 1. Backend Setup
90
+ 1. Navigate to the backend directory:
91
+ ```bash
92
+ cd backend
93
+ ```
94
+ 2. Create and activate a Python virtual environment:
95
+ ```bash
96
+ python3 -m venv .venv
97
+ source .venv/bin/activate
98
+ ```
99
+ 3. Install dependencies:
100
+ ```bash
101
+ pip install -r requirements.txt
102
+ ```
103
+ 4. Copy/create a `.env` file and fill in your SMTP credentials (or leave them blank to use console simulation logs):
104
+ ```bash
105
+ uvicorn main:app --reload
106
+ ```
107
+
108
+ #### 2. Frontend Setup
109
+ 1. In a new terminal, navigate to the frontend directory:
110
+ ```bash
111
+ cd frontend
112
+ ```
113
+ 2. Install npm packages:
114
+ ```bash
115
+ npm install
116
+ ```
117
+ 3. Run the development server:
118
+ ```bash
119
+ npm run dev
120
+ ```
121
+
122
+ ---
123
+
124
+ ## πŸ“§ Configuring SMTP Server for Real Emails
125
+
126
+ To make the app send actual emails to your users, edit the `backend/.env` file:
127
+ ```env
128
+ SMTP_SERVER=smtp.gmail.com
129
+ SMTP_PORT=587
130
+ SMTP_USER=your_email@gmail.com
131
+ SMTP_PASSWORD=your_google_app_password
132
+ ```
133
+ *Note: For security reasons, do not use your primary password. Instead, generate a secure 16-character **App Password** from your Google Account settings.*
134
+
135
+ ---
136
+
137
+ ## πŸ€— Deploying to Hugging Face Spaces
138
+
139
+ You can easily deploy this full-stack application to Hugging Face Spaces using the **Docker** SDK! Since we set up a multi-stage Dockerfile, Hugging Face will compile your React files and run your Python API together on a single port automatically.
140
+
141
+ ### Step-by-Step Deployment Guide:
142
+ 1. **Create a Hugging Face Account**: Sign up at [huggingface.co](https://huggingface.co/).
143
+ 2. **Create a New Space**:
144
+ * Click your profile icon -> **New Space**.
145
+ * Choose a **Space name** (e.g. `secure-auth-smtp`).
146
+ * Select **Docker** as the SDK.
147
+ * Choose the **Blank** template.
148
+ * Click **Create Space**.
149
+ 3. **Configure Secret Variables**:
150
+ * Go to the **Settings** tab of your new Space.
151
+ * Scroll down to **Variables and secrets**.
152
+ * Add a new secret for each of the following:
153
+ * `SECRET_KEY`: (Any secure random text)
154
+ * `DATABASE_URL`: `sqlite:////app/users.db`
155
+ * `SMTP_SERVER`: `smtp.gmail.com`
156
+ * `SMTP_PORT`: `587`
157
+ * `SMTP_USER`: `your_email@gmail.com`
158
+ * `SMTP_PASSWORD`: `your_google_app_password_token`
159
+ 4. **Push your Repository**:
160
+ * Clone the Hugging Face space using git, copy all the files from this folder into it, and push:
161
+ ```bash
162
+ git add .
163
+ git commit -m "Initial commit"
164
+ git push
165
+ ```
166
+ 5. **Ready!**: The space will build automatically and show **Running** once deployed.
167
+
168
+ ---
169
+
170
+ ## πŸ›‘οΈ License
171
+
172
+ Distributed under the MIT License. See `LICENSE` for more information.
backend/Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ EXPOSE 8000
11
+
12
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
backend/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Marks this folder as a Python package
backend/auth.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import bcrypt
2
+ import smtplib
3
+ import os
4
+ from datetime import datetime, timedelta
5
+ from email.mime.text import MIMEText
6
+ from fastapi import Depends, HTTPException, status
7
+ from fastapi.security import OAuth2PasswordBearer
8
+ from jose import jwt, JWTError
9
+ from sqlalchemy.orm import Session
10
+
11
+ from config import SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES, REFRESH_TOKEN_EXPIRE_DAYS, SMTP_SERVER, SMTP_PORT, SMTP_USER, SMTP_PASSWORD
12
+ from database import get_db
13
+ from models import User, RevokedToken
14
+
15
+ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
16
+
17
+ # ─── PASSWORD HASHING ───
18
+
19
+ def hash_password(password: str) -> str:
20
+ salt = bcrypt.gensalt()
21
+ return bcrypt.hashpw(password.encode('utf-8'), salt).decode('utf-8')
22
+
23
+ def check_password(password: str, hashed_password: str) -> bool:
24
+ try:
25
+ return bcrypt.checkpw(password.encode('utf-8'), hashed_password.encode('utf-8'))
26
+ except Exception:
27
+ return False
28
+
29
+ # ─── JWT TOKENS ───
30
+
31
+ def make_access_token(user_id: int, email: str) -> str:
32
+ expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
33
+ data = {"sub": str(user_id), "email": email, "type": "access", "exp": expire}
34
+ return jwt.encode(data, SECRET_KEY, algorithm=ALGORITHM)
35
+
36
+ def make_refresh_token(user_id: int) -> str:
37
+ expire = datetime.utcnow() + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
38
+ data = {"sub": str(user_id), "type": "refresh", "exp": expire}
39
+ return jwt.encode(data, SECRET_KEY, algorithm=ALGORITHM)
40
+
41
+ def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)) -> User:
42
+ credentials_exception = HTTPException(
43
+ status_code=status.HTTP_401_UNAUTHORIZED,
44
+ detail="Could not validate credentials",
45
+ headers={"WWW-Authenticate": "Bearer"},
46
+ )
47
+
48
+ revoked = db.query(RevokedToken).filter(RevokedToken.token == token).first()
49
+ if revoked:
50
+ raise HTTPException(
51
+ status_code=status.HTTP_401_UNAUTHORIZED,
52
+ detail="Session has expired or you logged out. Please sign in again.",
53
+ headers={"WWW-Authenticate": "Bearer"},
54
+ )
55
+
56
+ try:
57
+ payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
58
+ if payload.get("type") != "access":
59
+ raise HTTPException(status_code=401, detail="Invalid token type")
60
+ user_id: str = payload.get("sub")
61
+ if user_id is None:
62
+ raise credentials_exception
63
+ except JWTError:
64
+ raise credentials_exception
65
+
66
+ user = db.query(User).filter(User.id == int(user_id)).first()
67
+ if user is None:
68
+ raise HTTPException(status_code=404, detail="User not found")
69
+ if not user.is_active:
70
+ raise HTTPException(status_code=400, detail="User account is inactive")
71
+ if not user.is_verified:
72
+ raise HTTPException(status_code=400, detail="User email is not verified yet")
73
+
74
+ return user
75
+
76
+ # ─── SMTP / CONSOLE FALLBACK EMAIL ROUTINE ───
77
+
78
+ def send_email(to_email: str, subject: str, body: str) -> bool:
79
+ """
80
+ Sends an email using built-in smtplib.
81
+ If SMTP variables are not set in the .env, logs a block in the console so it can still be tested.
82
+ """
83
+ # Check if credentials are set
84
+ if not all([SMTP_SERVER, SMTP_USER, SMTP_PASSWORD]):
85
+ print(f"\nβ”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”")
86
+ print(f"β”‚ [SIMULATION] Email Sent To: {to_email:<27} β”‚")
87
+ print(f"β”‚ Subject: {subject:<44} β”‚")
88
+ print(f"β”‚ Code/OTP: {body:<44} β”‚")
89
+ print(f"β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜\n")
90
+ return True
91
+
92
+ try:
93
+ msg = MIMEText(body)
94
+ msg["Subject"] = subject
95
+ msg["From"] = SMTP_USER
96
+ msg["To"] = to_email
97
+
98
+ # Start standard TLS connection on port 587 with a 5-second timeout
99
+ with smtplib.SMTP(SMTP_SERVER, SMTP_PORT, timeout=5) as server:
100
+ server.starttls()
101
+ server.login(SMTP_USER, SMTP_PASSWORD)
102
+ server.send_message(msg)
103
+ return True
104
+ except Exception as e:
105
+ # Print error details and output code to console so server doesn't crash
106
+ print(f"\n[SMTP ERROR] Sending failed: {e}")
107
+ print(f"β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”")
108
+ print(f"β”‚ [SIMULATION FALLBACK] Email To: {to_email:<22} β”‚")
109
+ print(f"β”‚ Subject: {subject:<44} β”‚")
110
+ print(f"β”‚ Code/OTP: {body:<44} β”‚")
111
+ print(f"β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜\n")
112
+ return False
backend/config.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+
4
+ load_dotenv()
5
+
6
+ # JWT Token Settings
7
+ SECRET_KEY = os.getenv("SECRET_KEY", "super_secret_dev_key_change_me_123456789")
8
+ ALGORITHM = os.getenv("ALGORITHM", "HS256")
9
+ ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "30"))
10
+ REFRESH_TOKEN_EXPIRE_DAYS = int(os.getenv("REFRESH_TOKEN_EXPIRE_DAYS", "7"))
11
+
12
+ # Database Settings
13
+ DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./users.db")
14
+
15
+ # SMTP Mail Server Settings (Loads credentials to send real OTP verification emails)
16
+ SMTP_SERVER = os.getenv("SMTP_SERVER", "")
17
+ SMTP_PORT = int(os.getenv("SMTP_PORT", "587"))
18
+ SMTP_USER = os.getenv("SMTP_USER", "")
19
+ SMTP_PASSWORD = os.getenv("SMTP_PASSWORD", "")
backend/database.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import create_engine
2
+ from sqlalchemy.orm import sessionmaker, DeclarativeBase
3
+ from config import DATABASE_URL
4
+
5
+ connect_args = {"check_same_thread": False} if DATABASE_URL.startswith("sqlite") else {}
6
+
7
+ engine = create_engine(DATABASE_URL, connect_args=connect_args)
8
+ SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
9
+
10
+ class Base(DeclarativeBase):
11
+ pass
12
+
13
+ def get_db():
14
+ db = SessionLocal()
15
+ try:
16
+ yield db
17
+ finally:
18
+ db.close()
backend/main.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ import os
3
+ from datetime import datetime
4
+ from fastapi import FastAPI, Request
5
+ from fastapi.middleware.cors import CORSMiddleware
6
+ from fastapi.responses import JSONResponse, HTMLResponse
7
+ from fastapi.staticfiles import StaticFiles
8
+
9
+ from database import engine, Base
10
+ from routes import auth_routes, user_routes
11
+
12
+ Base.metadata.create_all(bind=engine)
13
+
14
+ app = FastAPI(
15
+ title="SMTP-Enabled Authentication System",
16
+ description="A beginner-friendly secure authentication system with modular routes and email verification.",
17
+ version="1.0.0"
18
+ )
19
+
20
+ app.add_middleware(
21
+ CORSMiddleware,
22
+ allow_origins=["http://localhost:5173", "http://localhost:3000"],
23
+ allow_credentials=True,
24
+ allow_methods=["*"],
25
+ allow_headers=["*"],
26
+ )
27
+
28
+ # ─── SIMPLE RATE LIMITING & REQUEST LOGGING ───
29
+
30
+ RATE_LIMIT_WINDOW = 60
31
+ RATE_LIMIT_MAX = 60
32
+ client_requests = {}
33
+
34
+ @app.middleware("http")
35
+ async def rate_limiting_and_logging_middleware(request: Request, call_next):
36
+ client_ip = request.client.host if request.client else "unknown"
37
+ now = time.time()
38
+
39
+ if client_ip not in client_requests:
40
+ client_requests[client_ip] = []
41
+
42
+ client_requests[client_ip] = [t for t in client_requests[client_ip] if now - t < RATE_LIMIT_WINDOW]
43
+
44
+ if len(client_requests[client_ip]) >= RATE_LIMIT_MAX:
45
+ return JSONResponse(
46
+ status_code=429,
47
+ content={"detail": "Too many requests. Please slow down and try again in a minute."}
48
+ )
49
+
50
+ client_requests[client_ip].append(now)
51
+ start_time = time.time()
52
+
53
+ try:
54
+ response = await call_next(request)
55
+ except Exception as exc:
56
+ log_line = f"{datetime.utcnow().isoformat()} - {client_ip} - {request.method} {request.url.path} - ERROR: {str(exc)}\n"
57
+ with open("requests.log", "a") as log_file:
58
+ log_file.write(log_line)
59
+
60
+ return JSONResponse(
61
+ status_code=500,
62
+ content={"detail": "An internal server error occurred. Please contact the administrator."}
63
+ )
64
+
65
+ duration = time.time() - start_time
66
+
67
+ log_line = (
68
+ f"{datetime.utcnow().isoformat()} - IP: {client_ip} - "
69
+ f"{request.method} {request.url.path} - Status: {response.status_code} - "
70
+ f"Took: {duration:.4f}s\n"
71
+ )
72
+
73
+ with open("requests.log", "a") as log_file:
74
+ log_file.write(log_line)
75
+
76
+ return response
77
+
78
+ app.include_router(auth_routes.router)
79
+ app.include_router(user_routes.router)
80
+
81
+ @app.get("/api")
82
+ def home():
83
+ return {
84
+ "status": "online",
85
+ "message": "Welcome to the SMTP Auth API! Go to /docs to test all endpoints."
86
+ }
87
+
88
+ # ─── SERVE FRONTEND STATIC FILES (HF Spaces & Production) ───
89
+ if os.path.exists("static"):
90
+ app.mount("/", StaticFiles(directory="static", html=True), name="static")
91
+
92
+ # ─── SPA FALLBACK HANDLER (Serves React Router paths on direct visits/refreshes) ───
93
+ @app.exception_handler(404)
94
+ async def spa_fallback_404_handler(request: Request, exc: Exception):
95
+ # If the request is not an API call, serve the frontend index.html
96
+ path = request.url.path
97
+ if not path.startswith("/auth") and not path.startswith("/admin") and path != "/api":
98
+ if os.path.exists("static/index.html"):
99
+ with open("static/index.html", "r") as f:
100
+ return HTMLResponse(content=f.read(), status_code=200)
101
+ return JSONResponse(status_code=404, content={"detail": "Not Found"})
102
+
103
+
backend/models.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import Column, Integer, String, Boolean, DateTime
2
+ from datetime import datetime
3
+ from database import Base
4
+
5
+ class User(Base):
6
+ __tablename__ = "users"
7
+
8
+ id = Column(Integer, primary_key=True, index=True)
9
+ username = Column(String, unique=True, index=True, nullable=False)
10
+ email = Column(String, unique=True, index=True, nullable=False)
11
+ password = Column(String, nullable=False)
12
+ is_active = Column(Boolean, default=True)
13
+ role = Column(String, default="user")
14
+ created_at = Column(DateTime, default=datetime.utcnow)
15
+
16
+ # ─── Verification Columns for SMTP Flow ───
17
+ is_verified = Column(Boolean, default=False) # must be True before login is allowed
18
+ verification_code = Column(String, nullable=True) # stores the active registration OTP code
19
+ reset_code = Column(String, nullable=True) # stores the active forgot password OTP code
20
+
21
+ class RevokedToken(Base):
22
+ __tablename__ = "revoked_tokens"
23
+
24
+ id = Column(Integer, primary_key=True, index=True)
25
+ token = Column(String, unique=True, index=True, nullable=False)
26
+ revoked_at = Column(DateTime, default=datetime.utcnow)
27
+ expires_at = Column(DateTime, nullable=False)
backend/requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ fastapi>=0.110.0
2
+ uvicorn[standard]>=0.28.0
3
+ sqlalchemy>=2.0.0
4
+ bcrypt>=4.1.0
5
+ python-jose[cryptography]>=3.3.0
6
+ pydantic[email]>=2.6.0
7
+ python-dotenv>=1.0.0
8
+ python-multipart>=0.0.9
backend/routes/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Marks this folder as a Python package
backend/routes/auth_routes.py ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+ from datetime import datetime
3
+ from fastapi import APIRouter, Depends, HTTPException, status
4
+ from fastapi.security import OAuth2PasswordRequestForm
5
+ from jose import jwt, JWTError
6
+ from sqlalchemy.orm import Session
7
+
8
+ from config import SECRET_KEY, ALGORITHM
9
+ from database import get_db
10
+ from models import User, RevokedToken
11
+ from schemas import RegisterInput, TokenOutput, RefreshInput, UserOutput, VerifyEmailInput, ForgotPasswordRequest, PasswordResetInput
12
+ from auth import hash_password, check_password, make_access_token, make_refresh_token, oauth2_scheme, get_current_user, send_email
13
+
14
+ router = APIRouter(prefix="/auth", tags=["Authentication"])
15
+
16
+ def validate_password_strength(password: str):
17
+ if len(password) < 8:
18
+ raise HTTPException(status_code=400, detail="Password must be at least 8 characters long.")
19
+ if not any(char.isdigit() for char in password):
20
+ raise HTTPException(status_code=400, detail="Password must contain at least one number.")
21
+ if not any(char.isalpha() for char in password):
22
+ raise HTTPException(status_code=400, detail="Password must contain at least one letter.")
23
+
24
+ def generate_otp() -> str:
25
+ """Generates a 6-digit numeric string OTP."""
26
+ return "".join(random.choices("0123456789", k=6))
27
+
28
+ @router.post("/register", response_model=UserOutput, status_code=201)
29
+ def register(data: RegisterInput, db: Session = Depends(get_db)):
30
+ """Registers an inactive user and sends a verification code via email."""
31
+ validate_password_strength(data.password)
32
+
33
+ if db.query(User).filter(User.username == data.username).first():
34
+ raise HTTPException(status_code=400, detail="Username is already taken")
35
+ if db.query(User).filter(User.email == data.email).first():
36
+ raise HTTPException(status_code=400, detail="Email is already registered")
37
+
38
+ # Generate 6-digit verification code
39
+ otp_code = generate_otp()
40
+
41
+ new_user = User(
42
+ username=data.username,
43
+ email=data.email,
44
+ password=hash_password(data.password),
45
+ is_verified=False,
46
+ verification_code=otp_code
47
+ )
48
+
49
+ db.add(new_user)
50
+ db.commit()
51
+ db.refresh(new_user)
52
+
53
+ # Send verification email
54
+ send_email(
55
+ to_email=new_user.email,
56
+ subject="Email Verification Code",
57
+ body=otp_code
58
+ )
59
+
60
+ return new_user
61
+
62
+ @router.post("/verify-email")
63
+ def verify_email(data: VerifyEmailInput, db: Session = Depends(get_db)):
64
+ """Verifies user's email using the registration OTP code."""
65
+ user = db.query(User).filter(User.email == data.email).first()
66
+ if not user:
67
+ raise HTTPException(status_code=404, detail="User not found")
68
+
69
+ if user.is_verified:
70
+ return {"detail": "Email already verified. You can sign in."}
71
+
72
+ if user.verification_code != data.code:
73
+ raise HTTPException(status_code=400, detail="Invalid verification code")
74
+
75
+ # Mark as verified
76
+ user.is_verified = True
77
+ user.verification_code = None
78
+ db.commit()
79
+
80
+ return {"detail": "Email verified successfully! You can now sign in."}
81
+
82
+ @router.post("/login", response_model=TokenOutput)
83
+ def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
84
+ """Logs in verified users and returns JWT tokens."""
85
+ user = db.query(User).filter(User.username == form_data.username).first()
86
+
87
+ if not user or not check_password(form_data.password, user.password):
88
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password")
89
+
90
+ if not user.is_active:
91
+ raise HTTPException(status_code=400, detail="User account is deactivated")
92
+
93
+ # Strict check: User must be verified
94
+ if not user.is_verified:
95
+ raise HTTPException(
96
+ status_code=status.HTTP_400_BAD_REQUEST,
97
+ detail="Email is not verified. Please verify your email first."
98
+ )
99
+
100
+ access = make_access_token(user.id, user.email)
101
+ refresh = make_refresh_token(user.id)
102
+
103
+ return TokenOutput(access_token=access, refresh_token=refresh)
104
+
105
+ @router.post("/forgot-password")
106
+ def forgot_password(data: ForgotPasswordRequest, db: Session = Depends(get_db)):
107
+ """Generates a password reset OTP and sends it via email."""
108
+ user = db.query(User).filter(User.email == data.email).first()
109
+
110
+ # Simple security practice: don't expose if email exists or not, but for basic debugging 404 is user-friendly.
111
+ if not user:
112
+ raise HTTPException(status_code=404, detail="Email not found")
113
+
114
+ otp_code = generate_otp()
115
+ user.reset_code = otp_code
116
+ db.commit()
117
+
118
+ send_email(
119
+ to_email=user.email,
120
+ subject="Password Reset Request Code",
121
+ body=otp_code
122
+ )
123
+
124
+ return {"detail": "Verification code has been sent to your email."}
125
+
126
+ @router.post("/reset-password")
127
+ def reset_password(data: PasswordResetInput, db: Session = Depends(get_db)):
128
+ """Validates the reset OTP and updates the password."""
129
+ validate_password_strength(data.new_password)
130
+
131
+ user = db.query(User).filter(User.email == data.email).first()
132
+ if not user:
133
+ raise HTTPException(status_code=404, detail="User not found")
134
+
135
+ if user.reset_code != data.code:
136
+ raise HTTPException(status_code=400, detail="Invalid password reset code")
137
+
138
+ user.password = hash_password(data.new_password)
139
+ user.reset_code = None
140
+ db.commit()
141
+
142
+ return {"detail": "Password has been reset successfully."}
143
+
144
+ @router.post("/refresh", response_model=TokenOutput)
145
+ def refresh(data: RefreshInput, db: Session = Depends(get_db)):
146
+ try:
147
+ payload = jwt.decode(data.refresh_token, SECRET_KEY, algorithms=[ALGORITHM])
148
+ if payload.get("type") != "refresh":
149
+ raise HTTPException(status_code=401, detail="Invalid token type")
150
+ user_id = int(payload.get("sub"))
151
+ exp_timestamp = payload.get("exp")
152
+ expires_at = datetime.utcfromtimestamp(exp_timestamp)
153
+ except JWTError:
154
+ raise HTTPException(status_code=401, detail="Refresh token is expired or invalid")
155
+
156
+ revoked = db.query(RevokedToken).filter(RevokedToken.token == data.refresh_token).first()
157
+ if revoked:
158
+ raise HTTPException(status_code=401, detail="Refresh token has been revoked")
159
+
160
+ user = db.query(User).filter(User.id == user_id).first()
161
+ if not user or not user.is_active or not user.is_verified:
162
+ raise HTTPException(status_code=401, detail="User not found, inactive, or unverified")
163
+
164
+ db_revoked = RevokedToken(token=data.refresh_token, expires_at=expires_at)
165
+ db.add(db_revoked)
166
+
167
+ access = make_access_token(user.id, user.email)
168
+ new_refresh = make_refresh_token(user.id)
169
+
170
+ db.commit()
171
+ return TokenOutput(access_token=access, refresh_token=new_refresh)
172
+
173
+ @router.post("/logout")
174
+ def logout(
175
+ data: RefreshInput = None,
176
+ access_token: str = Depends(oauth2_scheme),
177
+ db: Session = Depends(get_db),
178
+ current_user: User = Depends(get_current_user)
179
+ ):
180
+ try:
181
+ payload = jwt.decode(access_token, SECRET_KEY, algorithms=[ALGORITHM])
182
+ exp = datetime.utcfromtimestamp(payload.get("exp"))
183
+ except JWTError:
184
+ exp = datetime.utcnow() + datetime.timedelta(minutes=30)
185
+
186
+ if not db.query(RevokedToken).filter(RevokedToken.token == access_token).first():
187
+ db.add(RevokedToken(token=access_token, expires_at=exp))
188
+
189
+ if data and data.refresh_token:
190
+ try:
191
+ r_payload = jwt.decode(data.refresh_token, SECRET_KEY, algorithms=[ALGORITHM])
192
+ r_exp = datetime.utcfromtimestamp(r_payload.get("exp"))
193
+ if not db.query(RevokedToken).filter(RevokedToken.token == data.refresh_token).first():
194
+ db.add(RevokedToken(token=data.refresh_token, expires_at=r_exp))
195
+ except JWTError:
196
+ pass
197
+
198
+ db.commit()
199
+ return {"detail": "Successfully logged out and session revoked"}
backend/routes/user_routes.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, status
2
+ from sqlalchemy.orm import Session
3
+ from typing import List
4
+
5
+ from database import get_db
6
+ from models import User
7
+ from schemas import UserOutput, ProfileUpdateInput
8
+ from auth import get_current_user, hash_password
9
+ from routes.auth_routes import validate_password_strength
10
+
11
+ router = APIRouter(tags=["User Management"])
12
+
13
+ @router.get("/me", response_model=UserOutput)
14
+ def get_profile(current_user: User = Depends(get_current_user)):
15
+ return current_user
16
+
17
+ @router.put("/me", response_model=UserOutput)
18
+ def update_profile(
19
+ data: ProfileUpdateInput,
20
+ db: Session = Depends(get_db),
21
+ current_user: User = Depends(get_current_user)
22
+ ):
23
+ if data.username and data.username != current_user.username:
24
+ existing = db.query(User).filter(User.username == data.username).first()
25
+ if existing:
26
+ raise HTTPException(status_code=400, detail="Username is already taken")
27
+ current_user.username = data.username
28
+
29
+ if data.email and data.email != current_user.email:
30
+ existing = db.query(User).filter(User.email == data.email).first()
31
+ if existing:
32
+ raise HTTPException(status_code=400, detail="Email is already registered")
33
+ current_user.email = data.email
34
+
35
+ if data.password:
36
+ validate_password_strength(data.password)
37
+ current_user.password = hash_password(data.password)
38
+
39
+ db.commit()
40
+ db.refresh(current_user)
41
+ return current_user
42
+
43
+ @router.delete("/me", status_code=200)
44
+ def delete_account(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
45
+ db.delete(current_user)
46
+ db.commit()
47
+ return {"detail": "Account deleted successfully"}
48
+
49
+ @router.get("/admin/users", response_model=List[UserOutput])
50
+ def get_all_users_admin(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
51
+ if current_user.role != "admin":
52
+ raise HTTPException(
53
+ status_code=status.HTTP_403_FORBIDDEN,
54
+ detail="Forbidden: Admin access required"
55
+ )
56
+ users = db.query(User).all()
57
+ return users
backend/schemas.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, EmailStr, Field
2
+ from typing import Optional
3
+
4
+ class RegisterInput(BaseModel):
5
+ username: str = Field(..., min_length=3, max_length=50)
6
+ email: EmailStr
7
+ password: str = Field(..., min_length=8)
8
+
9
+ class TokenOutput(BaseModel):
10
+ access_token: str
11
+ refresh_token: str
12
+ token_type: str = "bearer"
13
+
14
+ class RefreshInput(BaseModel):
15
+ refresh_token: str
16
+
17
+ class UserOutput(BaseModel):
18
+ id: int
19
+ username: str
20
+ email: str
21
+ role: str
22
+ is_active: bool
23
+ is_verified: bool
24
+
25
+ class Config:
26
+ from_attributes = True
27
+
28
+ class ProfileUpdateInput(BaseModel):
29
+ username: Optional[str] = Field(None, min_length=3, max_length=50)
30
+ email: Optional[EmailStr] = None
31
+ password: Optional[str] = Field(None, min_length=8)
32
+
33
+ # ─── SMTP ROUTE SCHEMAS ───
34
+
35
+ class VerifyEmailInput(BaseModel):
36
+ email: EmailStr
37
+ code: str = Field(..., min_length=6, max_length=6, description="6-digit verification code")
38
+
39
+ class ForgotPasswordRequest(BaseModel):
40
+ email: EmailStr
41
+
42
+ class PasswordResetInput(BaseModel):
43
+ email: EmailStr
44
+ code: str = Field(..., min_length=6, max_length=6, description="6-digit reset code")
45
+ new_password: str = Field(..., min_length=8, description="Must be at least 8 characters")
docker-compose.yml ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ backend:
5
+ build: ./backend
6
+ ports:
7
+ - "8000:8000"
8
+ volumes:
9
+ - ./backend:/app
10
+ # Load SMTP settings and key variables from backend/.env automatically
11
+ env_file:
12
+ - ./backend/.env
13
+ # Override DATABASE_URL so it uses the container path
14
+ environment:
15
+ - DATABASE_URL=sqlite:////app/users.db
16
+ restart: always
17
+
18
+ frontend:
19
+ build: ./frontend
20
+ ports:
21
+ - "5173:5173"
22
+ volumes:
23
+ - ./frontend:/app
24
+ - /app/node_modules
25
+ depends_on:
26
+ - backend
27
+ restart: always
frontend/Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:18-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY package.json .
6
+ RUN npm install
7
+
8
+ COPY . .
9
+
10
+ EXPOSE 5173
11
+
12
+ CMD ["npm", "run", "dev", "--", "--host"]
frontend/index.html ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%236366f1'%3E%3Cpath d='M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z'/%3E%3C/svg%3E" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Secure Auth System (SMTP)</title>
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
11
+ </head>
12
+ <body>
13
+ <div id="root"></div>
14
+ <script type="module" src="/src/main.jsx"></script>
15
+ </body>
16
+ </html>
frontend/package-lock.json ADDED
@@ -0,0 +1,2029 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "auth-frontend-smtp",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "auth-frontend-smtp",
9
+ "version": "1.0.0",
10
+ "dependencies": {
11
+ "axios": "^1.7.2",
12
+ "react": "^18.3.1",
13
+ "react-dom": "^18.3.1",
14
+ "react-router-dom": "^6.24.0"
15
+ },
16
+ "devDependencies": {
17
+ "@vitejs/plugin-react": "^4.3.1",
18
+ "vite": "^5.3.1"
19
+ }
20
+ },
21
+ "node_modules/@babel/code-frame": {
22
+ "version": "7.29.7",
23
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
24
+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
25
+ "dev": true,
26
+ "license": "MIT",
27
+ "dependencies": {
28
+ "@babel/helper-validator-identifier": "^7.29.7",
29
+ "js-tokens": "^4.0.0",
30
+ "picocolors": "^1.1.1"
31
+ },
32
+ "engines": {
33
+ "node": ">=6.9.0"
34
+ }
35
+ },
36
+ "node_modules/@babel/compat-data": {
37
+ "version": "7.29.7",
38
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
39
+ "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
40
+ "dev": true,
41
+ "license": "MIT",
42
+ "engines": {
43
+ "node": ">=6.9.0"
44
+ }
45
+ },
46
+ "node_modules/@babel/core": {
47
+ "version": "7.29.7",
48
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
49
+ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
50
+ "dev": true,
51
+ "license": "MIT",
52
+ "dependencies": {
53
+ "@babel/code-frame": "^7.29.7",
54
+ "@babel/generator": "^7.29.7",
55
+ "@babel/helper-compilation-targets": "^7.29.7",
56
+ "@babel/helper-module-transforms": "^7.29.7",
57
+ "@babel/helpers": "^7.29.7",
58
+ "@babel/parser": "^7.29.7",
59
+ "@babel/template": "^7.29.7",
60
+ "@babel/traverse": "^7.29.7",
61
+ "@babel/types": "^7.29.7",
62
+ "@jridgewell/remapping": "^2.3.5",
63
+ "convert-source-map": "^2.0.0",
64
+ "debug": "^4.1.0",
65
+ "gensync": "^1.0.0-beta.2",
66
+ "json5": "^2.2.3",
67
+ "semver": "^6.3.1"
68
+ },
69
+ "engines": {
70
+ "node": ">=6.9.0"
71
+ },
72
+ "funding": {
73
+ "type": "opencollective",
74
+ "url": "https://opencollective.com/babel"
75
+ }
76
+ },
77
+ "node_modules/@babel/generator": {
78
+ "version": "7.29.7",
79
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
80
+ "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
81
+ "dev": true,
82
+ "license": "MIT",
83
+ "dependencies": {
84
+ "@babel/parser": "^7.29.7",
85
+ "@babel/types": "^7.29.7",
86
+ "@jridgewell/gen-mapping": "^0.3.12",
87
+ "@jridgewell/trace-mapping": "^0.3.28",
88
+ "jsesc": "^3.0.2"
89
+ },
90
+ "engines": {
91
+ "node": ">=6.9.0"
92
+ }
93
+ },
94
+ "node_modules/@babel/helper-compilation-targets": {
95
+ "version": "7.29.7",
96
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
97
+ "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
98
+ "dev": true,
99
+ "license": "MIT",
100
+ "dependencies": {
101
+ "@babel/compat-data": "^7.29.7",
102
+ "@babel/helper-validator-option": "^7.29.7",
103
+ "browserslist": "^4.24.0",
104
+ "lru-cache": "^5.1.1",
105
+ "semver": "^6.3.1"
106
+ },
107
+ "engines": {
108
+ "node": ">=6.9.0"
109
+ }
110
+ },
111
+ "node_modules/@babel/helper-globals": {
112
+ "version": "7.29.7",
113
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
114
+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
115
+ "dev": true,
116
+ "license": "MIT",
117
+ "engines": {
118
+ "node": ">=6.9.0"
119
+ }
120
+ },
121
+ "node_modules/@babel/helper-module-imports": {
122
+ "version": "7.29.7",
123
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
124
+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
125
+ "dev": true,
126
+ "license": "MIT",
127
+ "dependencies": {
128
+ "@babel/traverse": "^7.29.7",
129
+ "@babel/types": "^7.29.7"
130
+ },
131
+ "engines": {
132
+ "node": ">=6.9.0"
133
+ }
134
+ },
135
+ "node_modules/@babel/helper-module-transforms": {
136
+ "version": "7.29.7",
137
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
138
+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
139
+ "dev": true,
140
+ "license": "MIT",
141
+ "dependencies": {
142
+ "@babel/helper-module-imports": "^7.29.7",
143
+ "@babel/helper-validator-identifier": "^7.29.7",
144
+ "@babel/traverse": "^7.29.7"
145
+ },
146
+ "engines": {
147
+ "node": ">=6.9.0"
148
+ },
149
+ "peerDependencies": {
150
+ "@babel/core": "^7.0.0"
151
+ }
152
+ },
153
+ "node_modules/@babel/helper-plugin-utils": {
154
+ "version": "7.29.7",
155
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
156
+ "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
157
+ "dev": true,
158
+ "license": "MIT",
159
+ "engines": {
160
+ "node": ">=6.9.0"
161
+ }
162
+ },
163
+ "node_modules/@babel/helper-string-parser": {
164
+ "version": "7.29.7",
165
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
166
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
167
+ "dev": true,
168
+ "license": "MIT",
169
+ "engines": {
170
+ "node": ">=6.9.0"
171
+ }
172
+ },
173
+ "node_modules/@babel/helper-validator-identifier": {
174
+ "version": "7.29.7",
175
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
176
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
177
+ "dev": true,
178
+ "license": "MIT",
179
+ "engines": {
180
+ "node": ">=6.9.0"
181
+ }
182
+ },
183
+ "node_modules/@babel/helper-validator-option": {
184
+ "version": "7.29.7",
185
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
186
+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
187
+ "dev": true,
188
+ "license": "MIT",
189
+ "engines": {
190
+ "node": ">=6.9.0"
191
+ }
192
+ },
193
+ "node_modules/@babel/helpers": {
194
+ "version": "7.29.7",
195
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
196
+ "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
197
+ "dev": true,
198
+ "license": "MIT",
199
+ "dependencies": {
200
+ "@babel/template": "^7.29.7",
201
+ "@babel/types": "^7.29.7"
202
+ },
203
+ "engines": {
204
+ "node": ">=6.9.0"
205
+ }
206
+ },
207
+ "node_modules/@babel/parser": {
208
+ "version": "7.29.7",
209
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
210
+ "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
211
+ "dev": true,
212
+ "license": "MIT",
213
+ "dependencies": {
214
+ "@babel/types": "^7.29.7"
215
+ },
216
+ "bin": {
217
+ "parser": "bin/babel-parser.js"
218
+ },
219
+ "engines": {
220
+ "node": ">=6.0.0"
221
+ }
222
+ },
223
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
224
+ "version": "7.29.7",
225
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
226
+ "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
227
+ "dev": true,
228
+ "license": "MIT",
229
+ "dependencies": {
230
+ "@babel/helper-plugin-utils": "^7.29.7"
231
+ },
232
+ "engines": {
233
+ "node": ">=6.9.0"
234
+ },
235
+ "peerDependencies": {
236
+ "@babel/core": "^7.0.0-0"
237
+ }
238
+ },
239
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
240
+ "version": "7.29.7",
241
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
242
+ "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
243
+ "dev": true,
244
+ "license": "MIT",
245
+ "dependencies": {
246
+ "@babel/helper-plugin-utils": "^7.29.7"
247
+ },
248
+ "engines": {
249
+ "node": ">=6.9.0"
250
+ },
251
+ "peerDependencies": {
252
+ "@babel/core": "^7.0.0-0"
253
+ }
254
+ },
255
+ "node_modules/@babel/template": {
256
+ "version": "7.29.7",
257
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
258
+ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
259
+ "dev": true,
260
+ "license": "MIT",
261
+ "dependencies": {
262
+ "@babel/code-frame": "^7.29.7",
263
+ "@babel/parser": "^7.29.7",
264
+ "@babel/types": "^7.29.7"
265
+ },
266
+ "engines": {
267
+ "node": ">=6.9.0"
268
+ }
269
+ },
270
+ "node_modules/@babel/traverse": {
271
+ "version": "7.29.7",
272
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
273
+ "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
274
+ "dev": true,
275
+ "license": "MIT",
276
+ "dependencies": {
277
+ "@babel/code-frame": "^7.29.7",
278
+ "@babel/generator": "^7.29.7",
279
+ "@babel/helper-globals": "^7.29.7",
280
+ "@babel/parser": "^7.29.7",
281
+ "@babel/template": "^7.29.7",
282
+ "@babel/types": "^7.29.7",
283
+ "debug": "^4.3.1"
284
+ },
285
+ "engines": {
286
+ "node": ">=6.9.0"
287
+ }
288
+ },
289
+ "node_modules/@babel/types": {
290
+ "version": "7.29.7",
291
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
292
+ "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
293
+ "dev": true,
294
+ "license": "MIT",
295
+ "dependencies": {
296
+ "@babel/helper-string-parser": "^7.29.7",
297
+ "@babel/helper-validator-identifier": "^7.29.7"
298
+ },
299
+ "engines": {
300
+ "node": ">=6.9.0"
301
+ }
302
+ },
303
+ "node_modules/@esbuild/aix-ppc64": {
304
+ "version": "0.21.5",
305
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
306
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
307
+ "cpu": [
308
+ "ppc64"
309
+ ],
310
+ "dev": true,
311
+ "license": "MIT",
312
+ "optional": true,
313
+ "os": [
314
+ "aix"
315
+ ],
316
+ "engines": {
317
+ "node": ">=12"
318
+ }
319
+ },
320
+ "node_modules/@esbuild/android-arm": {
321
+ "version": "0.21.5",
322
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
323
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
324
+ "cpu": [
325
+ "arm"
326
+ ],
327
+ "dev": true,
328
+ "license": "MIT",
329
+ "optional": true,
330
+ "os": [
331
+ "android"
332
+ ],
333
+ "engines": {
334
+ "node": ">=12"
335
+ }
336
+ },
337
+ "node_modules/@esbuild/android-arm64": {
338
+ "version": "0.21.5",
339
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
340
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
341
+ "cpu": [
342
+ "arm64"
343
+ ],
344
+ "dev": true,
345
+ "license": "MIT",
346
+ "optional": true,
347
+ "os": [
348
+ "android"
349
+ ],
350
+ "engines": {
351
+ "node": ">=12"
352
+ }
353
+ },
354
+ "node_modules/@esbuild/android-x64": {
355
+ "version": "0.21.5",
356
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
357
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
358
+ "cpu": [
359
+ "x64"
360
+ ],
361
+ "dev": true,
362
+ "license": "MIT",
363
+ "optional": true,
364
+ "os": [
365
+ "android"
366
+ ],
367
+ "engines": {
368
+ "node": ">=12"
369
+ }
370
+ },
371
+ "node_modules/@esbuild/darwin-arm64": {
372
+ "version": "0.21.5",
373
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
374
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
375
+ "cpu": [
376
+ "arm64"
377
+ ],
378
+ "dev": true,
379
+ "license": "MIT",
380
+ "optional": true,
381
+ "os": [
382
+ "darwin"
383
+ ],
384
+ "engines": {
385
+ "node": ">=12"
386
+ }
387
+ },
388
+ "node_modules/@esbuild/darwin-x64": {
389
+ "version": "0.21.5",
390
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
391
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
392
+ "cpu": [
393
+ "x64"
394
+ ],
395
+ "dev": true,
396
+ "license": "MIT",
397
+ "optional": true,
398
+ "os": [
399
+ "darwin"
400
+ ],
401
+ "engines": {
402
+ "node": ">=12"
403
+ }
404
+ },
405
+ "node_modules/@esbuild/freebsd-arm64": {
406
+ "version": "0.21.5",
407
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
408
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
409
+ "cpu": [
410
+ "arm64"
411
+ ],
412
+ "dev": true,
413
+ "license": "MIT",
414
+ "optional": true,
415
+ "os": [
416
+ "freebsd"
417
+ ],
418
+ "engines": {
419
+ "node": ">=12"
420
+ }
421
+ },
422
+ "node_modules/@esbuild/freebsd-x64": {
423
+ "version": "0.21.5",
424
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
425
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
426
+ "cpu": [
427
+ "x64"
428
+ ],
429
+ "dev": true,
430
+ "license": "MIT",
431
+ "optional": true,
432
+ "os": [
433
+ "freebsd"
434
+ ],
435
+ "engines": {
436
+ "node": ">=12"
437
+ }
438
+ },
439
+ "node_modules/@esbuild/linux-arm": {
440
+ "version": "0.21.5",
441
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
442
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
443
+ "cpu": [
444
+ "arm"
445
+ ],
446
+ "dev": true,
447
+ "license": "MIT",
448
+ "optional": true,
449
+ "os": [
450
+ "linux"
451
+ ],
452
+ "engines": {
453
+ "node": ">=12"
454
+ }
455
+ },
456
+ "node_modules/@esbuild/linux-arm64": {
457
+ "version": "0.21.5",
458
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
459
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
460
+ "cpu": [
461
+ "arm64"
462
+ ],
463
+ "dev": true,
464
+ "license": "MIT",
465
+ "optional": true,
466
+ "os": [
467
+ "linux"
468
+ ],
469
+ "engines": {
470
+ "node": ">=12"
471
+ }
472
+ },
473
+ "node_modules/@esbuild/linux-ia32": {
474
+ "version": "0.21.5",
475
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
476
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
477
+ "cpu": [
478
+ "ia32"
479
+ ],
480
+ "dev": true,
481
+ "license": "MIT",
482
+ "optional": true,
483
+ "os": [
484
+ "linux"
485
+ ],
486
+ "engines": {
487
+ "node": ">=12"
488
+ }
489
+ },
490
+ "node_modules/@esbuild/linux-loong64": {
491
+ "version": "0.21.5",
492
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
493
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
494
+ "cpu": [
495
+ "loong64"
496
+ ],
497
+ "dev": true,
498
+ "license": "MIT",
499
+ "optional": true,
500
+ "os": [
501
+ "linux"
502
+ ],
503
+ "engines": {
504
+ "node": ">=12"
505
+ }
506
+ },
507
+ "node_modules/@esbuild/linux-mips64el": {
508
+ "version": "0.21.5",
509
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
510
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
511
+ "cpu": [
512
+ "mips64el"
513
+ ],
514
+ "dev": true,
515
+ "license": "MIT",
516
+ "optional": true,
517
+ "os": [
518
+ "linux"
519
+ ],
520
+ "engines": {
521
+ "node": ">=12"
522
+ }
523
+ },
524
+ "node_modules/@esbuild/linux-ppc64": {
525
+ "version": "0.21.5",
526
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
527
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
528
+ "cpu": [
529
+ "ppc64"
530
+ ],
531
+ "dev": true,
532
+ "license": "MIT",
533
+ "optional": true,
534
+ "os": [
535
+ "linux"
536
+ ],
537
+ "engines": {
538
+ "node": ">=12"
539
+ }
540
+ },
541
+ "node_modules/@esbuild/linux-riscv64": {
542
+ "version": "0.21.5",
543
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
544
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
545
+ "cpu": [
546
+ "riscv64"
547
+ ],
548
+ "dev": true,
549
+ "license": "MIT",
550
+ "optional": true,
551
+ "os": [
552
+ "linux"
553
+ ],
554
+ "engines": {
555
+ "node": ">=12"
556
+ }
557
+ },
558
+ "node_modules/@esbuild/linux-s390x": {
559
+ "version": "0.21.5",
560
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
561
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
562
+ "cpu": [
563
+ "s390x"
564
+ ],
565
+ "dev": true,
566
+ "license": "MIT",
567
+ "optional": true,
568
+ "os": [
569
+ "linux"
570
+ ],
571
+ "engines": {
572
+ "node": ">=12"
573
+ }
574
+ },
575
+ "node_modules/@esbuild/linux-x64": {
576
+ "version": "0.21.5",
577
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
578
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
579
+ "cpu": [
580
+ "x64"
581
+ ],
582
+ "dev": true,
583
+ "license": "MIT",
584
+ "optional": true,
585
+ "os": [
586
+ "linux"
587
+ ],
588
+ "engines": {
589
+ "node": ">=12"
590
+ }
591
+ },
592
+ "node_modules/@esbuild/netbsd-x64": {
593
+ "version": "0.21.5",
594
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
595
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
596
+ "cpu": [
597
+ "x64"
598
+ ],
599
+ "dev": true,
600
+ "license": "MIT",
601
+ "optional": true,
602
+ "os": [
603
+ "netbsd"
604
+ ],
605
+ "engines": {
606
+ "node": ">=12"
607
+ }
608
+ },
609
+ "node_modules/@esbuild/openbsd-x64": {
610
+ "version": "0.21.5",
611
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
612
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
613
+ "cpu": [
614
+ "x64"
615
+ ],
616
+ "dev": true,
617
+ "license": "MIT",
618
+ "optional": true,
619
+ "os": [
620
+ "openbsd"
621
+ ],
622
+ "engines": {
623
+ "node": ">=12"
624
+ }
625
+ },
626
+ "node_modules/@esbuild/sunos-x64": {
627
+ "version": "0.21.5",
628
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
629
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
630
+ "cpu": [
631
+ "x64"
632
+ ],
633
+ "dev": true,
634
+ "license": "MIT",
635
+ "optional": true,
636
+ "os": [
637
+ "sunos"
638
+ ],
639
+ "engines": {
640
+ "node": ">=12"
641
+ }
642
+ },
643
+ "node_modules/@esbuild/win32-arm64": {
644
+ "version": "0.21.5",
645
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
646
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
647
+ "cpu": [
648
+ "arm64"
649
+ ],
650
+ "dev": true,
651
+ "license": "MIT",
652
+ "optional": true,
653
+ "os": [
654
+ "win32"
655
+ ],
656
+ "engines": {
657
+ "node": ">=12"
658
+ }
659
+ },
660
+ "node_modules/@esbuild/win32-ia32": {
661
+ "version": "0.21.5",
662
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
663
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
664
+ "cpu": [
665
+ "ia32"
666
+ ],
667
+ "dev": true,
668
+ "license": "MIT",
669
+ "optional": true,
670
+ "os": [
671
+ "win32"
672
+ ],
673
+ "engines": {
674
+ "node": ">=12"
675
+ }
676
+ },
677
+ "node_modules/@esbuild/win32-x64": {
678
+ "version": "0.21.5",
679
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
680
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
681
+ "cpu": [
682
+ "x64"
683
+ ],
684
+ "dev": true,
685
+ "license": "MIT",
686
+ "optional": true,
687
+ "os": [
688
+ "win32"
689
+ ],
690
+ "engines": {
691
+ "node": ">=12"
692
+ }
693
+ },
694
+ "node_modules/@jridgewell/gen-mapping": {
695
+ "version": "0.3.13",
696
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
697
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
698
+ "dev": true,
699
+ "license": "MIT",
700
+ "dependencies": {
701
+ "@jridgewell/sourcemap-codec": "^1.5.0",
702
+ "@jridgewell/trace-mapping": "^0.3.24"
703
+ }
704
+ },
705
+ "node_modules/@jridgewell/remapping": {
706
+ "version": "2.3.5",
707
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
708
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
709
+ "dev": true,
710
+ "license": "MIT",
711
+ "dependencies": {
712
+ "@jridgewell/gen-mapping": "^0.3.5",
713
+ "@jridgewell/trace-mapping": "^0.3.24"
714
+ }
715
+ },
716
+ "node_modules/@jridgewell/resolve-uri": {
717
+ "version": "3.1.2",
718
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
719
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
720
+ "dev": true,
721
+ "license": "MIT",
722
+ "engines": {
723
+ "node": ">=6.0.0"
724
+ }
725
+ },
726
+ "node_modules/@jridgewell/sourcemap-codec": {
727
+ "version": "1.5.5",
728
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
729
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
730
+ "dev": true,
731
+ "license": "MIT"
732
+ },
733
+ "node_modules/@jridgewell/trace-mapping": {
734
+ "version": "0.3.31",
735
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
736
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
737
+ "dev": true,
738
+ "license": "MIT",
739
+ "dependencies": {
740
+ "@jridgewell/resolve-uri": "^3.1.0",
741
+ "@jridgewell/sourcemap-codec": "^1.4.14"
742
+ }
743
+ },
744
+ "node_modules/@remix-run/router": {
745
+ "version": "1.23.3",
746
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.3.tgz",
747
+ "integrity": "sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==",
748
+ "license": "MIT",
749
+ "engines": {
750
+ "node": ">=14.0.0"
751
+ }
752
+ },
753
+ "node_modules/@rolldown/pluginutils": {
754
+ "version": "1.0.0-beta.27",
755
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
756
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
757
+ "dev": true,
758
+ "license": "MIT"
759
+ },
760
+ "node_modules/@rollup/rollup-android-arm-eabi": {
761
+ "version": "4.62.0",
762
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz",
763
+ "integrity": "sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==",
764
+ "cpu": [
765
+ "arm"
766
+ ],
767
+ "dev": true,
768
+ "license": "MIT",
769
+ "optional": true,
770
+ "os": [
771
+ "android"
772
+ ]
773
+ },
774
+ "node_modules/@rollup/rollup-android-arm64": {
775
+ "version": "4.62.0",
776
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.0.tgz",
777
+ "integrity": "sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==",
778
+ "cpu": [
779
+ "arm64"
780
+ ],
781
+ "dev": true,
782
+ "license": "MIT",
783
+ "optional": true,
784
+ "os": [
785
+ "android"
786
+ ]
787
+ },
788
+ "node_modules/@rollup/rollup-darwin-arm64": {
789
+ "version": "4.62.0",
790
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.0.tgz",
791
+ "integrity": "sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==",
792
+ "cpu": [
793
+ "arm64"
794
+ ],
795
+ "dev": true,
796
+ "license": "MIT",
797
+ "optional": true,
798
+ "os": [
799
+ "darwin"
800
+ ]
801
+ },
802
+ "node_modules/@rollup/rollup-darwin-x64": {
803
+ "version": "4.62.0",
804
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.0.tgz",
805
+ "integrity": "sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==",
806
+ "cpu": [
807
+ "x64"
808
+ ],
809
+ "dev": true,
810
+ "license": "MIT",
811
+ "optional": true,
812
+ "os": [
813
+ "darwin"
814
+ ]
815
+ },
816
+ "node_modules/@rollup/rollup-freebsd-arm64": {
817
+ "version": "4.62.0",
818
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.0.tgz",
819
+ "integrity": "sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==",
820
+ "cpu": [
821
+ "arm64"
822
+ ],
823
+ "dev": true,
824
+ "license": "MIT",
825
+ "optional": true,
826
+ "os": [
827
+ "freebsd"
828
+ ]
829
+ },
830
+ "node_modules/@rollup/rollup-freebsd-x64": {
831
+ "version": "4.62.0",
832
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.0.tgz",
833
+ "integrity": "sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==",
834
+ "cpu": [
835
+ "x64"
836
+ ],
837
+ "dev": true,
838
+ "license": "MIT",
839
+ "optional": true,
840
+ "os": [
841
+ "freebsd"
842
+ ]
843
+ },
844
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
845
+ "version": "4.62.0",
846
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.0.tgz",
847
+ "integrity": "sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==",
848
+ "cpu": [
849
+ "arm"
850
+ ],
851
+ "dev": true,
852
+ "license": "MIT",
853
+ "optional": true,
854
+ "os": [
855
+ "linux"
856
+ ]
857
+ },
858
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
859
+ "version": "4.62.0",
860
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.0.tgz",
861
+ "integrity": "sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==",
862
+ "cpu": [
863
+ "arm"
864
+ ],
865
+ "dev": true,
866
+ "license": "MIT",
867
+ "optional": true,
868
+ "os": [
869
+ "linux"
870
+ ]
871
+ },
872
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
873
+ "version": "4.62.0",
874
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.0.tgz",
875
+ "integrity": "sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==",
876
+ "cpu": [
877
+ "arm64"
878
+ ],
879
+ "dev": true,
880
+ "license": "MIT",
881
+ "optional": true,
882
+ "os": [
883
+ "linux"
884
+ ]
885
+ },
886
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
887
+ "version": "4.62.0",
888
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.0.tgz",
889
+ "integrity": "sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==",
890
+ "cpu": [
891
+ "arm64"
892
+ ],
893
+ "dev": true,
894
+ "license": "MIT",
895
+ "optional": true,
896
+ "os": [
897
+ "linux"
898
+ ]
899
+ },
900
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
901
+ "version": "4.62.0",
902
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.0.tgz",
903
+ "integrity": "sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==",
904
+ "cpu": [
905
+ "loong64"
906
+ ],
907
+ "dev": true,
908
+ "license": "MIT",
909
+ "optional": true,
910
+ "os": [
911
+ "linux"
912
+ ]
913
+ },
914
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
915
+ "version": "4.62.0",
916
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.0.tgz",
917
+ "integrity": "sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==",
918
+ "cpu": [
919
+ "loong64"
920
+ ],
921
+ "dev": true,
922
+ "license": "MIT",
923
+ "optional": true,
924
+ "os": [
925
+ "linux"
926
+ ]
927
+ },
928
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
929
+ "version": "4.62.0",
930
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.0.tgz",
931
+ "integrity": "sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==",
932
+ "cpu": [
933
+ "ppc64"
934
+ ],
935
+ "dev": true,
936
+ "license": "MIT",
937
+ "optional": true,
938
+ "os": [
939
+ "linux"
940
+ ]
941
+ },
942
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
943
+ "version": "4.62.0",
944
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.0.tgz",
945
+ "integrity": "sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==",
946
+ "cpu": [
947
+ "ppc64"
948
+ ],
949
+ "dev": true,
950
+ "license": "MIT",
951
+ "optional": true,
952
+ "os": [
953
+ "linux"
954
+ ]
955
+ },
956
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
957
+ "version": "4.62.0",
958
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.0.tgz",
959
+ "integrity": "sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==",
960
+ "cpu": [
961
+ "riscv64"
962
+ ],
963
+ "dev": true,
964
+ "license": "MIT",
965
+ "optional": true,
966
+ "os": [
967
+ "linux"
968
+ ]
969
+ },
970
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
971
+ "version": "4.62.0",
972
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.0.tgz",
973
+ "integrity": "sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==",
974
+ "cpu": [
975
+ "riscv64"
976
+ ],
977
+ "dev": true,
978
+ "license": "MIT",
979
+ "optional": true,
980
+ "os": [
981
+ "linux"
982
+ ]
983
+ },
984
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
985
+ "version": "4.62.0",
986
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.0.tgz",
987
+ "integrity": "sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==",
988
+ "cpu": [
989
+ "s390x"
990
+ ],
991
+ "dev": true,
992
+ "license": "MIT",
993
+ "optional": true,
994
+ "os": [
995
+ "linux"
996
+ ]
997
+ },
998
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
999
+ "version": "4.62.0",
1000
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.0.tgz",
1001
+ "integrity": "sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==",
1002
+ "cpu": [
1003
+ "x64"
1004
+ ],
1005
+ "dev": true,
1006
+ "license": "MIT",
1007
+ "optional": true,
1008
+ "os": [
1009
+ "linux"
1010
+ ]
1011
+ },
1012
+ "node_modules/@rollup/rollup-linux-x64-musl": {
1013
+ "version": "4.62.0",
1014
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.0.tgz",
1015
+ "integrity": "sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==",
1016
+ "cpu": [
1017
+ "x64"
1018
+ ],
1019
+ "dev": true,
1020
+ "license": "MIT",
1021
+ "optional": true,
1022
+ "os": [
1023
+ "linux"
1024
+ ]
1025
+ },
1026
+ "node_modules/@rollup/rollup-openbsd-x64": {
1027
+ "version": "4.62.0",
1028
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.0.tgz",
1029
+ "integrity": "sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==",
1030
+ "cpu": [
1031
+ "x64"
1032
+ ],
1033
+ "dev": true,
1034
+ "license": "MIT",
1035
+ "optional": true,
1036
+ "os": [
1037
+ "openbsd"
1038
+ ]
1039
+ },
1040
+ "node_modules/@rollup/rollup-openharmony-arm64": {
1041
+ "version": "4.62.0",
1042
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.0.tgz",
1043
+ "integrity": "sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==",
1044
+ "cpu": [
1045
+ "arm64"
1046
+ ],
1047
+ "dev": true,
1048
+ "license": "MIT",
1049
+ "optional": true,
1050
+ "os": [
1051
+ "openharmony"
1052
+ ]
1053
+ },
1054
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
1055
+ "version": "4.62.0",
1056
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.0.tgz",
1057
+ "integrity": "sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==",
1058
+ "cpu": [
1059
+ "arm64"
1060
+ ],
1061
+ "dev": true,
1062
+ "license": "MIT",
1063
+ "optional": true,
1064
+ "os": [
1065
+ "win32"
1066
+ ]
1067
+ },
1068
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
1069
+ "version": "4.62.0",
1070
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.0.tgz",
1071
+ "integrity": "sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==",
1072
+ "cpu": [
1073
+ "ia32"
1074
+ ],
1075
+ "dev": true,
1076
+ "license": "MIT",
1077
+ "optional": true,
1078
+ "os": [
1079
+ "win32"
1080
+ ]
1081
+ },
1082
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
1083
+ "version": "4.62.0",
1084
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.0.tgz",
1085
+ "integrity": "sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==",
1086
+ "cpu": [
1087
+ "x64"
1088
+ ],
1089
+ "dev": true,
1090
+ "license": "MIT",
1091
+ "optional": true,
1092
+ "os": [
1093
+ "win32"
1094
+ ]
1095
+ },
1096
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
1097
+ "version": "4.62.0",
1098
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.0.tgz",
1099
+ "integrity": "sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==",
1100
+ "cpu": [
1101
+ "x64"
1102
+ ],
1103
+ "dev": true,
1104
+ "license": "MIT",
1105
+ "optional": true,
1106
+ "os": [
1107
+ "win32"
1108
+ ]
1109
+ },
1110
+ "node_modules/@types/babel__core": {
1111
+ "version": "7.20.5",
1112
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
1113
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
1114
+ "dev": true,
1115
+ "license": "MIT",
1116
+ "dependencies": {
1117
+ "@babel/parser": "^7.20.7",
1118
+ "@babel/types": "^7.20.7",
1119
+ "@types/babel__generator": "*",
1120
+ "@types/babel__template": "*",
1121
+ "@types/babel__traverse": "*"
1122
+ }
1123
+ },
1124
+ "node_modules/@types/babel__generator": {
1125
+ "version": "7.27.0",
1126
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
1127
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
1128
+ "dev": true,
1129
+ "license": "MIT",
1130
+ "dependencies": {
1131
+ "@babel/types": "^7.0.0"
1132
+ }
1133
+ },
1134
+ "node_modules/@types/babel__template": {
1135
+ "version": "7.4.4",
1136
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
1137
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
1138
+ "dev": true,
1139
+ "license": "MIT",
1140
+ "dependencies": {
1141
+ "@babel/parser": "^7.1.0",
1142
+ "@babel/types": "^7.0.0"
1143
+ }
1144
+ },
1145
+ "node_modules/@types/babel__traverse": {
1146
+ "version": "7.28.0",
1147
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
1148
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
1149
+ "dev": true,
1150
+ "license": "MIT",
1151
+ "dependencies": {
1152
+ "@babel/types": "^7.28.2"
1153
+ }
1154
+ },
1155
+ "node_modules/@types/estree": {
1156
+ "version": "1.0.9",
1157
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
1158
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
1159
+ "dev": true,
1160
+ "license": "MIT"
1161
+ },
1162
+ "node_modules/@vitejs/plugin-react": {
1163
+ "version": "4.7.0",
1164
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
1165
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
1166
+ "dev": true,
1167
+ "license": "MIT",
1168
+ "dependencies": {
1169
+ "@babel/core": "^7.28.0",
1170
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
1171
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
1172
+ "@rolldown/pluginutils": "1.0.0-beta.27",
1173
+ "@types/babel__core": "^7.20.5",
1174
+ "react-refresh": "^0.17.0"
1175
+ },
1176
+ "engines": {
1177
+ "node": "^14.18.0 || >=16.0.0"
1178
+ },
1179
+ "peerDependencies": {
1180
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
1181
+ }
1182
+ },
1183
+ "node_modules/agent-base": {
1184
+ "version": "6.0.2",
1185
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
1186
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
1187
+ "license": "MIT",
1188
+ "dependencies": {
1189
+ "debug": "4"
1190
+ },
1191
+ "engines": {
1192
+ "node": ">= 6.0.0"
1193
+ }
1194
+ },
1195
+ "node_modules/asynckit": {
1196
+ "version": "0.4.0",
1197
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
1198
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
1199
+ "license": "MIT"
1200
+ },
1201
+ "node_modules/axios": {
1202
+ "version": "1.18.0",
1203
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.0.tgz",
1204
+ "integrity": "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==",
1205
+ "license": "MIT",
1206
+ "dependencies": {
1207
+ "follow-redirects": "^1.16.0",
1208
+ "form-data": "^4.0.5",
1209
+ "https-proxy-agent": "^5.0.1",
1210
+ "proxy-from-env": "^2.1.0"
1211
+ }
1212
+ },
1213
+ "node_modules/baseline-browser-mapping": {
1214
+ "version": "2.10.37",
1215
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz",
1216
+ "integrity": "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==",
1217
+ "dev": true,
1218
+ "license": "Apache-2.0",
1219
+ "bin": {
1220
+ "baseline-browser-mapping": "dist/cli.cjs"
1221
+ },
1222
+ "engines": {
1223
+ "node": ">=6.0.0"
1224
+ }
1225
+ },
1226
+ "node_modules/browserslist": {
1227
+ "version": "4.28.2",
1228
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
1229
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
1230
+ "dev": true,
1231
+ "funding": [
1232
+ {
1233
+ "type": "opencollective",
1234
+ "url": "https://opencollective.com/browserslist"
1235
+ },
1236
+ {
1237
+ "type": "tidelift",
1238
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1239
+ },
1240
+ {
1241
+ "type": "github",
1242
+ "url": "https://github.com/sponsors/ai"
1243
+ }
1244
+ ],
1245
+ "license": "MIT",
1246
+ "dependencies": {
1247
+ "baseline-browser-mapping": "^2.10.12",
1248
+ "caniuse-lite": "^1.0.30001782",
1249
+ "electron-to-chromium": "^1.5.328",
1250
+ "node-releases": "^2.0.36",
1251
+ "update-browserslist-db": "^1.2.3"
1252
+ },
1253
+ "bin": {
1254
+ "browserslist": "cli.js"
1255
+ },
1256
+ "engines": {
1257
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
1258
+ }
1259
+ },
1260
+ "node_modules/call-bind-apply-helpers": {
1261
+ "version": "1.0.2",
1262
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
1263
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
1264
+ "license": "MIT",
1265
+ "dependencies": {
1266
+ "es-errors": "^1.3.0",
1267
+ "function-bind": "^1.1.2"
1268
+ },
1269
+ "engines": {
1270
+ "node": ">= 0.4"
1271
+ }
1272
+ },
1273
+ "node_modules/caniuse-lite": {
1274
+ "version": "1.0.30001799",
1275
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz",
1276
+ "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==",
1277
+ "dev": true,
1278
+ "funding": [
1279
+ {
1280
+ "type": "opencollective",
1281
+ "url": "https://opencollective.com/browserslist"
1282
+ },
1283
+ {
1284
+ "type": "tidelift",
1285
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
1286
+ },
1287
+ {
1288
+ "type": "github",
1289
+ "url": "https://github.com/sponsors/ai"
1290
+ }
1291
+ ],
1292
+ "license": "CC-BY-4.0"
1293
+ },
1294
+ "node_modules/combined-stream": {
1295
+ "version": "1.0.8",
1296
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
1297
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
1298
+ "license": "MIT",
1299
+ "dependencies": {
1300
+ "delayed-stream": "~1.0.0"
1301
+ },
1302
+ "engines": {
1303
+ "node": ">= 0.8"
1304
+ }
1305
+ },
1306
+ "node_modules/convert-source-map": {
1307
+ "version": "2.0.0",
1308
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
1309
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
1310
+ "dev": true,
1311
+ "license": "MIT"
1312
+ },
1313
+ "node_modules/debug": {
1314
+ "version": "4.4.3",
1315
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
1316
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
1317
+ "license": "MIT",
1318
+ "dependencies": {
1319
+ "ms": "^2.1.3"
1320
+ },
1321
+ "engines": {
1322
+ "node": ">=6.0"
1323
+ },
1324
+ "peerDependenciesMeta": {
1325
+ "supports-color": {
1326
+ "optional": true
1327
+ }
1328
+ }
1329
+ },
1330
+ "node_modules/delayed-stream": {
1331
+ "version": "1.0.0",
1332
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
1333
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
1334
+ "license": "MIT",
1335
+ "engines": {
1336
+ "node": ">=0.4.0"
1337
+ }
1338
+ },
1339
+ "node_modules/dunder-proto": {
1340
+ "version": "1.0.1",
1341
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
1342
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
1343
+ "license": "MIT",
1344
+ "dependencies": {
1345
+ "call-bind-apply-helpers": "^1.0.1",
1346
+ "es-errors": "^1.3.0",
1347
+ "gopd": "^1.2.0"
1348
+ },
1349
+ "engines": {
1350
+ "node": ">= 0.4"
1351
+ }
1352
+ },
1353
+ "node_modules/electron-to-chromium": {
1354
+ "version": "1.5.372",
1355
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.372.tgz",
1356
+ "integrity": "sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA==",
1357
+ "dev": true,
1358
+ "license": "ISC"
1359
+ },
1360
+ "node_modules/es-define-property": {
1361
+ "version": "1.0.1",
1362
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
1363
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
1364
+ "license": "MIT",
1365
+ "engines": {
1366
+ "node": ">= 0.4"
1367
+ }
1368
+ },
1369
+ "node_modules/es-errors": {
1370
+ "version": "1.3.0",
1371
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
1372
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
1373
+ "license": "MIT",
1374
+ "engines": {
1375
+ "node": ">= 0.4"
1376
+ }
1377
+ },
1378
+ "node_modules/es-object-atoms": {
1379
+ "version": "1.1.2",
1380
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
1381
+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
1382
+ "license": "MIT",
1383
+ "dependencies": {
1384
+ "es-errors": "^1.3.0"
1385
+ },
1386
+ "engines": {
1387
+ "node": ">= 0.4"
1388
+ }
1389
+ },
1390
+ "node_modules/es-set-tostringtag": {
1391
+ "version": "2.1.0",
1392
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
1393
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
1394
+ "license": "MIT",
1395
+ "dependencies": {
1396
+ "es-errors": "^1.3.0",
1397
+ "get-intrinsic": "^1.2.6",
1398
+ "has-tostringtag": "^1.0.2",
1399
+ "hasown": "^2.0.2"
1400
+ },
1401
+ "engines": {
1402
+ "node": ">= 0.4"
1403
+ }
1404
+ },
1405
+ "node_modules/esbuild": {
1406
+ "version": "0.21.5",
1407
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
1408
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
1409
+ "dev": true,
1410
+ "hasInstallScript": true,
1411
+ "license": "MIT",
1412
+ "bin": {
1413
+ "esbuild": "bin/esbuild"
1414
+ },
1415
+ "engines": {
1416
+ "node": ">=12"
1417
+ },
1418
+ "optionalDependencies": {
1419
+ "@esbuild/aix-ppc64": "0.21.5",
1420
+ "@esbuild/android-arm": "0.21.5",
1421
+ "@esbuild/android-arm64": "0.21.5",
1422
+ "@esbuild/android-x64": "0.21.5",
1423
+ "@esbuild/darwin-arm64": "0.21.5",
1424
+ "@esbuild/darwin-x64": "0.21.5",
1425
+ "@esbuild/freebsd-arm64": "0.21.5",
1426
+ "@esbuild/freebsd-x64": "0.21.5",
1427
+ "@esbuild/linux-arm": "0.21.5",
1428
+ "@esbuild/linux-arm64": "0.21.5",
1429
+ "@esbuild/linux-ia32": "0.21.5",
1430
+ "@esbuild/linux-loong64": "0.21.5",
1431
+ "@esbuild/linux-mips64el": "0.21.5",
1432
+ "@esbuild/linux-ppc64": "0.21.5",
1433
+ "@esbuild/linux-riscv64": "0.21.5",
1434
+ "@esbuild/linux-s390x": "0.21.5",
1435
+ "@esbuild/linux-x64": "0.21.5",
1436
+ "@esbuild/netbsd-x64": "0.21.5",
1437
+ "@esbuild/openbsd-x64": "0.21.5",
1438
+ "@esbuild/sunos-x64": "0.21.5",
1439
+ "@esbuild/win32-arm64": "0.21.5",
1440
+ "@esbuild/win32-ia32": "0.21.5",
1441
+ "@esbuild/win32-x64": "0.21.5"
1442
+ }
1443
+ },
1444
+ "node_modules/escalade": {
1445
+ "version": "3.2.0",
1446
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
1447
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
1448
+ "dev": true,
1449
+ "license": "MIT",
1450
+ "engines": {
1451
+ "node": ">=6"
1452
+ }
1453
+ },
1454
+ "node_modules/follow-redirects": {
1455
+ "version": "1.16.0",
1456
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
1457
+ "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
1458
+ "funding": [
1459
+ {
1460
+ "type": "individual",
1461
+ "url": "https://github.com/sponsors/RubenVerborgh"
1462
+ }
1463
+ ],
1464
+ "license": "MIT",
1465
+ "engines": {
1466
+ "node": ">=4.0"
1467
+ },
1468
+ "peerDependenciesMeta": {
1469
+ "debug": {
1470
+ "optional": true
1471
+ }
1472
+ }
1473
+ },
1474
+ "node_modules/form-data": {
1475
+ "version": "4.0.6",
1476
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
1477
+ "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
1478
+ "license": "MIT",
1479
+ "dependencies": {
1480
+ "asynckit": "^0.4.0",
1481
+ "combined-stream": "^1.0.8",
1482
+ "es-set-tostringtag": "^2.1.0",
1483
+ "hasown": "^2.0.4",
1484
+ "mime-types": "^2.1.35"
1485
+ },
1486
+ "engines": {
1487
+ "node": ">= 6"
1488
+ }
1489
+ },
1490
+ "node_modules/fsevents": {
1491
+ "version": "2.3.3",
1492
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1493
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1494
+ "dev": true,
1495
+ "hasInstallScript": true,
1496
+ "license": "MIT",
1497
+ "optional": true,
1498
+ "os": [
1499
+ "darwin"
1500
+ ],
1501
+ "engines": {
1502
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1503
+ }
1504
+ },
1505
+ "node_modules/function-bind": {
1506
+ "version": "1.1.2",
1507
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
1508
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
1509
+ "license": "MIT",
1510
+ "funding": {
1511
+ "url": "https://github.com/sponsors/ljharb"
1512
+ }
1513
+ },
1514
+ "node_modules/gensync": {
1515
+ "version": "1.0.0-beta.2",
1516
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
1517
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
1518
+ "dev": true,
1519
+ "license": "MIT",
1520
+ "engines": {
1521
+ "node": ">=6.9.0"
1522
+ }
1523
+ },
1524
+ "node_modules/get-intrinsic": {
1525
+ "version": "1.3.0",
1526
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
1527
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
1528
+ "license": "MIT",
1529
+ "dependencies": {
1530
+ "call-bind-apply-helpers": "^1.0.2",
1531
+ "es-define-property": "^1.0.1",
1532
+ "es-errors": "^1.3.0",
1533
+ "es-object-atoms": "^1.1.1",
1534
+ "function-bind": "^1.1.2",
1535
+ "get-proto": "^1.0.1",
1536
+ "gopd": "^1.2.0",
1537
+ "has-symbols": "^1.1.0",
1538
+ "hasown": "^2.0.2",
1539
+ "math-intrinsics": "^1.1.0"
1540
+ },
1541
+ "engines": {
1542
+ "node": ">= 0.4"
1543
+ },
1544
+ "funding": {
1545
+ "url": "https://github.com/sponsors/ljharb"
1546
+ }
1547
+ },
1548
+ "node_modules/get-proto": {
1549
+ "version": "1.0.1",
1550
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
1551
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
1552
+ "license": "MIT",
1553
+ "dependencies": {
1554
+ "dunder-proto": "^1.0.1",
1555
+ "es-object-atoms": "^1.0.0"
1556
+ },
1557
+ "engines": {
1558
+ "node": ">= 0.4"
1559
+ }
1560
+ },
1561
+ "node_modules/gopd": {
1562
+ "version": "1.2.0",
1563
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
1564
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
1565
+ "license": "MIT",
1566
+ "engines": {
1567
+ "node": ">= 0.4"
1568
+ },
1569
+ "funding": {
1570
+ "url": "https://github.com/sponsors/ljharb"
1571
+ }
1572
+ },
1573
+ "node_modules/has-symbols": {
1574
+ "version": "1.1.0",
1575
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
1576
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
1577
+ "license": "MIT",
1578
+ "engines": {
1579
+ "node": ">= 0.4"
1580
+ },
1581
+ "funding": {
1582
+ "url": "https://github.com/sponsors/ljharb"
1583
+ }
1584
+ },
1585
+ "node_modules/has-tostringtag": {
1586
+ "version": "1.0.2",
1587
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
1588
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
1589
+ "license": "MIT",
1590
+ "dependencies": {
1591
+ "has-symbols": "^1.0.3"
1592
+ },
1593
+ "engines": {
1594
+ "node": ">= 0.4"
1595
+ },
1596
+ "funding": {
1597
+ "url": "https://github.com/sponsors/ljharb"
1598
+ }
1599
+ },
1600
+ "node_modules/hasown": {
1601
+ "version": "2.0.4",
1602
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
1603
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
1604
+ "license": "MIT",
1605
+ "dependencies": {
1606
+ "function-bind": "^1.1.2"
1607
+ },
1608
+ "engines": {
1609
+ "node": ">= 0.4"
1610
+ }
1611
+ },
1612
+ "node_modules/https-proxy-agent": {
1613
+ "version": "5.0.1",
1614
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
1615
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
1616
+ "license": "MIT",
1617
+ "dependencies": {
1618
+ "agent-base": "6",
1619
+ "debug": "4"
1620
+ },
1621
+ "engines": {
1622
+ "node": ">= 6"
1623
+ }
1624
+ },
1625
+ "node_modules/js-tokens": {
1626
+ "version": "4.0.0",
1627
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
1628
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
1629
+ "license": "MIT"
1630
+ },
1631
+ "node_modules/jsesc": {
1632
+ "version": "3.1.0",
1633
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
1634
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
1635
+ "dev": true,
1636
+ "license": "MIT",
1637
+ "bin": {
1638
+ "jsesc": "bin/jsesc"
1639
+ },
1640
+ "engines": {
1641
+ "node": ">=6"
1642
+ }
1643
+ },
1644
+ "node_modules/json5": {
1645
+ "version": "2.2.3",
1646
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
1647
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
1648
+ "dev": true,
1649
+ "license": "MIT",
1650
+ "bin": {
1651
+ "json5": "lib/cli.js"
1652
+ },
1653
+ "engines": {
1654
+ "node": ">=6"
1655
+ }
1656
+ },
1657
+ "node_modules/loose-envify": {
1658
+ "version": "1.4.0",
1659
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
1660
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
1661
+ "license": "MIT",
1662
+ "dependencies": {
1663
+ "js-tokens": "^3.0.0 || ^4.0.0"
1664
+ },
1665
+ "bin": {
1666
+ "loose-envify": "cli.js"
1667
+ }
1668
+ },
1669
+ "node_modules/lru-cache": {
1670
+ "version": "5.1.1",
1671
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
1672
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
1673
+ "dev": true,
1674
+ "license": "ISC",
1675
+ "dependencies": {
1676
+ "yallist": "^3.0.2"
1677
+ }
1678
+ },
1679
+ "node_modules/math-intrinsics": {
1680
+ "version": "1.1.0",
1681
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
1682
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
1683
+ "license": "MIT",
1684
+ "engines": {
1685
+ "node": ">= 0.4"
1686
+ }
1687
+ },
1688
+ "node_modules/mime-db": {
1689
+ "version": "1.52.0",
1690
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
1691
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
1692
+ "license": "MIT",
1693
+ "engines": {
1694
+ "node": ">= 0.6"
1695
+ }
1696
+ },
1697
+ "node_modules/mime-types": {
1698
+ "version": "2.1.35",
1699
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
1700
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
1701
+ "license": "MIT",
1702
+ "dependencies": {
1703
+ "mime-db": "1.52.0"
1704
+ },
1705
+ "engines": {
1706
+ "node": ">= 0.6"
1707
+ }
1708
+ },
1709
+ "node_modules/ms": {
1710
+ "version": "2.1.3",
1711
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1712
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1713
+ "license": "MIT"
1714
+ },
1715
+ "node_modules/nanoid": {
1716
+ "version": "3.3.12",
1717
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
1718
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
1719
+ "dev": true,
1720
+ "funding": [
1721
+ {
1722
+ "type": "github",
1723
+ "url": "https://github.com/sponsors/ai"
1724
+ }
1725
+ ],
1726
+ "license": "MIT",
1727
+ "bin": {
1728
+ "nanoid": "bin/nanoid.cjs"
1729
+ },
1730
+ "engines": {
1731
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1732
+ }
1733
+ },
1734
+ "node_modules/node-releases": {
1735
+ "version": "2.0.47",
1736
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz",
1737
+ "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==",
1738
+ "dev": true,
1739
+ "license": "MIT",
1740
+ "engines": {
1741
+ "node": ">=18"
1742
+ }
1743
+ },
1744
+ "node_modules/picocolors": {
1745
+ "version": "1.1.1",
1746
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1747
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1748
+ "dev": true,
1749
+ "license": "ISC"
1750
+ },
1751
+ "node_modules/postcss": {
1752
+ "version": "8.5.15",
1753
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
1754
+ "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
1755
+ "dev": true,
1756
+ "funding": [
1757
+ {
1758
+ "type": "opencollective",
1759
+ "url": "https://opencollective.com/postcss/"
1760
+ },
1761
+ {
1762
+ "type": "tidelift",
1763
+ "url": "https://tidelift.com/funding/github/npm/postcss"
1764
+ },
1765
+ {
1766
+ "type": "github",
1767
+ "url": "https://github.com/sponsors/ai"
1768
+ }
1769
+ ],
1770
+ "license": "MIT",
1771
+ "dependencies": {
1772
+ "nanoid": "^3.3.12",
1773
+ "picocolors": "^1.1.1",
1774
+ "source-map-js": "^1.2.1"
1775
+ },
1776
+ "engines": {
1777
+ "node": "^10 || ^12 || >=14"
1778
+ }
1779
+ },
1780
+ "node_modules/proxy-from-env": {
1781
+ "version": "2.1.0",
1782
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
1783
+ "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
1784
+ "license": "MIT",
1785
+ "engines": {
1786
+ "node": ">=10"
1787
+ }
1788
+ },
1789
+ "node_modules/react": {
1790
+ "version": "18.3.1",
1791
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
1792
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
1793
+ "license": "MIT",
1794
+ "dependencies": {
1795
+ "loose-envify": "^1.1.0"
1796
+ },
1797
+ "engines": {
1798
+ "node": ">=0.10.0"
1799
+ }
1800
+ },
1801
+ "node_modules/react-dom": {
1802
+ "version": "18.3.1",
1803
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
1804
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
1805
+ "license": "MIT",
1806
+ "dependencies": {
1807
+ "loose-envify": "^1.1.0",
1808
+ "scheduler": "^0.23.2"
1809
+ },
1810
+ "peerDependencies": {
1811
+ "react": "^18.3.1"
1812
+ }
1813
+ },
1814
+ "node_modules/react-refresh": {
1815
+ "version": "0.17.0",
1816
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
1817
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
1818
+ "dev": true,
1819
+ "license": "MIT",
1820
+ "engines": {
1821
+ "node": ">=0.10.0"
1822
+ }
1823
+ },
1824
+ "node_modules/react-router": {
1825
+ "version": "6.30.4",
1826
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.4.tgz",
1827
+ "integrity": "sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==",
1828
+ "license": "MIT",
1829
+ "dependencies": {
1830
+ "@remix-run/router": "1.23.3"
1831
+ },
1832
+ "engines": {
1833
+ "node": ">=14.0.0"
1834
+ },
1835
+ "peerDependencies": {
1836
+ "react": ">=16.8"
1837
+ }
1838
+ },
1839
+ "node_modules/react-router-dom": {
1840
+ "version": "6.30.4",
1841
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.4.tgz",
1842
+ "integrity": "sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==",
1843
+ "license": "MIT",
1844
+ "dependencies": {
1845
+ "@remix-run/router": "1.23.3",
1846
+ "react-router": "6.30.4"
1847
+ },
1848
+ "engines": {
1849
+ "node": ">=14.0.0"
1850
+ },
1851
+ "peerDependencies": {
1852
+ "react": ">=16.8",
1853
+ "react-dom": ">=16.8"
1854
+ }
1855
+ },
1856
+ "node_modules/rollup": {
1857
+ "version": "4.62.0",
1858
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz",
1859
+ "integrity": "sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==",
1860
+ "dev": true,
1861
+ "license": "MIT",
1862
+ "dependencies": {
1863
+ "@types/estree": "1.0.9"
1864
+ },
1865
+ "bin": {
1866
+ "rollup": "dist/bin/rollup"
1867
+ },
1868
+ "engines": {
1869
+ "node": ">=18.0.0",
1870
+ "npm": ">=8.0.0"
1871
+ },
1872
+ "optionalDependencies": {
1873
+ "@rollup/rollup-android-arm-eabi": "4.62.0",
1874
+ "@rollup/rollup-android-arm64": "4.62.0",
1875
+ "@rollup/rollup-darwin-arm64": "4.62.0",
1876
+ "@rollup/rollup-darwin-x64": "4.62.0",
1877
+ "@rollup/rollup-freebsd-arm64": "4.62.0",
1878
+ "@rollup/rollup-freebsd-x64": "4.62.0",
1879
+ "@rollup/rollup-linux-arm-gnueabihf": "4.62.0",
1880
+ "@rollup/rollup-linux-arm-musleabihf": "4.62.0",
1881
+ "@rollup/rollup-linux-arm64-gnu": "4.62.0",
1882
+ "@rollup/rollup-linux-arm64-musl": "4.62.0",
1883
+ "@rollup/rollup-linux-loong64-gnu": "4.62.0",
1884
+ "@rollup/rollup-linux-loong64-musl": "4.62.0",
1885
+ "@rollup/rollup-linux-ppc64-gnu": "4.62.0",
1886
+ "@rollup/rollup-linux-ppc64-musl": "4.62.0",
1887
+ "@rollup/rollup-linux-riscv64-gnu": "4.62.0",
1888
+ "@rollup/rollup-linux-riscv64-musl": "4.62.0",
1889
+ "@rollup/rollup-linux-s390x-gnu": "4.62.0",
1890
+ "@rollup/rollup-linux-x64-gnu": "4.62.0",
1891
+ "@rollup/rollup-linux-x64-musl": "4.62.0",
1892
+ "@rollup/rollup-openbsd-x64": "4.62.0",
1893
+ "@rollup/rollup-openharmony-arm64": "4.62.0",
1894
+ "@rollup/rollup-win32-arm64-msvc": "4.62.0",
1895
+ "@rollup/rollup-win32-ia32-msvc": "4.62.0",
1896
+ "@rollup/rollup-win32-x64-gnu": "4.62.0",
1897
+ "@rollup/rollup-win32-x64-msvc": "4.62.0",
1898
+ "fsevents": "~2.3.2"
1899
+ }
1900
+ },
1901
+ "node_modules/scheduler": {
1902
+ "version": "0.23.2",
1903
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
1904
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
1905
+ "license": "MIT",
1906
+ "dependencies": {
1907
+ "loose-envify": "^1.1.0"
1908
+ }
1909
+ },
1910
+ "node_modules/semver": {
1911
+ "version": "6.3.1",
1912
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
1913
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
1914
+ "dev": true,
1915
+ "license": "ISC",
1916
+ "bin": {
1917
+ "semver": "bin/semver.js"
1918
+ }
1919
+ },
1920
+ "node_modules/source-map-js": {
1921
+ "version": "1.2.1",
1922
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
1923
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
1924
+ "dev": true,
1925
+ "license": "BSD-3-Clause",
1926
+ "engines": {
1927
+ "node": ">=0.10.0"
1928
+ }
1929
+ },
1930
+ "node_modules/update-browserslist-db": {
1931
+ "version": "1.2.3",
1932
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
1933
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
1934
+ "dev": true,
1935
+ "funding": [
1936
+ {
1937
+ "type": "opencollective",
1938
+ "url": "https://opencollective.com/browserslist"
1939
+ },
1940
+ {
1941
+ "type": "tidelift",
1942
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1943
+ },
1944
+ {
1945
+ "type": "github",
1946
+ "url": "https://github.com/sponsors/ai"
1947
+ }
1948
+ ],
1949
+ "license": "MIT",
1950
+ "dependencies": {
1951
+ "escalade": "^3.2.0",
1952
+ "picocolors": "^1.1.1"
1953
+ },
1954
+ "bin": {
1955
+ "update-browserslist-db": "cli.js"
1956
+ },
1957
+ "peerDependencies": {
1958
+ "browserslist": ">= 4.21.0"
1959
+ }
1960
+ },
1961
+ "node_modules/vite": {
1962
+ "version": "5.4.21",
1963
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
1964
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
1965
+ "dev": true,
1966
+ "license": "MIT",
1967
+ "dependencies": {
1968
+ "esbuild": "^0.21.3",
1969
+ "postcss": "^8.4.43",
1970
+ "rollup": "^4.20.0"
1971
+ },
1972
+ "bin": {
1973
+ "vite": "bin/vite.js"
1974
+ },
1975
+ "engines": {
1976
+ "node": "^18.0.0 || >=20.0.0"
1977
+ },
1978
+ "funding": {
1979
+ "url": "https://github.com/vitejs/vite?sponsor=1"
1980
+ },
1981
+ "optionalDependencies": {
1982
+ "fsevents": "~2.3.3"
1983
+ },
1984
+ "peerDependencies": {
1985
+ "@types/node": "^18.0.0 || >=20.0.0",
1986
+ "less": "*",
1987
+ "lightningcss": "^1.21.0",
1988
+ "sass": "*",
1989
+ "sass-embedded": "*",
1990
+ "stylus": "*",
1991
+ "sugarss": "*",
1992
+ "terser": "^5.4.0"
1993
+ },
1994
+ "peerDependenciesMeta": {
1995
+ "@types/node": {
1996
+ "optional": true
1997
+ },
1998
+ "less": {
1999
+ "optional": true
2000
+ },
2001
+ "lightningcss": {
2002
+ "optional": true
2003
+ },
2004
+ "sass": {
2005
+ "optional": true
2006
+ },
2007
+ "sass-embedded": {
2008
+ "optional": true
2009
+ },
2010
+ "stylus": {
2011
+ "optional": true
2012
+ },
2013
+ "sugarss": {
2014
+ "optional": true
2015
+ },
2016
+ "terser": {
2017
+ "optional": true
2018
+ }
2019
+ }
2020
+ },
2021
+ "node_modules/yallist": {
2022
+ "version": "3.1.1",
2023
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
2024
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
2025
+ "dev": true,
2026
+ "license": "ISC"
2027
+ }
2028
+ }
2029
+ }
frontend/package.json ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "auth-frontend-smtp",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "vite build"
8
+ },
9
+ "dependencies": {
10
+ "axios": "^1.7.2",
11
+ "react": "^18.3.1",
12
+ "react-dom": "^18.3.1",
13
+ "react-router-dom": "^6.24.0"
14
+ },
15
+ "devDependencies": {
16
+ "@vitejs/plugin-react": "^4.3.1",
17
+ "vite": "^5.3.1"
18
+ }
19
+ }
frontend/src/App.jsx ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Routes, Route, Navigate } from "react-router-dom"
2
+ import { AuthProvider } from "./context/AuthContext"
3
+ import Navbar from "./components/Navbar"
4
+ import ProtectedRoute from "./components/ProtectedRoute"
5
+ import Login from "./pages/Login"
6
+ import Register from "./pages/Register"
7
+ import VerifyEmail from "./pages/VerifyEmail"
8
+ import Dashboard from "./pages/Dashboard"
9
+
10
+ export default function App() {
11
+ return (
12
+ <AuthProvider>
13
+ <Navbar />
14
+ <Routes>
15
+ <Route path="/login" element={<Login />} />
16
+ <Route path="/register" element={<Register />} />
17
+ <Route path="/verify-email" element={<VerifyEmail />} />
18
+ <Route
19
+ path="/dashboard"
20
+ element={
21
+ <ProtectedRoute>
22
+ <Dashboard />
23
+ </ProtectedRoute>
24
+ }
25
+ />
26
+ <Route path="*" element={<Navigate to="/dashboard" replace />} />
27
+ </Routes>
28
+ </AuthProvider>
29
+ )
30
+ }
frontend/src/api/axios.js ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import axios from "axios"
2
+
3
+ const api = axios.create({
4
+ baseURL: window.location.hostname === "localhost" ? "http://localhost:8000" : "",
5
+ })
6
+
7
+ api.interceptors.request.use((config) => {
8
+ const token = sessionStorage.getItem("access_token")
9
+ if (token) {
10
+ config.headers.Authorization = `Bearer ${token}`
11
+ }
12
+ return config
13
+ }, (error) => {
14
+ return Promise.reject(error)
15
+ })
16
+
17
+ api.interceptors.response.use(
18
+ (res) => res,
19
+ async (error) => {
20
+ const originalRequest = error.config
21
+ if (error.response?.status === 401 && !originalRequest._retry) {
22
+ originalRequest._retry = true
23
+ const refresh = sessionStorage.getItem("refresh_token")
24
+
25
+ if (!refresh) {
26
+ sessionStorage.clear()
27
+ window.location.href = "/login"
28
+ return Promise.reject(error)
29
+ }
30
+
31
+ try {
32
+ const baseURL = window.location.hostname === "localhost" ? "http://localhost:8000" : "";
33
+ const { data } = await axios.post(`${baseURL}/auth/refresh`, {
34
+ refresh_token: refresh,
35
+ })
36
+ sessionStorage.setItem("access_token", data.access_token)
37
+ sessionStorage.setItem("refresh_token", data.refresh_token)
38
+ originalRequest.headers.Authorization = `Bearer ${data.access_token}`
39
+ return api(originalRequest)
40
+ } catch (refreshError) {
41
+ sessionStorage.clear()
42
+ window.location.href = "/login"
43
+ return Promise.reject(refreshError)
44
+ }
45
+ }
46
+ return Promise.reject(error)
47
+ }
48
+ )
49
+
50
+ export default api
frontend/src/components/Navbar.jsx ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Link, NavLink, useNavigate } from "react-router-dom"
2
+ import { useAuth } from "../context/AuthContext"
3
+
4
+ export default function Navbar() {
5
+ const { user, logout } = useAuth()
6
+ const navigate = useNavigate()
7
+
8
+ async function handleLogout() {
9
+ await logout()
10
+ navigate("/login")
11
+ }
12
+
13
+ return (
14
+ <nav className="navbar">
15
+ <Link to="/" className="nav-brand">
16
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style={{ verticalAlign: 'middle' }}>
17
+ <path d="M12 2L3 7v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V7l-9-5z" fill="currentColor"/>
18
+ </svg>
19
+ <span>SecureAuth</span>
20
+ </Link>
21
+
22
+ <div className="nav-links">
23
+ {user ? (
24
+ <>
25
+ <span style={{ fontSize: "0.9rem", color: "var(--text-secondary)" }}>
26
+ Welcome, <strong style={{ color: "var(--text-primary)" }}>{user.username}</strong>
27
+ {user.role === "admin" && <span style={{ marginLeft: "6px", fontSize: "0.75rem", background: "rgba(99,102,241,0.2)", padding: "2px 6px", borderRadius: "10px", color: "#818cf8", fontWeight: "700" }}>ADMIN</span>}
28
+ </span>
29
+ <button className="btn btn-secondary" style={{ padding: "0.4rem 0.9rem", fontSize: "0.85rem", width: "auto" }} onClick={handleLogout}>
30
+ Sign Out
31
+ </button>
32
+ </>
33
+ ) : (
34
+ <>
35
+ <NavLink to="/login" className={({ isActive }) => isActive ? "nav-link active" : "nav-link"}>
36
+ Sign In
37
+ </NavLink>
38
+ <NavLink to="/register" className={({ isActive }) => isActive ? "nav-link active" : "nav-link"}>
39
+ Register
40
+ </NavLink>
41
+ </>
42
+ )}
43
+ </div>
44
+ </nav>
45
+ )
46
+ }
frontend/src/components/ProtectedRoute.jsx ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Navigate } from "react-router-dom"
2
+ import { useAuth } from "../context/AuthContext"
3
+
4
+ export default function ProtectedRoute({ children }) {
5
+ const { user, loading } = useAuth()
6
+
7
+ if (loading) {
8
+ return (
9
+ <div style={{
10
+ display: "flex",
11
+ flexDirection: "column",
12
+ alignItems: "center",
13
+ justifyContent: "center",
14
+ height: "80vh",
15
+ gap: "1rem"
16
+ }}>
17
+ <div style={{
18
+ width: "40px",
19
+ height: "40px",
20
+ border: "4px solid rgba(255, 255, 255, 0.1)",
21
+ borderTop: "4px solid var(--accent-color)",
22
+ borderRadius: "50%",
23
+ animation: "spin 1s linear infinite"
24
+ }} />
25
+ <p style={{ color: "var(--text-secondary)", fontSize: "0.95rem" }}>Loading your session...</p>
26
+ <style>{`
27
+ @keyframes spin {
28
+ 0% { transform: rotate(0deg); }
29
+ 100% { transform: rotate(360deg); }
30
+ }
31
+ `}</style>
32
+ </div>
33
+ )
34
+ }
35
+
36
+ return user ? children : <Navigate to="/login" replace />
37
+ }
frontend/src/context/AuthContext.jsx ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createContext, useContext, useState, useEffect } from "react"
2
+ import api from "../api/axios"
3
+
4
+ const AuthContext = createContext(null)
5
+
6
+ export function AuthProvider({ children }) {
7
+ const [user, setUser] = useState(null)
8
+ const [loading, setLoading] = useState(true)
9
+
10
+ useEffect(() => {
11
+ const token = sessionStorage.getItem("access_token")
12
+ if (!token) {
13
+ setLoading(false)
14
+ return
15
+ }
16
+ api.get("/me")
17
+ .then(({ data }) => setUser(data))
18
+ .catch(() => {
19
+ sessionStorage.clear()
20
+ setUser(null)
21
+ })
22
+ .finally(() => setLoading(false))
23
+ }, [])
24
+
25
+ async function login(username, password) {
26
+ const form = new URLSearchParams()
27
+ form.append("username", username)
28
+ form.append("password", password)
29
+
30
+ const { data } = await api.post("/auth/login", form, {
31
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
32
+ })
33
+
34
+ sessionStorage.setItem("access_token", data.access_token)
35
+ sessionStorage.setItem("refresh_token", data.refresh_token)
36
+
37
+ const me = await api.get("/me")
38
+ setUser(me.data)
39
+ }
40
+
41
+ async function register(username, email, password) {
42
+ // Registration returns user data, and emails the verification OTP code
43
+ await api.post("/auth/register", { username, email, password })
44
+ }
45
+
46
+ // ─── SMTP ROUTING FUNCTIONS ───
47
+
48
+ async function verifyEmail(email, code) {
49
+ await api.post("/auth/verify-email", { email, code })
50
+ }
51
+
52
+ async function forgotPassword(email) {
53
+ await api.post("/auth/forgot-password", { email })
54
+ }
55
+
56
+ async function resetPassword(email, code, new_password) {
57
+ await api.post("/auth/reset-password", { email, code, new_password })
58
+ }
59
+
60
+ async function logout() {
61
+ const refreshToken = sessionStorage.getItem("refresh_token")
62
+ try {
63
+ await api.post("/auth/logout", { refresh_token: refreshToken })
64
+ } catch (err) {
65
+ console.warn("Logout request to server failed, clearing local session.", err)
66
+ } finally {
67
+ sessionStorage.clear()
68
+ setUser(null)
69
+ }
70
+ }
71
+
72
+ async function updateProfile(profileData) {
73
+ const { data } = await api.put("/me", profileData)
74
+ setUser(data)
75
+ return data;
76
+ }
77
+
78
+ async function deleteAccount() {
79
+ await api.delete("/me")
80
+ sessionStorage.clear()
81
+ setUser(null)
82
+ }
83
+
84
+ return (
85
+ <AuthContext.Provider value={{
86
+ user,
87
+ loading,
88
+ login,
89
+ register,
90
+ logout,
91
+ updateProfile,
92
+ deleteAccount,
93
+ verifyEmail,
94
+ forgotPassword,
95
+ resetPassword
96
+ }}>
97
+ {children}
98
+ </AuthContext.Provider>
99
+ )
100
+ }
101
+
102
+ export function useAuth() {
103
+ return useContext(AuthContext)
104
+ }
frontend/src/index.css ADDED
@@ -0,0 +1,464 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ─── Premium Glassmorphism Dark Mode Design System ─── */
2
+
3
+ :root {
4
+ --bg-primary: #0b0f19;
5
+ --bg-grid: rgba(99, 102, 241, 0.03);
6
+ --card-bg: rgba(17, 24, 39, 0.65);
7
+ --card-border: rgba(255, 255, 255, 0.08);
8
+
9
+ --primary-gradient: linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%);
10
+ --secondary-gradient: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
11
+ --danger-gradient: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
12
+ --success-gradient: linear-gradient(135deg, #10b981 0%, #059669 100%);
13
+
14
+ --text-primary: #f3f4f6;
15
+ --text-secondary: #9ca3af;
16
+ --text-muted: #6b7280;
17
+
18
+ --accent-glow: rgba(99, 102, 241, 0.3);
19
+ --accent-color: #6366f1;
20
+ --focus-ring: 0 0 0 3px rgba(99, 102, 241, 0.4);
21
+
22
+ --radius-lg: 16px;
23
+ --radius-md: 10px;
24
+ --radius-sm: 6px;
25
+
26
+ --transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
27
+ }
28
+
29
+ /* Reset and Base Styles */
30
+ * {
31
+ margin: 0;
32
+ padding: 0;
33
+ box-sizing: border-box;
34
+ font-family: 'Outfit', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
35
+ }
36
+
37
+ body {
38
+ background-color: var(--bg-primary);
39
+ background-image:
40
+ radial-gradient(circle at 10% 20%, rgba(99, 102, 241, 0.08) 0%, transparent 40%),
41
+ radial-gradient(circle at 90% 80%, rgba(139, 92, 246, 0.08) 0%, transparent 40%),
42
+ linear-gradient(to right, var(--bg-grid) 1px, transparent 1px),
43
+ linear-gradient(to bottom, var(--bg-grid) 1px, transparent 1px);
44
+ background-size: 100% 100%, 100% 100%, 40px 40px, 40px 40px;
45
+ color: var(--text-primary);
46
+ min-height: 100vh;
47
+ display: flex;
48
+ flex-direction: column;
49
+ overflow-x: hidden;
50
+ }
51
+
52
+ #root {
53
+ min-height: 100vh;
54
+ display: flex;
55
+ flex-direction: column;
56
+ flex: 1;
57
+ }
58
+
59
+ ::-webkit-scrollbar {
60
+ width: 8px;
61
+ }
62
+ ::-webkit-scrollbar-track {
63
+ background: var(--bg-primary);
64
+ }
65
+ ::-webkit-scrollbar-thumb {
66
+ background: #1f2937;
67
+ border-radius: 4px;
68
+ }
69
+ ::-webkit-scrollbar-thumb:hover {
70
+ background: #374151;
71
+ }
72
+
73
+ .page-container {
74
+ flex: 1;
75
+ display: flex;
76
+ align-items: center;
77
+ justify-content: center;
78
+ padding: 2.5rem 1.5rem;
79
+ }
80
+
81
+ .navbar {
82
+ background: rgba(11, 15, 25, 0.7);
83
+ backdrop-filter: blur(16px);
84
+ -webkit-backdrop-filter: blur(16px);
85
+ border-bottom: 1px solid var(--card-border);
86
+ padding: 1rem 2rem;
87
+ display: flex;
88
+ justify-content: space-between;
89
+ align-items: center;
90
+ position: sticky;
91
+ top: 0;
92
+ z-index: 100;
93
+ }
94
+
95
+ .nav-brand {
96
+ font-size: 1.4rem;
97
+ font-weight: 700;
98
+ background: var(--primary-gradient);
99
+ -webkit-background-clip: text;
100
+ -webkit-text-fill-color: transparent;
101
+ display: flex;
102
+ align-items: center;
103
+ gap: 8px;
104
+ text-decoration: none;
105
+ }
106
+
107
+ .nav-links {
108
+ display: flex;
109
+ gap: 1.5rem;
110
+ align-items: center;
111
+ }
112
+
113
+ .nav-link {
114
+ color: var(--text-secondary);
115
+ text-decoration: none;
116
+ font-size: 0.95rem;
117
+ font-weight: 500;
118
+ transition: var(--transition-smooth);
119
+ padding: 0.5rem 1rem;
120
+ border-radius: var(--radius-sm);
121
+ }
122
+
123
+ .nav-link:hover {
124
+ color: var(--text-primary);
125
+ background: rgba(255, 255, 255, 0.05);
126
+ }
127
+
128
+ .nav-link.active {
129
+ color: var(--accent-color);
130
+ background: rgba(99, 102, 241, 0.1);
131
+ }
132
+
133
+ .glass-card {
134
+ background: var(--card-bg);
135
+ backdrop-filter: blur(20px);
136
+ -webkit-backdrop-filter: blur(20px);
137
+ border: 1px solid var(--card-border);
138
+ border-radius: var(--radius-lg);
139
+ padding: 2.5rem;
140
+ width: 100%;
141
+ max-width: 460px;
142
+ box-shadow: 0 20px 40px -15px rgba(0, 0, 0, 0.5);
143
+ animation: fadeIn 0.4s ease-out;
144
+ }
145
+
146
+ .glass-card.wide {
147
+ max-width: 800px;
148
+ }
149
+
150
+ @keyframes fadeIn {
151
+ from {
152
+ opacity: 0;
153
+ transform: translateY(15px);
154
+ }
155
+ to {
156
+ opacity: 1;
157
+ transform: translateY(0);
158
+ }
159
+ }
160
+
161
+ h1, h2, h3 {
162
+ font-weight: 700;
163
+ letter-spacing: -0.025em;
164
+ margin-bottom: 1.5rem;
165
+ }
166
+
167
+ h2 {
168
+ font-size: 1.8rem;
169
+ background: linear-gradient(to right, #ffffff, #d1d5db);
170
+ -webkit-background-clip: text;
171
+ -webkit-text-fill-color: transparent;
172
+ }
173
+
174
+ .card-subtitle {
175
+ color: var(--text-secondary);
176
+ font-size: 0.95rem;
177
+ margin-top: -1rem;
178
+ margin-bottom: 2rem;
179
+ }
180
+
181
+ .form-group {
182
+ margin-bottom: 1.25rem;
183
+ display: flex;
184
+ flex-direction: column;
185
+ gap: 6px;
186
+ }
187
+
188
+ label {
189
+ font-size: 0.85rem;
190
+ font-weight: 600;
191
+ text-transform: uppercase;
192
+ letter-spacing: 0.05em;
193
+ color: var(--text-secondary);
194
+ }
195
+
196
+ input {
197
+ background: rgba(31, 41, 55, 0.5);
198
+ border: 1px solid var(--card-border);
199
+ border-radius: var(--radius-md);
200
+ padding: 0.8rem 1rem;
201
+ color: var(--text-primary);
202
+ font-size: 0.95rem;
203
+ transition: var(--transition-smooth);
204
+ width: 100%;
205
+ }
206
+
207
+ input:focus {
208
+ outline: none;
209
+ border-color: var(--accent-color);
210
+ box-shadow: var(--focus-ring);
211
+ background: rgba(31, 41, 55, 0.8);
212
+ }
213
+
214
+ input::placeholder {
215
+ color: var(--text-muted);
216
+ }
217
+
218
+ .btn {
219
+ background: var(--primary-gradient);
220
+ border: none;
221
+ border-radius: var(--radius-md);
222
+ color: white;
223
+ cursor: pointer;
224
+ display: inline-flex;
225
+ align-items: center;
226
+ justify-content: center;
227
+ font-weight: 600;
228
+ font-size: 0.95rem;
229
+ padding: 0.85rem 1.5rem;
230
+ transition: var(--transition-smooth);
231
+ width: 100%;
232
+ box-shadow: 0 4px 12px rgba(139, 92, 246, 0.25);
233
+ gap: 8px;
234
+ }
235
+
236
+ .btn:hover:not(:disabled) {
237
+ transform: translateY(-2px);
238
+ box-shadow: 0 6px 20px rgba(139, 92, 246, 0.4);
239
+ filter: brightness(1.1);
240
+ }
241
+
242
+ .btn:active:not(:disabled) {
243
+ transform: translateY(0);
244
+ }
245
+
246
+ .btn:disabled {
247
+ opacity: 0.6;
248
+ cursor: not-allowed;
249
+ }
250
+
251
+ .btn.btn-secondary {
252
+ background: rgba(255, 255, 255, 0.08);
253
+ border: 1px solid var(--card-border);
254
+ color: var(--text-primary);
255
+ box-shadow: none;
256
+ }
257
+
258
+ .btn.btn-secondary:hover:not(:disabled) {
259
+ background: rgba(255, 255, 255, 0.15);
260
+ box-shadow: none;
261
+ }
262
+
263
+ .btn.btn-danger {
264
+ background: var(--danger-gradient);
265
+ box-shadow: 0 4px 12px rgba(239, 68, 68, 0.25);
266
+ }
267
+
268
+ .btn.btn-danger:hover:not(:disabled) {
269
+ box-shadow: 0 6px 20px rgba(239, 68, 68, 0.4);
270
+ }
271
+
272
+ .btn-group {
273
+ display: flex;
274
+ gap: 1rem;
275
+ margin-top: 1.5rem;
276
+ }
277
+
278
+ .alert {
279
+ padding: 0.85rem 1rem;
280
+ border-radius: var(--radius-md);
281
+ font-size: 0.9rem;
282
+ font-weight: 500;
283
+ margin-bottom: 1.25rem;
284
+ display: flex;
285
+ align-items: center;
286
+ gap: 8px;
287
+ animation: shake 0.4s ease-out;
288
+ }
289
+
290
+ .alert-error {
291
+ background: rgba(239, 68, 68, 0.12);
292
+ border: 1px solid rgba(239, 68, 68, 0.3);
293
+ color: #fca5a5;
294
+ }
295
+
296
+ .alert-success {
297
+ background: rgba(16, 185, 129, 0.12);
298
+ border: 1px solid rgba(16, 185, 129, 0.3);
299
+ color: #a7f3d0;
300
+ }
301
+
302
+ .alert-info {
303
+ background: rgba(59, 130, 246, 0.12);
304
+ border: 1px solid rgba(59, 130, 246, 0.3);
305
+ color: #93c5fd;
306
+ }
307
+
308
+ @keyframes shake {
309
+ 0%, 100% { transform: translateX(0); }
310
+ 25% { transform: translateX(-4px); }
311
+ 75% { transform: translateX(4px); }
312
+ }
313
+
314
+ .footer-text {
315
+ text-align: center;
316
+ font-size: 0.9rem;
317
+ color: var(--text-secondary);
318
+ margin-top: 1.5rem;
319
+ }
320
+
321
+ .footer-text a {
322
+ color: #8b5cf6;
323
+ text-decoration: none;
324
+ font-weight: 600;
325
+ transition: var(--transition-smooth);
326
+ }
327
+
328
+ .footer-text a:hover {
329
+ text-decoration: underline;
330
+ color: #a78bfa;
331
+ }
332
+
333
+ .dashboard-grid {
334
+ display: grid;
335
+ grid-template-columns: 1fr;
336
+ gap: 2rem;
337
+ }
338
+
339
+ @media (min-width: 768px) {
340
+ .dashboard-grid {
341
+ grid-template-columns: 2fr 1fr;
342
+ }
343
+ }
344
+
345
+ .info-table {
346
+ width: 100%;
347
+ border-collapse: collapse;
348
+ margin-bottom: 1.5rem;
349
+ }
350
+
351
+ .info-table tr {
352
+ border-bottom: 1px solid var(--card-border);
353
+ }
354
+
355
+ .info-table tr:last-child {
356
+ border-bottom: none;
357
+ }
358
+
359
+ .info-table td {
360
+ padding: 0.9rem 0;
361
+ font-size: 0.95rem;
362
+ }
363
+
364
+ .info-table td.label {
365
+ font-weight: 600;
366
+ color: var(--text-secondary);
367
+ width: 35%;
368
+ }
369
+
370
+ .info-table td.value {
371
+ color: var(--text-primary);
372
+ word-break: break-all;
373
+ }
374
+
375
+ .status-badge {
376
+ display: inline-flex;
377
+ align-items: center;
378
+ padding: 0.2rem 0.6rem;
379
+ border-radius: 20px;
380
+ font-size: 0.75rem;
381
+ font-weight: 700;
382
+ text-transform: uppercase;
383
+ }
384
+
385
+ .status-badge.active {
386
+ background: rgba(16, 185, 129, 0.15);
387
+ color: #34d399;
388
+ border: 1px solid rgba(16, 185, 129, 0.3);
389
+ }
390
+
391
+ .users-list {
392
+ max-height: 250px;
393
+ overflow-y: auto;
394
+ margin-top: 1rem;
395
+ display: flex;
396
+ flex-direction: column;
397
+ gap: 10px;
398
+ padding-right: 5px;
399
+ }
400
+
401
+ .user-item {
402
+ background: rgba(255, 255, 255, 0.03);
403
+ border: 1px solid var(--card-border);
404
+ padding: 0.75rem 1rem;
405
+ border-radius: var(--radius-md);
406
+ display: flex;
407
+ justify-content: space-between;
408
+ align-items: center;
409
+ }
410
+
411
+ .user-item-info {
412
+ display: flex;
413
+ flex-direction: column;
414
+ }
415
+
416
+ .user-item-name {
417
+ font-weight: 600;
418
+ font-size: 0.95rem;
419
+ }
420
+
421
+ .user-item-email {
422
+ color: var(--text-secondary);
423
+ font-size: 0.8rem;
424
+ }
425
+
426
+ .otp-container {
427
+ display: flex;
428
+ justify-content: center;
429
+ gap: 10px;
430
+ margin: 1.5rem 0;
431
+ }
432
+
433
+ .otp-box {
434
+ width: 48px;
435
+ height: 48px;
436
+ font-size: 1.5rem;
437
+ font-weight: 700;
438
+ text-align: center;
439
+ background: rgba(31, 41, 55, 0.6);
440
+ }
441
+
442
+ .simulation-log-box {
443
+ margin-top: 1.5rem;
444
+ background: #090d16;
445
+ border: 1px dashed rgba(99, 102, 241, 0.3);
446
+ border-radius: var(--radius-md);
447
+ padding: 1rem;
448
+ font-family: 'Courier New', Courier, monospace;
449
+ }
450
+
451
+ .simulation-log-title {
452
+ font-size: 0.75rem;
453
+ font-weight: 700;
454
+ color: #818cf8;
455
+ margin-bottom: 0.5rem;
456
+ text-transform: uppercase;
457
+ }
458
+
459
+ .simulation-log-content {
460
+ font-size: 0.85rem;
461
+ color: #a7f3d0;
462
+ white-space: pre-wrap;
463
+ word-break: break-all;
464
+ }
frontend/src/main.jsx ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import { BrowserRouter } from 'react-router-dom'
4
+ import App from './App'
5
+ import './index.css'
6
+
7
+ ReactDOM.createRoot(document.getElementById('root')).render(
8
+ <React.StrictMode>
9
+ <BrowserRouter>
10
+ <App />
11
+ </BrowserRouter>
12
+ </React.StrictMode>,
13
+ )
frontend/src/pages/Dashboard.jsx ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from "react"
2
+ import { useAuth } from "../context/AuthContext"
3
+ import api from "../api/axios"
4
+
5
+ export default function Dashboard() {
6
+ const { user, updateProfile, deleteAccount } = useAuth()
7
+ const [isEditing, setIsEditing] = useState(false)
8
+ const [profileForm, setProfileForm] = useState({ username: user?.username || "", email: user?.email || "", password: "" })
9
+ const [error, setError] = useState("")
10
+ const [success, setSuccess] = useState("")
11
+ const [loading, setLoading] = useState(false)
12
+ const [adminUsers, setAdminUsers] = useState([])
13
+ const [adminError, setAdminError] = useState("")
14
+
15
+ useEffect(() => {
16
+ if (user?.role === "admin") {
17
+ api.get("/admin/users")
18
+ .then(({ data }) => setAdminUsers(data))
19
+ .catch(err => setAdminError(err.response?.data?.detail || "Could not load user list."))
20
+ }
21
+ }, [user])
22
+
23
+ function handleFormChange(e) {
24
+ setProfileForm({ ...profileForm, [e.target.name]: e.target.value })
25
+ }
26
+
27
+ async function handleProfileUpdate(e) {
28
+ e.preventDefault()
29
+ setError("")
30
+ setSuccess("")
31
+ setLoading(true)
32
+
33
+ const payload = {}
34
+ if (profileForm.username !== user.username) payload.username = profileForm.username
35
+ if (profileForm.email !== user.email) payload.email = profileForm.email
36
+ if (profileForm.password) payload.password = profileForm.password
37
+
38
+ if (Object.keys(payload).length === 0) {
39
+ setError("No changes detected.")
40
+ setLoading(false)
41
+ setIsEditing(false)
42
+ return
43
+ }
44
+
45
+ try {
46
+ await updateProfile(payload)
47
+ setSuccess("Profile updated successfully!")
48
+ setIsEditing(false)
49
+ setProfileForm(prev => ({ ...prev, password: "" }))
50
+ } catch (err) {
51
+ setError(err.response?.data?.detail || "Update failed. Please check inputs.")
52
+ } finally {
53
+ setLoading(false)
54
+ }
55
+ }
56
+
57
+ async function handleDeleteAccount() {
58
+ const confirmDelete = window.confirm(
59
+ "Warning: Are you absolutely sure you want to delete your account permanently? This action cannot be undone."
60
+ )
61
+ if (!confirmDelete) return
62
+
63
+ try {
64
+ await deleteAccount()
65
+ } catch (err) {
66
+ setError("Failed to delete account. Please try again.")
67
+ }
68
+ }
69
+
70
+ return (
71
+ <div className="page-container">
72
+ <div className="glass-card wide">
73
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", borderBottom: "1px solid var(--card-border)", paddingBottom: "1rem", marginBottom: "1.5rem" }}>
74
+ <div>
75
+ <h2>User Dashboard</h2>
76
+ <p className="card-subtitle" style={{ margin: 0 }}>Manage your session, roles, and profile settings</p>
77
+ </div>
78
+ <span className="status-badge active">Active Session</span>
79
+ </div>
80
+
81
+ {error && <div className="alert alert-error">{error}</div>}
82
+ {success && <div className="alert alert-success">{success}</div>}
83
+
84
+ <div className="dashboard-grid">
85
+ <div>
86
+ <h3 style={{ fontSize: "1.2rem", marginBottom: "1rem", color: "var(--accent-color)" }}>Profile Details</h3>
87
+
88
+ {!isEditing ? (
89
+ <>
90
+ <table className="info-table">
91
+ <tbody>
92
+ <tr>
93
+ <td className="label">Username</td>
94
+ <td className="value">{user?.username}</td>
95
+ </tr>
96
+ <tr>
97
+ <td className="label">Email Address</td>
98
+ <td className="value">{user?.email}</td>
99
+ </tr>
100
+ <tr>
101
+ <td className="label">User ID</td>
102
+ <td className="value">#{user?.id}</td>
103
+ </tr>
104
+ <tr>
105
+ <td className="label">Account Role</td>
106
+ <td className="value" style={{ textTransform: "capitalize" }}>{user?.role}</td>
107
+ </tr>
108
+ </tbody>
109
+ </table>
110
+
111
+ <div style={{ display: "flex", gap: "10px" }}>
112
+ <button
113
+ className="btn"
114
+ style={{ width: "auto" }}
115
+ onClick={() => {
116
+ setProfileForm({ username: user.username, email: user.email, password: "" })
117
+ setIsEditing(true)
118
+ }}
119
+ >
120
+ Edit Profile
121
+ </button>
122
+ <button className="btn btn-danger" style={{ width: "auto" }} onClick={handleDeleteAccount}>
123
+ Delete Account
124
+ </button>
125
+ </div>
126
+ </>
127
+ ) : (
128
+ <form onSubmit={handleProfileUpdate}>
129
+ <div className="form-group">
130
+ <label htmlFor="edit-username">Username</label>
131
+ <input
132
+ id="edit-username"
133
+ name="username"
134
+ type="text"
135
+ value={profileForm.username}
136
+ onChange={handleFormChange}
137
+ required
138
+ />
139
+ </div>
140
+ <div className="form-group">
141
+ <label htmlFor="edit-email">Email Address</label>
142
+ <input
143
+ id="edit-email"
144
+ name="email"
145
+ type="email"
146
+ value={profileForm.email}
147
+ onChange={handleFormChange}
148
+ required
149
+ />
150
+ </div>
151
+ <div className="form-group">
152
+ <label htmlFor="edit-password">New Password (optional)</label>
153
+ <input
154
+ id="edit-password"
155
+ name="password"
156
+ type="password"
157
+ value={profileForm.password}
158
+ onChange={handleFormChange}
159
+ placeholder="Leave blank to keep same"
160
+ />
161
+ </div>
162
+
163
+ <div className="btn-group">
164
+ <button className="btn btn-secondary" type="button" onClick={() => setIsEditing(false)} disabled={loading}>
165
+ Cancel
166
+ </button>
167
+ <button className="btn" type="submit" disabled={loading}>
168
+ {loading ? "Saving..." : "Save Changes"}
169
+ </button>
170
+ </div>
171
+ </form>
172
+ )}
173
+ </div>
174
+
175
+ <div style={{ borderLeft: "1px solid var(--card-border)", paddingLeft: "1.5rem" }}>
176
+ <h3 style={{ fontSize: "1.2rem", marginBottom: "1rem" }}>System Panel</h3>
177
+ {user?.role === "admin" ? (
178
+ <div>
179
+ <p style={{ fontSize: "0.85rem", color: "var(--text-secondary)", marginBottom: "0.5rem" }}>
180
+ Admin Authorization Verified. Total users database list:
181
+ </p>
182
+ {adminError && <p className="alert alert-error" style={{ padding: "0.5rem" }}>{adminError}</p>}
183
+
184
+ <div className="users-list">
185
+ {adminUsers.map(u => (
186
+ <div className="user-item" key={u.id}>
187
+ <div className="user-item-info">
188
+ <span className="user-item-name">{u.username}</span>
189
+ <span className="user-item-email">{u.email}</span>
190
+ </div>
191
+ <span style={{ fontSize: "0.75rem", background: "rgba(255,255,255,0.05)", padding: "2px 6px", borderRadius: "4px" }}>
192
+ {u.role}
193
+ </span>
194
+ </div>
195
+ ))}
196
+ </div>
197
+ </div>
198
+ ) : (
199
+ <div>
200
+ <p style={{ fontSize: "0.85rem", color: "var(--text-muted)", lineHeight: "1.4" }}>
201
+ Regular user account permissions detected. Admin features (such as user administration list checks) are disabled.
202
+ </p>
203
+ <div style={{ marginTop: "1rem", padding: "0.75rem", border: "1px dashed #374151", borderRadius: "8px", background: "rgba(0,0,0,0.1)" }}>
204
+ <span style={{ fontSize: "0.75rem", fontWeight: "700", color: "#6b7280", display: "block" }}>ADMIN ACCESS REQUIRED</span>
205
+ <span style={{ fontSize: "0.75rem", color: "#4b5563" }}>Route `GET /admin/users` will throw 403 Forbidden for this account.</span>
206
+ </div>
207
+ </div>
208
+ )}
209
+ </div>
210
+ </div>
211
+
212
+ </div>
213
+ </div>
214
+ )
215
+ }
frontend/src/pages/Login.jsx ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react"
2
+ import { useAuth } from "../context/AuthContext"
3
+ import { useNavigate, Link, useLocation } from "react-router-dom"
4
+
5
+ export default function Login() {
6
+ const { login, forgotPassword, resetPassword } = useAuth()
7
+ const navigate = useNavigate()
8
+ const location = useLocation()
9
+
10
+ // Normal Login
11
+ const [form, setForm] = useState({ username: "", password: "" })
12
+ const [unverifiedEmail, setUnverifiedEmail] = useState("")
13
+
14
+ // Forgot Password Steps
15
+ const [forgotMode, setForgotMode] = useState(false)
16
+ const [forgotStep, setForgotStep] = useState(1) // 1 = enter email, 2 = enter code and new password
17
+ const [resetEmail, setResetEmail] = useState("")
18
+ const [resetCode, setResetCode] = useState("")
19
+ const [newPassword, setNewPassword] = useState("")
20
+
21
+ const [error, setError] = useState("")
22
+ const [successMsg, setSuccessMsg] = useState(location.state?.success || "")
23
+ const [loading, setLoading] = useState(false)
24
+
25
+ function handleChange(e) {
26
+ setForm({ ...form, [e.target.name]: e.target.value })
27
+ }
28
+
29
+ async function handleSubmit(e) {
30
+ e.preventDefault()
31
+ setError("")
32
+ setSuccessMsg("")
33
+ setUnverifiedEmail("")
34
+ setLoading(true)
35
+ try {
36
+ await login(form.username, form.password)
37
+ navigate("/dashboard")
38
+ } catch (err) {
39
+ const errMsg = err.response?.data?.detail || "Invalid username or password."
40
+ setError(errMsg)
41
+
42
+ // If the backend indicates unverified email, extract email/username if possible
43
+ if (errMsg.includes("not verified")) {
44
+ // We'll allow the user to click a link to verify
45
+ setUnverifiedEmail(form.username) // Username is typed in, can redirect
46
+ }
47
+ } finally {
48
+ setLoading(false)
49
+ }
50
+ }
51
+
52
+ // Step 1: Send OTP to Email
53
+ async function handleForgotRequest(e) {
54
+ e.preventDefault()
55
+ setError("")
56
+ setSuccessMsg("")
57
+ setLoading(true)
58
+ try {
59
+ await forgotPassword(resetEmail)
60
+ setSuccessMsg("Verification code sent to your email!")
61
+ setForgotStep(2) // Move to enter code step
62
+ } catch (err) {
63
+ setError(err.response?.data?.detail || "Email address not found.")
64
+ } finally {
65
+ setLoading(false)
66
+ }
67
+ }
68
+
69
+ // Step 2: Verify OTP and Reset Password
70
+ async function handleResetSubmit(e) {
71
+ e.preventDefault()
72
+ setError("")
73
+ setSuccessMsg("")
74
+ setLoading(true)
75
+ try {
76
+ await resetPassword(resetEmail, resetCode, newPassword)
77
+ setSuccessMsg("Password reset successfully! You can now sign in.")
78
+ setForgotMode(false)
79
+ setForgotStep(1)
80
+ setResetEmail("")
81
+ setResetCode("")
82
+ setNewPassword("")
83
+ } catch (err) {
84
+ setError(err.response?.data?.detail || "Password reset failed. Invalid code.")
85
+ } finally {
86
+ setLoading(false)
87
+ }
88
+ }
89
+
90
+ return (
91
+ <div className="page-container">
92
+ <div className="glass-card">
93
+ {!forgotMode ? (
94
+ <>
95
+ <h2>Sign In</h2>
96
+ <p className="card-subtitle">Enter your credentials to access your dashboard</p>
97
+
98
+ {error && <div className="alert alert-error">{error}</div>}
99
+ {successMsg && <div className="alert alert-success">{successMsg}</div>}
100
+
101
+ {unverifiedEmail && (
102
+ <div style={{ marginBottom: "1.25rem", textAlign: "center" }}>
103
+ <Link
104
+ to={`/verify-email?email=${encodeURIComponent(unverifiedEmail)}`}
105
+ className="btn btn-secondary"
106
+ style={{ fontSize: "0.85rem", padding: "0.5rem 1rem" }}
107
+ >
108
+ Verify Your Email Now
109
+ </Link>
110
+ </div>
111
+ )}
112
+
113
+ <form onSubmit={handleSubmit}>
114
+ <div className="form-group">
115
+ <label htmlFor="username">Username</label>
116
+ <input
117
+ id="username"
118
+ name="username"
119
+ type="text"
120
+ value={form.username}
121
+ onChange={handleChange}
122
+ placeholder="Enter your username"
123
+ required
124
+ />
125
+ </div>
126
+
127
+ <div className="form-group">
128
+ <label htmlFor="password">Password</label>
129
+ <input
130
+ id="password"
131
+ name="password"
132
+ type="password"
133
+ value={form.password}
134
+ onChange={handleChange}
135
+ placeholder="Enter your password"
136
+ required
137
+ />
138
+ </div>
139
+
140
+ <div style={{ textAlign: "right", marginBottom: "1.5rem" }}>
141
+ <button
142
+ type="button"
143
+ style={{ background: "none", border: "none", color: "#8b5cf6", cursor: "pointer", fontSize: "0.85rem", fontWeight: "500" }}
144
+ onClick={() => { setForgotMode(true); setError(""); setForgotStep(1); }}
145
+ >
146
+ Forgot Password?
147
+ </button>
148
+ </div>
149
+
150
+ <button className="btn" type="submit" disabled={loading}>
151
+ {loading ? "Signing in..." : "Sign In"}
152
+ </button>
153
+ </form>
154
+
155
+ <p className="footer-text">
156
+ Don't have an account? <Link to="/register">Register here</Link>
157
+ </p>
158
+ </>
159
+ ) : (
160
+ <>
161
+ <h2>Reset Password</h2>
162
+ <p className="card-subtitle">Recover your account using email verification</p>
163
+
164
+ {error && <div className="alert alert-error">{error}</div>}
165
+ {successMsg && <div className="alert alert-success">{successMsg}</div>}
166
+
167
+ {forgotStep === 1 ? (
168
+ <form onSubmit={handleForgotRequest}>
169
+ <div className="form-group">
170
+ <label htmlFor="reset-email">Email Address</label>
171
+ <input
172
+ id="reset-email"
173
+ type="email"
174
+ value={resetEmail}
175
+ onChange={(e) => setResetEmail(e.target.value)}
176
+ placeholder="Enter your registered email"
177
+ required
178
+ />
179
+ </div>
180
+
181
+ <div className="btn-group">
182
+ <button
183
+ className="btn btn-secondary"
184
+ type="button"
185
+ onClick={() => { setForgotMode(false); setError(""); }}
186
+ disabled={loading}
187
+ >
188
+ Cancel
189
+ </button>
190
+ <button className="btn" type="submit" disabled={loading}>
191
+ {loading ? "Sending..." : "Request Reset Code"}
192
+ </button>
193
+ </div>
194
+ </form>
195
+ ) : (
196
+ <form onSubmit={handleResetSubmit}>
197
+ <div className="form-group">
198
+ <label htmlFor="reset-code">Verification Code</label>
199
+ <input
200
+ id="reset-code"
201
+ type="text"
202
+ maxLength={6}
203
+ value={resetCode}
204
+ onChange={(e) => setResetCode(e.target.value.replace(/\D/g, ""))}
205
+ placeholder="6-digit OTP code"
206
+ style={{ textAlign: "center", letterSpacing: "3px", fontWeight: "700" }}
207
+ required
208
+ />
209
+ </div>
210
+
211
+ <div className="form-group">
212
+ <label htmlFor="reset-new-password">New Password</label>
213
+ <input
214
+ id="reset-new-password"
215
+ type="password"
216
+ value={newPassword}
217
+ onChange={(e) => setNewPassword(e.target.value)}
218
+ placeholder="At least 8 chars + 1 letter + 1 number"
219
+ required
220
+ />
221
+ </div>
222
+
223
+ <div className="btn-group">
224
+ <button
225
+ className="btn btn-secondary"
226
+ type="button"
227
+ onClick={() => { setForgotStep(1); setError(""); }}
228
+ disabled={loading}
229
+ >
230
+ Back
231
+ </button>
232
+ <button className="btn" type="submit" disabled={loading}>
233
+ {loading ? "Resetting..." : "Submit Reset"}
234
+ </button>
235
+ </div>
236
+ </form>
237
+ )}
238
+ </>
239
+ )}
240
+ </div>
241
+ </div>
242
+ )
243
+ }
frontend/src/pages/Register.jsx ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react"
2
+ import { useAuth } from "../context/AuthContext"
3
+ import { useNavigate, Link } from "react-router-dom"
4
+
5
+ export default function Register() {
6
+ const { register } = useAuth()
7
+ const navigate = useNavigate()
8
+ const [form, setForm] = useState({ username: "", email: "", password: "" })
9
+ const [error, setError] = useState("")
10
+ const [loading, setLoading] = useState(false)
11
+
12
+ function handleChange(e) {
13
+ setForm({ ...form, [e.target.name]: e.target.value })
14
+ }
15
+
16
+ async function handleSubmit(e) {
17
+ e.preventDefault()
18
+ setError("")
19
+ setLoading(true)
20
+
21
+ if (form.password.length < 8) {
22
+ setError("Password must be at least 8 characters long.")
23
+ setLoading(false)
24
+ return
25
+ }
26
+
27
+ try {
28
+ await register(form.username, form.email, form.password)
29
+ // Redirect straight to verification page with email parameter in URL query
30
+ navigate(`/verify-email?email=${encodeURIComponent(form.email)}`)
31
+ } catch (err) {
32
+ setError(err.response?.data?.detail || "Registration failed. Please check credentials.")
33
+ } finally {
34
+ setLoading(false)
35
+ }
36
+ }
37
+
38
+ return (
39
+ <div className="page-container">
40
+ <div className="glass-card">
41
+ <h2>Create Account</h2>
42
+ <p className="card-subtitle">Sign up now and verify your email to access the dashboard</p>
43
+
44
+ {error && <div className="alert alert-error">{error}</div>}
45
+
46
+ <form onSubmit={handleSubmit}>
47
+ <div className="form-group">
48
+ <label htmlFor="username">Username</label>
49
+ <input
50
+ id="username"
51
+ name="username"
52
+ type="text"
53
+ value={form.username}
54
+ onChange={handleChange}
55
+ placeholder="Choose a username"
56
+ required
57
+ />
58
+ </div>
59
+
60
+ <div className="form-group">
61
+ <label htmlFor="email">Email Address</label>
62
+ <input
63
+ id="email"
64
+ name="email"
65
+ type="email"
66
+ value={form.email}
67
+ onChange={handleChange}
68
+ placeholder="name@example.com"
69
+ required
70
+ />
71
+ </div>
72
+
73
+ <div className="form-group">
74
+ <label htmlFor="password">Password</label>
75
+ <input
76
+ id="password"
77
+ name="password"
78
+ type="password"
79
+ value={form.password}
80
+ onChange={handleChange}
81
+ placeholder="At least 8 chars + 1 letter + 1 number"
82
+ required
83
+ />
84
+ </div>
85
+
86
+ <button className="btn" type="submit" disabled={loading} style={{ marginTop: "1rem" }}>
87
+ {loading ? "Registering account..." : "Register"}
88
+ </button>
89
+ </form>
90
+
91
+ <p className="footer-text">
92
+ Already have an account? <Link to="/login">Sign in</Link>
93
+ </p>
94
+ </div>
95
+ </div>
96
+ )
97
+ }
frontend/src/pages/VerifyEmail.jsx ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from "react"
2
+ import { useAuth } from "../context/AuthContext"
3
+ import { useNavigate, useSearchParams } from "react-router-dom"
4
+
5
+ export default function VerifyEmail() {
6
+ const { verifyEmail } = useAuth()
7
+ const navigate = useNavigate()
8
+ const [searchParams] = useSearchParams()
9
+
10
+ const [email, setEmail] = useState("")
11
+ const [code, setCode] = useState("")
12
+ const [error, setError] = useState("")
13
+ const [loading, setLoading] = useState(false)
14
+
15
+ useEffect(() => {
16
+ const emailParam = searchParams.get("email")
17
+ if (emailParam) {
18
+ setEmail(emailParam)
19
+ }
20
+ }, [searchParams])
21
+
22
+ async function handleSubmit(e) {
23
+ e.preventDefault()
24
+ setError("")
25
+ setLoading(true)
26
+
27
+ if (code.length !== 6) {
28
+ setError("Please enter a valid 6-digit verification code.")
29
+ setLoading(false)
30
+ return
31
+ }
32
+
33
+ try {
34
+ await verifyEmail(email, code)
35
+ navigate("/login", {
36
+ state: { success: "Email verified successfully! You can now sign in." }
37
+ })
38
+ } catch (err) {
39
+ setError(err.response?.data?.detail || "Verification failed. Please check the code.")
40
+ } finally {
41
+ setLoading(false)
42
+ }
43
+ }
44
+
45
+ return (
46
+ <div className="page-container">
47
+ <div className="glass-card">
48
+ <h2>Verify Email</h2>
49
+ <p className="card-subtitle">
50
+ We sent a 6-digit verification code. Please check your email inbox (or FastAPI terminal logs).
51
+ </p>
52
+
53
+ {error && <div className="alert alert-error">{error}</div>}
54
+
55
+ <form onSubmit={handleSubmit}>
56
+ <div className="form-group">
57
+ <label htmlFor="verify-email">Email Address</label>
58
+ <input
59
+ id="verify-email"
60
+ type="email"
61
+ value={email}
62
+ onChange={(e) => setEmail(e.target.value)}
63
+ placeholder="name@example.com"
64
+ required
65
+ />
66
+ </div>
67
+
68
+ <div className="form-group">
69
+ <label htmlFor="otp-code">Verification Code</label>
70
+ <input
71
+ id="otp-code"
72
+ type="text"
73
+ maxLength={6}
74
+ value={code}
75
+ onChange={(e) => setCode(e.target.value.replace(/\D/g, ""))}
76
+ placeholder="Enter 6-digit OTP code"
77
+ style={{ letterSpacing: "4px", textAlign: "center", fontSize: "1.2rem", fontWeight: "700" }}
78
+ required
79
+ />
80
+ </div>
81
+
82
+ <button className="btn" type="submit" disabled={loading} style={{ marginTop: "1rem" }}>
83
+ {loading ? "Verifying..." : "Verify & Activate"}
84
+ </button>
85
+ </form>
86
+ </div>
87
+ </div>
88
+ )
89
+ }
frontend/vite.config.js ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: {
7
+ port: 5173,
8
+ host: true
9
+ }
10
+ })
hld.md ADDED
@@ -0,0 +1,768 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Auth-System-SMTP β€” High-Level Design (HLD)
2
+
3
+ ---
4
+
5
+ ## 1. System Overview
6
+
7
+ ```
8
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
9
+ β”‚ CLIENT (Browser) β”‚
10
+ β”‚ React 18 + Vite (Port 5173) β”‚
11
+ β”‚ β”‚
12
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
13
+ β”‚ β”‚ Register β”‚ β”‚ Login β”‚ β”‚ Verify β”‚ β”‚ Dashboard β”‚ β”‚
14
+ β”‚ β”‚ Page β”‚ β”‚ Page β”‚ β”‚ Email β”‚ β”‚ (Profile/Admin) β”‚ β”‚
15
+ β”‚ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
16
+ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚
17
+ β”‚ β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
18
+ β”‚ β”‚ AuthContext (State) β”‚ β”‚
19
+ β”‚ β”‚ + Axios Interceptors (Token) β”‚ β”‚
20
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
21
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
22
+ β”‚ HTTP/HTTPS
23
+ β”‚ Authorization: Bearer <token>
24
+ β–Ό
25
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
26
+ β”‚ API SERVER (FastAPI) β”‚
27
+ β”‚ (Port 8000, Uvicorn) β”‚
28
+ β”‚ β”‚
29
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
30
+ β”‚ β”‚ Middleware Layer β”‚ β”‚
31
+ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚
32
+ β”‚ β”‚ β”‚ CORS β”‚ β”‚ Rate Limiter β”‚ β”‚ Request Logger β”‚ β”‚ β”‚
33
+ β”‚ β”‚ β”‚ Middlewareβ”‚ β”‚ (60 req/min) β”‚ β”‚ (IP, method, path) β”‚ β”‚ β”‚
34
+ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
35
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
36
+ β”‚ β”‚
37
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
38
+ β”‚ β”‚ Route Handlers β”‚ β”‚
39
+ β”‚ β”‚ β”‚ β”‚
40
+ β”‚ β”‚ Auth Routes (/auth) User Routes (/me, /admin) β”‚ β”‚
41
+ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚
42
+ β”‚ β”‚ β”‚ register β”‚ β”‚ GET /me β”‚ β”‚ β”‚
43
+ β”‚ β”‚ β”‚ verify-email β”‚ β”‚ PUT /me β”‚ β”‚ β”‚
44
+ β”‚ β”‚ β”‚ login β”‚ β”‚ DELETE /me β”‚ β”‚ β”‚
45
+ β”‚ β”‚ β”‚ forgot-password β”‚ β”‚ GET /admin/users β”‚ β”‚ β”‚
46
+ β”‚ β”‚ β”‚ reset-password β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
47
+ β”‚ β”‚ β”‚ refresh β”‚ β”‚ β”‚
48
+ β”‚ β”‚ β”‚ logout β”‚ β”‚ β”‚
49
+ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
50
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
51
+ β”‚ β”‚
52
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
53
+ β”‚ β”‚ Core Services β”‚ β”‚
54
+ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚
55
+ β”‚ β”‚ β”‚ Auth β”‚ β”‚ Config β”‚ β”‚ Database Manager β”‚ β”‚ β”‚
56
+ β”‚ β”‚ β”‚ Service β”‚ β”‚ Loader β”‚ β”‚ (SQLAlchemy Engine) β”‚ β”‚ β”‚
57
+ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚
58
+ β”‚ β”‚ β”‚ bcrypt β”‚ β”‚ .env β”‚ β”‚ SQLite3 Connection β”‚ β”‚ β”‚
59
+ β”‚ β”‚ β”‚ JWT β”‚ β”‚ variables β”‚ β”‚ Session Factory β”‚ β”‚ β”‚
60
+ β”‚ β”‚ β”‚ SMTP Send β”‚ β”‚ β”‚ β”‚ Base (ORM) β”‚ β”‚ β”‚
61
+ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
62
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
63
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
64
+ β”‚ β”‚
65
+ β–Ό β–Ό
66
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
67
+ β”‚ SMTP SERVER β”‚ β”‚ SQLite Database β”‚
68
+ β”‚ (Gmail / Console) β”‚ β”‚ (users.db) β”‚
69
+ β”‚ β”‚ β”‚ β”‚
70
+ β”‚ smtp.gmail.com β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
71
+ β”‚ Port 587 (STARTTLS) β”‚ β”‚ β”‚ users β”‚ β”‚revoked_tokensβ”‚ β”‚
72
+ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
73
+ β”‚ Sends OTP emails: β”‚ β”‚ β”‚
74
+ β”‚ - Email Verify β”‚ β”‚ Tables: β”‚
75
+ β”‚ - Password Reset β”‚ β”‚ - users β”‚
76
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ - revoked_tokens β”‚
77
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
78
+ ```
79
+
80
+ ---
81
+
82
+ ## 2. Component Architecture
83
+
84
+ ### 2.1 Backend Components
85
+
86
+ ```
87
+ backend/
88
+ β”œβ”€β”€ main.py ← FastAPI app, CORS, rate limiting, middleware
89
+ β”œβ”€β”€ config.py ← Loads .env variables via python-dotenv
90
+ β”œβ”€β”€ database.py ← SQLAlchemy engine + session + Base
91
+ β”œβ”€β”€ models.py ← ORM models: User, RevokedToken
92
+ β”œβ”€β”€ schemas.py ← Pydantic request/response schemas
93
+ β”œβ”€β”€ auth.py ← Password hashing, JWT, SMTP email
94
+ β”œβ”€β”€ routes/
95
+ β”‚ β”œβ”€β”€ auth_routes.py ← /auth endpoints (register, login, etc.)
96
+ β”‚ └── user_routes.py ← /me and /admin endpoints
97
+ └── .env ← Environment variables (secrets)
98
+ ```
99
+
100
+ **Data Flow:**
101
+ ```
102
+ Request β†’ Middleware (CORS, Rate Limit, Logging)
103
+ β†’ Route Handler (auth_routes / user_routes)
104
+ β†’ Auth Service (validate token, hash password, send email)
105
+ β†’ Database (SQLAlchemy ORM β†’ SQLite)
106
+ β†’ Response (JSON)
107
+ ```
108
+
109
+ ### 2.2 Frontend Components
110
+
111
+ ```
112
+ frontend/
113
+ β”œβ”€β”€ src/
114
+ β”‚ β”œβ”€β”€ main.jsx ← Entry point, BrowserRouter
115
+ β”‚ β”œβ”€β”€ App.jsx ← Root component, all routes
116
+ β”‚ β”œβ”€β”€ index.css ← Global styles (glassmorphism)
117
+ β”‚ β”œβ”€β”€ api/
118
+ β”‚ β”‚ └── axios.js ← Axios instance + interceptors
119
+ β”‚ β”œβ”€β”€ context/
120
+ β”‚ β”‚ └── AuthContext.jsx ← Auth state management
121
+ β”‚ β”œβ”€β”€ components/
122
+ β”‚ β”‚ β”œβ”€β”€ Navbar.jsx ← Navigation bar
123
+ β”‚ β”‚ └── ProtectedRoute.jsx ← Route guard
124
+ β”‚ └── pages/
125
+ β”‚ β”œβ”€β”€ Register.jsx ← Registration form
126
+ β”‚ β”œβ”€β”€ Login.jsx ← Login form
127
+ β”‚ β”œβ”€β”€ VerifyEmail.jsx ← OTP verification form
128
+ β”‚ └── Dashboard.jsx ← User dashboard + admin panel
129
+ ```
130
+
131
+ **Component Hierarchy:**
132
+ ```
133
+ <App>
134
+ β”œβ”€β”€ <AuthProvider> (Context)
135
+ β”‚ β”œβ”€β”€ <Navbar />
136
+ β”‚ β”œβ”€β”€ <Routes>
137
+ β”‚ β”‚ β”œβ”€β”€ /register β†’ <Register />
138
+ β”‚ β”‚ β”œβ”€β”€ /login β†’ <Login />
139
+ β”‚ β”‚ β”œβ”€β”€ /verify-email β†’ <VerifyEmail />
140
+ β”‚ β”‚ └── /dashboard β†’ <ProtectedRoute> β†’ <Dashboard />
141
+ β”‚ β”‚ └── / β†’ <Navigate to="/login" />
142
+ ```
143
+
144
+ ---
145
+
146
+ ## 3. Authentication Flow Diagrams
147
+
148
+ ### 3.1 Registration Flow
149
+
150
+ ```
151
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
152
+ β”‚ Client β”‚ β”‚ Server β”‚ β”‚ SMTP β”‚
153
+ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜
154
+ β”‚ β”‚ β”‚
155
+ β”‚ POST /auth/register β”‚ β”‚
156
+ β”‚ { username, email, password } β”‚ β”‚
157
+ │────────────────────────────────────────▢│ β”‚
158
+ β”‚ β”‚ β”‚
159
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚
160
+ β”‚ β”‚ Validate: β”‚ β”‚
161
+ β”‚ β”‚ - Email len β”‚ β”‚
162
+ β”‚ β”‚ - Password β”‚ β”‚
163
+ β”‚ β”‚ strength β”‚ β”‚
164
+ β”‚ β”‚ - Uniquenessβ”‚ β”‚
165
+ β”‚ └────────────── β”‚
166
+ β”‚ β”‚ β”‚
167
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚
168
+ β”‚ β”‚ bcrypt.hash β”‚ β”‚
169
+ β”‚ β”‚ + OTP gen β”‚ β”‚
170
+ β”‚ β”‚ + Create β”‚ β”‚
171
+ β”‚ β”‚ User β”‚ β”‚
172
+ β”‚ └────────────── β”‚
173
+ β”‚ β”‚ β”‚
174
+ β”‚ β”‚ Send OTP via SMTP β”‚
175
+ β”‚ │──────────────────────────────▢│
176
+ β”‚ β”‚ β”‚
177
+ β”‚ 200 OK (redirect to /verify-email) β”‚ β”‚
178
+ │◀────────────────────────────────────────│ β”‚
179
+ β”‚ β”‚ β”‚
180
+ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
181
+ β”‚ β”‚ β”‚ Console fallback: β”‚ β”‚
182
+ β”‚ β”‚ β”‚ Print OTP if SMTP failsβ”‚ β”‚
183
+ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
184
+ ```
185
+
186
+ ### 3.2 Login Flow
187
+
188
+ ```
189
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
190
+ β”‚ Client β”‚ β”‚ Server β”‚
191
+ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜
192
+ β”‚ β”‚
193
+ β”‚ POST /auth/login β”‚
194
+ β”‚ (OAuth2 form: username + password) β”‚
195
+ │────────────────────────────────────────▢│
196
+ β”‚ β”‚
197
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
198
+ β”‚ β”‚ Look up β”‚
199
+ β”‚ β”‚ user by β”‚
200
+ β”‚ β”‚ username β”‚
201
+ β”‚ └──────────────
202
+ β”‚ β”‚
203
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
204
+ β”‚ β”‚ bcrypt β”‚
205
+ β”‚ β”‚ .checkpw() β”‚
206
+ β”‚ └──────────────
207
+ β”‚ β”‚
208
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
209
+ β”‚ β”‚ Check: β”‚
210
+ β”‚ β”‚ - is_active β”‚
211
+ β”‚ β”‚ - is_verifiedβ”‚
212
+ β”‚ └──────────────
213
+ β”‚ β”‚
214
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
215
+ β”‚ β”‚ Generate: β”‚
216
+ β”‚ β”‚ - access_token β”‚
217
+ β”‚ β”‚ - refresh_token β”‚
218
+ β”‚ └──────────────
219
+ β”‚ β”‚
220
+ β”‚ 200 OK β”‚
221
+ β”‚ { access_token, refresh_token } β”‚
222
+ │◀────────────────────────────────────────│
223
+ β”‚ β”‚
224
+ β”‚ Store in sessionStorage β”‚
225
+ β”‚ Set AuthContext user β”‚
226
+ ```
227
+
228
+ ### 3.3 Token Refresh Flow
229
+
230
+ ```
231
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
232
+ β”‚ Client β”‚ β”‚ Server β”‚
233
+ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜
234
+ β”‚ β”‚
235
+ β”‚ API Request with expired access_token β”‚
236
+ │────────────────────────────────────────▢│
237
+ β”‚ β”‚
238
+ β”‚ 401 Unauthorized β”‚
239
+ │◀────────────────────────────────────────│
240
+ β”‚ β”‚
241
+ β”‚ Axios interceptor detects 401 β”‚
242
+ β”‚ β”‚
243
+ β”‚ POST /auth/refresh β”‚
244
+ β”‚ { refresh_token } β”‚
245
+ │────────────────────────────────────────▢│
246
+ β”‚ β”‚
247
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
248
+ β”‚ β”‚ Decode JWT β”‚
249
+ β”‚ β”‚ Check type β”‚
250
+ β”‚ β”‚ == "refresh"β”‚
251
+ β”‚ └──────────────
252
+ β”‚ β”‚
253
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
254
+ β”‚ β”‚ Check if β”‚
255
+ β”‚ β”‚ revoked β”‚
256
+ β”‚ └──────────────
257
+ β”‚ β”‚
258
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
259
+ β”‚ β”‚ Revoke old β”‚
260
+ β”‚ β”‚ refresh tokenβ”‚
261
+ β”‚ β”‚ (rotation) β”‚
262
+ β”‚ └──────────────
263
+ β”‚ β”‚
264
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
265
+ β”‚ β”‚ Issue new β”‚
266
+ β”‚ β”‚ access + β”‚
267
+ β”‚ β”‚ refresh pairβ”‚
268
+ β”‚ └──────────────
269
+ β”‚ β”‚
270
+ β”‚ 200 OK β”‚
271
+ β”‚ { access_token, refresh_token } β”‚
272
+ │◀────────────────────────────────────────│
273
+ β”‚ β”‚
274
+ β”‚ Retry original request with new token β”‚
275
+ │────────────────────────────────────────▢│
276
+ β”‚ β”‚
277
+ β”‚ 200 OK (success) β”‚
278
+ │◀────────────────────────────────────────│
279
+ ```
280
+
281
+ ### 3.4 Logout Flow
282
+
283
+ ```
284
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
285
+ β”‚ Client β”‚ β”‚ Server β”‚
286
+ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜
287
+ β”‚ β”‚
288
+ β”‚ POST /auth/logout β”‚
289
+ β”‚ { refresh_token } β”‚
290
+ β”‚ Authorization: Bearer <access_token> β”‚
291
+ │────────────────────────────────────────▢│
292
+ β”‚ β”‚
293
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
294
+ β”‚ β”‚ Insert both β”‚
295
+ β”‚ β”‚ tokens into β”‚
296
+ β”‚ β”‚ revoked_ β”‚
297
+ β”‚ β”‚ tokens tableβ”‚
298
+ β”‚ └──────────────
299
+ β”‚ β”‚
300
+ β”‚ 200 OK β”‚
301
+ │◀────────────────────────────────────────│
302
+ β”‚ β”‚
303
+ β”‚ Clear sessionStorage β”‚
304
+ β”‚ Set user = null β”‚
305
+ β”‚ Redirect to /login β”‚
306
+ ```
307
+
308
+ ---
309
+
310
+ ## 4. Data Models
311
+
312
+ ### 4.1 Entity Relationship Diagram
313
+
314
+ ```
315
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
316
+ β”‚ users β”‚ β”‚ revoked_tokens β”‚
317
+ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
318
+ β”‚ id INTEGER PK β”‚ β”‚ id INTEGER PK β”‚
319
+ β”‚ username VARCHAR UNI β”‚ β”‚ token VARCHAR UNI β”‚
320
+ β”‚ email VARCHAR UNI β”‚ β”‚ revoked_at DATETIME β”‚
321
+ β”‚ password VARCHAR β”‚ β”‚ expires_at DATETIME β”‚
322
+ β”‚ is_active BOOLEAN β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
323
+ β”‚ role VARCHAR β”‚
324
+ β”‚ created_at DATETIME β”‚ Relationship: One user can have
325
+ β”‚ is_verified BOOLEAN β”‚ many revoked tokens (1:N)
326
+ β”‚ verification_code VARCHARβ”‚
327
+ β”‚ reset_code VARCHAR β”‚
328
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
329
+ ```
330
+
331
+ ### 4.2 User Model Fields
332
+
333
+ | Field | Type | Constraints | Default | Description |
334
+ |-------|------|-------------|---------|-------------|
335
+ | `id` | Integer | PK, auto-increment, indexed | β€” | Unique user identifier |
336
+ | `username` | String(50) | UNIQUE, NOT NULL, indexed | β€” | Login username |
337
+ | `email` | String(120) | UNIQUE, NOT NULL, indexed | β€” | User email address |
338
+ | `password` | String(255) | NOT NULL | β€” | bcrypt hashed password |
339
+ | `is_active` | Boolean | β€” | `True` | Account active status |
340
+ | `role` | String(10) | β€” | `"user"` | User role (user/admin) |
341
+ | `created_at` | DateTime | β€” | `datetime.utcnow()` | Registration timestamp |
342
+ | `is_verified` | Boolean | β€” | `False` | Email verified status |
343
+ | `verification_code` | String(6) | nullable | `None` | Registration OTP |
344
+ | `reset_code` | String(6) | nullable | `None` | Password reset OTP |
345
+
346
+ ### 4.3 RevokedToken Model Fields
347
+
348
+ | Field | Type | Constraints | Default | Description |
349
+ |-------|------|-------------|---------|-------------|
350
+ | `id` | Integer | PK, auto-increment, indexed | β€” | Record identifier |
351
+ | `token` | String(512) | UNIQUE, NOT NULL, indexed | β€” | Full JWT string |
352
+ | `revoked_at` | DateTime | β€” | `datetime.utcnow()` | Revocation timestamp |
353
+ | `expires_at` | DateTime | NOT NULL | β€” | Token original expiry |
354
+
355
+ ---
356
+
357
+ ## 5. Security Architecture
358
+
359
+ ### 5.1 Authentication & Authorization Layers
360
+
361
+ ```
362
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
363
+ β”‚ SECURITY LAYERS β”‚
364
+ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
365
+ β”‚ β”‚
366
+ β”‚ Layer 1: Password Security β”‚
367
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
368
+ β”‚ β”‚ bcrypt hashing (salt + hash) β”‚ β”‚
369
+ β”‚ β”‚ Password strength validation (β‰₯8 chars, 1 letter, β”‚ β”‚
370
+ β”‚ β”‚ 1 digit) β”‚ β”‚
371
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
372
+ β”‚ β”‚
373
+ β”‚ Layer 2: JWT Token Security β”‚
374
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
375
+ β”‚ β”‚ Dual-token system (access 30min + refresh 7days) β”‚ β”‚
376
+ β”‚ β”‚ Token rotation on refresh (old token revoked) β”‚ β”‚
377
+ β”‚ β”‚ Token type validation (access vs refresh) β”‚ β”‚
378
+ β”‚ β”‚ Revocation checking on every request β”‚ β”‚
379
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
380
+ β”‚ β”‚
381
+ β”‚ Layer 3: Email Verification β”‚
382
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
383
+ β”‚ β”‚ OTP-based email verification β”‚ β”‚
384
+ β”‚ β”‚ 6-digit numeric code β”‚ β”‚
385
+ β”‚ β”‚ Must verify before login β”‚ β”‚
386
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
387
+ β”‚ β”‚
388
+ β”‚ Layer 4: Rate Limiting β”‚
389
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
390
+ β”‚ β”‚ Global: 60 requests per 60 seconds per IP β”‚ β”‚
391
+ β”‚ β”‚ HTTP 429 when exceeded β”‚ β”‚
392
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
393
+ β”‚ β”‚
394
+ β”‚ Layer 5: CORS Protection β”‚
395
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
396
+ β”‚ β”‚ Whitelist: localhost:5173, localhost:3000 β”‚ β”‚
397
+ β”‚ β”‚ Credentials allowed β”‚ β”‚
398
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
399
+ β”‚ β”‚
400
+ β”‚ Layer 6: Role-Based Access Control β”‚
401
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
402
+ β”‚ β”‚ Admin-only endpoints (/admin/users) β”‚ β”‚
403
+ β”‚ β”‚ User role checking in route handlers β”‚ β”‚
404
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
405
+ β”‚ β”‚
406
+ β”‚ Layer 7: Input Validation β”‚
407
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
408
+ β”‚ β”‚ Pydantic schemas for all inputs β”‚ β”‚
409
+ β”‚ β”‚ Email validation (EmailStr) β”‚ β”‚
410
+ β”‚ β”‚ Field length constraints β”‚ β”‚
411
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
412
+ β”‚ β”‚
413
+ β”‚ Layer 8: Error Handling β”‚
414
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
415
+ β”‚ β”‚ Generic error messages (no stack trace leak) β”‚ β”‚
416
+ β”‚ β”‚ Request logging for audit trail β”‚ β”‚
417
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
418
+ β”‚ β”‚
419
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
420
+ ```
421
+
422
+ ### 5.2 Token Lifecycle
423
+
424
+ ```
425
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
426
+ β”‚ User Login β”‚
427
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
428
+ β”‚
429
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
430
+ β”‚ Issue Access + β”‚
431
+ β”‚ Refresh Tokens β”‚
432
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
433
+ β”‚
434
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
435
+ β”‚ β”‚ β”‚
436
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β” β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”
437
+ β”‚ Use Access β”‚ β”‚ β”‚ Use Refresh β”‚
438
+ β”‚ Token (30min) β”‚ β”‚ β”‚ Token (7days) β”‚
439
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
440
+ β”‚ β”‚ β”‚
441
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β” β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”
442
+ β”‚ Token Expires β”‚ β”‚ β”‚ Refresh Used β”‚
443
+ β”‚ (401 Response) β”‚ β”‚ β”‚ (Token Rotation)β”‚
444
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
445
+ β”‚ β”‚ β”‚
446
+ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
447
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”
448
+ β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Issue New β”‚
449
+ β”‚ Send Refresh β”‚ β”‚ Access+Refresh β”‚
450
+ β”‚ Token to β”‚ β”‚ Pair β”‚
451
+ β”‚ /auth/refresh β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
452
+ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
453
+ β”‚
454
+ β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”
455
+ β”‚ Old Refresh β”‚
456
+ β”‚ Token Revoked β”‚
457
+ β”‚ (stored in DB)β”‚
458
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
459
+ ```
460
+
461
+ ---
462
+
463
+ ## 6. API Design
464
+
465
+ ### 6.1 Request/Response Schemas
466
+
467
+ **Register Request:**
468
+ ```json
469
+ POST /auth/register
470
+ {
471
+ "username": "string (3-50 chars)",
472
+ "email": "string (valid email)",
473
+ "password": "string (β‰₯8 chars, 1 letter, 1 digit)"
474
+ }
475
+ ```
476
+
477
+ **Register Response:**
478
+ ```json
479
+ 200 OK
480
+ {
481
+ "message": "User registered successfully. Please verify your email."
482
+ }
483
+ ```
484
+
485
+ **Login Request:**
486
+ ```json
487
+ POST /auth/login
488
+ Content-Type: application/x-www-form-urlencoded
489
+
490
+ username=string&password=string
491
+ ```
492
+
493
+ **Login Response:**
494
+ ```json
495
+ 200 OK
496
+ {
497
+ "access_token": "eyJ...",
498
+ "refresh_token": "eyJ...",
499
+ "token_type": "bearer"
500
+ }
501
+ ```
502
+
503
+ **Refresh Request:**
504
+ ```json
505
+ POST /auth/refresh
506
+ {
507
+ "refresh_token": "eyJ..."
508
+ }
509
+ ```
510
+
511
+ **Refresh Response:**
512
+ ```json
513
+ 200 OK
514
+ {
515
+ "access_token": "eyJ...",
516
+ "refresh_token": "eyJ..."
517
+ }
518
+ ```
519
+
520
+ ### 6.2 Error Response Format
521
+
522
+ ```json
523
+ 401 Unauthorized
524
+ {
525
+ "detail": "Invalid authentication credentials"
526
+ }
527
+
528
+ 422 Validation Error
529
+ {
530
+ "detail": [
531
+ {
532
+ "loc": ["body", "password"],
533
+ "msg": "Password must be at least 8 characters with at least one letter and one digit",
534
+ "type": "value_error"
535
+ }
536
+ ]
537
+ }
538
+
539
+ 429 Too Many Requests
540
+ {
541
+ "detail": "Too many requests. Please try again later."
542
+ }
543
+ ```
544
+
545
+ ---
546
+
547
+ ## 7. SMTP Email System
548
+
549
+ ### 7.1 Email Flow
550
+
551
+ ```
552
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
553
+ β”‚ Route β”‚ β”‚ Auth β”‚ β”‚ SMTP β”‚
554
+ β”‚ Handler │────▢│ Service │────▢│ Server β”‚
555
+ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚
556
+ β”‚ send_email()β”‚ β”‚ smtplib β”‚ β”‚ Gmail β”‚
557
+ β”‚ called β”‚ β”‚ STARTTLS β”‚ β”‚ Port 587 β”‚
558
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
559
+ β”‚
560
+ β”‚ (if SMTP not configured or fails)
561
+ β–Ό
562
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
563
+ β”‚ Console β”‚
564
+ β”‚ Fallback β”‚
565
+ β”‚ (print OTP) β”‚
566
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
567
+ ```
568
+
569
+ ### 7.2 SMTP Configuration
570
+
571
+ | Setting | Value | Purpose |
572
+ |---------|-------|---------|
573
+ | Server | `smtp.gmail.com` | Gmail SMTP server |
574
+ | Port | `587` | STARTTLS encryption |
575
+ | Protocol | STARTTLS | Upgrades plain connection to TLS |
576
+ | Auth | Username + App Password | Gmail-specific authentication |
577
+
578
+ ### 7.3 Email Templates Sent
579
+
580
+ **Email Verification:**
581
+ ```
582
+ Subject: Verify your email - Auth System
583
+
584
+ Hello {username},
585
+
586
+ Your verification code is: {otp_code}
587
+
588
+ This code will expire in 10 minutes.
589
+
590
+ If you did not register, please ignore this email.
591
+ ```
592
+
593
+ **Password Reset:**
594
+ ```
595
+ Subject: Password Reset Request - Auth System
596
+
597
+ Hello {username},
598
+
599
+ Your password reset code is: {otp_code}
600
+
601
+ This code will expire in 10 minutes.
602
+
603
+ If you did not request this, please ignore this email.
604
+ ```
605
+
606
+ ---
607
+
608
+ ## 8. Frontend State Management
609
+
610
+ ### 8.1 AuthContext Flow
611
+
612
+ ```
613
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
614
+ β”‚ AuthContext β”‚
615
+ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
616
+ β”‚ β”‚
617
+ β”‚ State: β”‚
618
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
619
+ β”‚ β”‚ user: null | { id, username, email, role, ... } β”‚ β”‚
620
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
621
+ β”‚ β”‚
622
+ β”‚ Methods: β”‚
623
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
624
+ β”‚ β”‚ login(username, password) β”‚ β”‚
625
+ β”‚ β”‚ β†’ POST /auth/login β”‚ β”‚
626
+ β”‚ β”‚ β†’ Store tokens in sessionStorage β”‚ β”‚
627
+ β”‚ β”‚ β†’ Fetch user profile β”‚ β”‚
628
+ β”‚ β”‚ β”‚ β”‚
629
+ β”‚ β”‚ logout() β”‚ β”‚
630
+ β”‚ β”‚ β†’ POST /auth/logout β”‚ β”‚
631
+ β”‚ β”‚ β†’ Clear sessionStorage β”‚ β”‚
632
+ β”‚ β”‚ β†’ Set user = null β”‚ β”‚
633
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
634
+ β”‚ β”‚
635
+ β”‚ Initialization (on mount): β”‚
636
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
637
+ β”‚ β”‚ 1. Check sessionStorage for access_token β”‚ β”‚
638
+ β”‚ β”‚ 2. If exists β†’ GET /me to validate β”‚ β”‚
639
+ β”‚ β”‚ 3. If valid β†’ set user state β”‚ β”‚
640
+ β”‚ β”‚ 4. If invalid β†’ clear session, set user = null β”‚ β”‚
641
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
642
+ β”‚ β”‚
643
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
644
+ ```
645
+
646
+ ### 8.2 Axios Interceptor Chain
647
+
648
+ ```
649
+ Request Flow:
650
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
651
+ β”‚ API │───▢│ Request │───▢│ Server │───▢│ Response β”‚
652
+ β”‚ Call β”‚ β”‚ Interceptor β”‚ β”‚ β”‚ β”‚ β”‚
653
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜
654
+ β”‚ β”‚
655
+ β”‚ Attach Bearer token β”‚
656
+ β”‚ from sessionStorage β”‚
657
+ β”‚
658
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
659
+ β”‚ Response Interceptor β”‚
660
+ β””οΏ½οΏ½οΏ½β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
661
+ β”‚
662
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
663
+ β”‚ If 401: β”‚
664
+ β”‚ 1. POST /auth/refresh β”‚
665
+ β”‚ 2. Update tokens β”‚
666
+ β”‚ 3. Retry request β”‚
667
+ β”‚ 4. If fail β†’ /login β”‚
668
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
669
+ ```
670
+
671
+ ---
672
+
673
+ ## 9. Deployment Architecture
674
+
675
+ ### 9.1 Development Setup
676
+
677
+ ```
678
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
679
+ β”‚ Development Environment β”‚
680
+ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
681
+ β”‚ β”‚
682
+ β”‚ Terminal 1: Backend β”‚
683
+ β”‚ $ cd backend β”‚
684
+ β”‚ $ source .venv/bin/activate β”‚
685
+ β”‚ $ uvicorn main:app --reload --port 8000 β”‚
686
+ β”‚ β”‚
687
+ β”‚ Terminal 2: Frontend β”‚
688
+ β”‚ $ cd frontend β”‚
689
+ β”‚ $ npm run dev β”‚
690
+ β”‚ β†’ Vite dev server on http://localhost:5173 β”‚
691
+ β”‚ β”‚
692
+ β”‚ Database: SQLite (auto-created) β”‚
693
+ β”‚ Email: Console fallback (no SMTP needed) β”‚
694
+ β”‚ β”‚
695
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
696
+ ```
697
+
698
+ ### 9.2 Production Recommendations
699
+
700
+ ```
701
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
702
+ β”‚ Production Architecture β”‚
703
+ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
704
+ β”‚ β”‚
705
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
706
+ β”‚ β”‚ Nginx │───▢│ Uvicorn │───▢│ PostgreSQL β”‚ β”‚
707
+ β”‚ β”‚ (Reverse β”‚ β”‚ (Gunicorn β”‚ β”‚ (or MySQL) β”‚ β”‚
708
+ β”‚ β”‚ Proxy) β”‚ β”‚ Workers) β”‚ β”‚ β”‚ β”‚
709
+ β”‚ β”‚ Port 443 β”‚ β”‚ Port 8000 β”‚ β”‚ Port 5432 β”‚ β”‚
710
+ β”‚ β”‚ HTTPS β”‚ β”‚ β”‚ β”‚ β”‚ β”‚
711
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
712
+ β”‚ β”‚ β”‚ β”‚
713
+ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚
714
+ β”‚ β”‚ β”‚ Redis β”‚ β”‚ β”‚
715
+ β”‚ β”‚ β”‚ (Cache + │◀───────────── β”‚
716
+ β”‚ β”‚ β”‚ Rate β”‚ β”‚ β”‚
717
+ β”‚ β”‚ β”‚ Limiting) β”‚ β”‚ β”‚
718
+ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
719
+ β”‚ β”‚ β”‚ β”‚
720
+ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚
721
+ β”‚ └───────────▢│ Static β”‚ β”‚ β”‚
722
+ β”‚ β”‚ Files β”‚ β”‚ β”‚
723
+ β”‚ β”‚ (React β”‚ β”‚ β”‚
724
+ β”‚ β”‚ Build) β”‚ β”‚ β”‚
725
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
726
+ β”‚ β”‚ β”‚
727
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
728
+ ```
729
+
730
+ ---
731
+
732
+ ## 10. Environment Configuration
733
+
734
+ | Variable | Development | Production | Purpose |
735
+ |----------|-------------|------------|---------|
736
+ | `SECRET_KEY` | `super_secret_dev_key...` | Strong random 256-bit key | JWT signing |
737
+ | `ALGORITHM` | `HS256` | `HS256` or `RS256` | JWT algorithm |
738
+ | `ACCESS_TOKEN_EXPIRE_MINUTES` | `30` | `15` | Shorter in prod |
739
+ | `REFRESH_TOKEN_EXPIRE_DAYS` | `7` | `7` | Refresh token TTL |
740
+ | `DATABASE_URL` | `sqlite:///./users.db` | `postgresql://...` | Database |
741
+ | `SMTP_SERVER` | `smtp.gmail.com` | `smtp.gmail.com` | Email server |
742
+ | `SMTP_PORT` | `587` | `587` | Email port |
743
+ | `SMTP_USER` | `user@gmail.com` | `noreply@domain.com` | Email sender |
744
+ | `SMTP_PASSWORD` | `app_password` | `app_password` | Email auth |
745
+
746
+ ---
747
+
748
+ ## 11. Future Improvements
749
+
750
+ ### High Priority
751
+ - [ ] Add `.gitignore` (exclude `.env`, `users.db`, `__pycache__`, `node_modules`)
752
+ - [ ] Use `secrets` module for OTP generation (cryptographic randomness)
753
+ - [ ] Add per-account brute-force lockout (e.g., 5 failed attempts β†’ 15min lock)
754
+ - [ ] Move to PostgreSQL for production use
755
+
756
+ ### Medium Priority
757
+ - [ ] Add Docker + docker-compose for containerized deployment
758
+ - [ ] Implement email rate limiting (prevent OTP spam)
759
+ - [ ] Add CSRF protection
760
+ - [ ] Add comprehensive unit and integration tests
761
+ - [ ] Implement account deletion with cascade (revoke all tokens)
762
+
763
+ ### Low Priority
764
+ - [ ] Add OAuth2 social login (Google, GitHub)
765
+ - [ ] Implement 2FA (TOTP-based)
766
+ - [ ] Add admin dashboard for user management
767
+ - [ ] Implement audit logging
768
+ - [ ] Add API versioning (v1/, v2/)
main.py DELETED
@@ -1,432 +0,0 @@
1
- import bcrypt
2
- from fastapi import FastAPI, Depends, HTTPException
3
- from fastapi.middleware.cors import CORSMiddleware
4
- from fastapi.security import OAuth2PasswordBearer
5
- from jose import jwt, JWTError
6
- from datetime import datetime, timedelta
7
- from pydantic import BaseModel, Field, validator
8
- from sqlalchemy import create_engine, Column, Integer, String, Boolean, DateTime
9
- from sqlalchemy.orm import sessionmaker, Session, DeclarativeBase
10
- import os
11
- import re
12
- import random
13
- import string
14
- import threading
15
- import logging
16
-
17
- logger = logging.getLogger(__name__)
18
- logging.basicConfig(level=logging.INFO)
19
-
20
- # Use /app/data in Docker, ./data locally
21
- DB_DIR = os.environ.get("DB_DIR", "./data")
22
- os.makedirs(DB_DIR, exist_ok=True)
23
- DATABASE_URL = f"sqlite:///{os.path.join(DB_DIR, 'users.db')}"
24
-
25
- SECRET_KEY = os.environ.get("SECRET_KEY", "mysecretkey123")
26
- ALGORITHM = "HS256"
27
- ACCESS_TOKEN_EXPIRE_MINUTES = 30
28
-
29
- engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
30
- SessionLocal = sessionmaker(bind=engine)
31
-
32
- class Base(DeclarativeBase):
33
- pass
34
-
35
- class User(Base):
36
- __tablename__ = "users"
37
-
38
- id = Column(Integer, primary_key=True)
39
- username = Column(String, unique=True)
40
- email = Column(String, unique=True)
41
- password = Column(String)
42
- is_activate = Column(Boolean, default=True)
43
-
44
- class OTP(Base):
45
- __tablename__ = "otp"
46
-
47
- id = Column(Integer, primary_key=True)
48
- email = Column(String)
49
- code = Column(String)
50
- purpose = Column(String)
51
- expires_at = Column(DateTime)
52
- is_used = Column(Boolean, default=False)
53
- username = Column(String, nullable=True)
54
- password_hash = Column(String, nullable=True)
55
-
56
- class RegisterRequest(BaseModel):
57
- username: str = Field(min_length=3, max_length=20)
58
- email: str
59
- password: str = Field(min_length=6)
60
-
61
- @validator("username")
62
- def validate_username(cls, v):
63
- if not re.match(r"^[a-zA-Z0-9_]+$", v):
64
- raise ValueError("Username must be alphanumeric with underscores only")
65
- return v
66
-
67
- @validator("email")
68
- def validate_email(cls, v):
69
- if not re.match(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", v):
70
- raise ValueError("Invalid email format")
71
- return v
72
-
73
- class LoginRequest(BaseModel):
74
- username: str
75
- password: str
76
-
77
- class TokenResponse(BaseModel):
78
- access_token: str
79
- refresh_token: str
80
- token_type: str = "bearer"
81
-
82
- class RefreshRequest(BaseModel):
83
- refresh_token: str
84
-
85
- class ForgotPasswordRequest(BaseModel):
86
- email: str
87
-
88
- class ForgotPasswordVerifyRequest(BaseModel):
89
- email: str
90
- otp: str
91
- new_password: str = Field(min_length=6)
92
-
93
- class VerifyRegisterRequest(BaseModel):
94
- email: str
95
- otp: str
96
-
97
- # Email config - SMTP for local, Brevo HTTP for Render
98
- BREVO_API_KEY = os.environ.get("BREVO_API_KEY", "")
99
- EMAIL_FROM = "ashutoshknp12@gmail.com"
100
- EMAIL_FROM_NAME = "Auth System"
101
-
102
- # Gmail SMTP for local testing (use env vars so passwords stay out of code)
103
- SMTP_HOST = os.environ.get("SMTP_HOST", "smtp.gmail.com")
104
- SMTP_PORT = int(os.environ.get("SMTP_PORT", "465"))
105
- SMTP_USER = os.environ.get("SMTP_USER", "ashutoshknp12@gmail.com")
106
- SMTP_PASS = os.environ.get("SMTP_PASS", "")
107
-
108
- def generate_otp():
109
- return ''.join(random.choices(string.digits, k=6))
110
-
111
- def send_otp_email(to_email, otp_code, purpose):
112
- """Send OTP email. Uses Brevo HTTP API on Render, Gmail SMTP locally."""
113
- if purpose == "register":
114
- subject = "Verify Your Account"
115
- elif purpose == "forgot_password":
116
- subject = "Password Reset Code"
117
- else:
118
- subject = "Your OTP Code"
119
-
120
- html = f"<p>Your OTP code is: <strong>{otp_code}</strong></p><p>It expires in 5 minutes.</p>"
121
- body = f"Your OTP code is: {otp_code}\nIt expires in 5 minutes."
122
-
123
- # --- Method 1: Brevo HTTP API (works on Render, uses HTTPS port 443) ---
124
- # Render free tier BLOCKS SMTP ports 25/465/587, so we MUST use HTTP API
125
- if BREVO_API_KEY:
126
- try:
127
- import urllib.request
128
- import json
129
- payload = json.dumps({
130
- "sender": {"name": EMAIL_FROM_NAME, "email": EMAIL_FROM},
131
- "to": [{"email": to_email}],
132
- "subject": subject,
133
- "htmlContent": html
134
- }).encode()
135
- req = urllib.request.Request(
136
- "https://api.brevo.com/v3/smtp/email",
137
- data=payload,
138
- headers={"api-key": BREVO_API_KEY, "Content-Type": "application/json"}
139
- )
140
- resp = urllib.request.urlopen(req, timeout=15)
141
- resp_body = resp.read().decode()
142
- logger.info(f"OTP email sent to {to_email} via Brevo. Response: {resp_body}")
143
- return True
144
- except Exception as e:
145
- logger.error(f"Brevo HTTP API failed for {to_email}: {e}")
146
- # Don't fall through to SMTP on Render β€” it's blocked anyway
147
- return False
148
-
149
- # --- Method 2: Gmail SMTP (works locally, NOT on Render free tier) ---
150
- if SMTP_PASS:
151
- try:
152
- import smtplib
153
- from email.mime.text import MIMEText
154
- msg = MIMEText(body)
155
- msg["Subject"] = subject
156
- msg["From"] = EMAIL_FROM
157
- msg["To"] = to_email
158
- server = smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, timeout=10)
159
- server.login(SMTP_USER, SMTP_PASS)
160
- server.sendmail(EMAIL_FROM, to_email, msg.as_string())
161
- server.quit()
162
- logger.info(f"OTP email sent to {to_email} via Gmail SMTP")
163
- return True
164
- except Exception as e:
165
- logger.error(f"Gmail SMTP failed for {to_email}: {e}")
166
- return False
167
-
168
- # No email method available
169
- logger.error("No email method set up! Set BREVO_API_KEY (for Render) or SMTP_PASS (for local)")
170
- return False
171
-
172
- # Removed background thread β€” send email directly so we can report errors to user
173
-
174
- def create_otp(db, email, purpose):
175
- code = generate_otp()
176
- otp = OTP(
177
- email=email,
178
- code=code,
179
- purpose=purpose,
180
- expires_at=datetime.utcnow() + timedelta(minutes=5)
181
- )
182
- db.add(otp)
183
- db.commit()
184
- return code
185
-
186
- def hash_password(password):
187
- return bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=4)).decode()
188
-
189
- def verify_password(plain, hashed):
190
- return bcrypt.checkpw(plain.encode(), hashed.encode())
191
-
192
- def create_access_token(data):
193
- to_encode = data.copy()
194
- to_encode["sub"] = str(to_encode["sub"])
195
- to_encode["exp"] = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
196
- to_encode["type"] = "access"
197
- return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
198
-
199
- def create_refresh_token(data):
200
- to_encode = data.copy()
201
- to_encode["sub"] = str(to_encode["sub"])
202
- to_encode["exp"] = datetime.utcnow() + timedelta(days=7)
203
- to_encode["type"] = "refresh"
204
- return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
205
-
206
-
207
-
208
- def get_db():
209
- db = SessionLocal()
210
- try:
211
- yield db
212
- finally:
213
- db.close()
214
-
215
-
216
-
217
- oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
218
-
219
- def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
220
- try:
221
- payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
222
- user_id = int(payload.get("sub"))
223
- token_type = payload.get("type")
224
- if user_id is None or token_type != "access":
225
- raise HTTPException(status_code=401, detail="Invalid token")
226
- except JWTError:
227
- raise HTTPException(status_code=401, detail="Invalid token")
228
-
229
- user = db.query(User).filter(User.id == user_id).first()
230
- if not user:
231
- raise HTTPException(status_code=401, detail="User not found")
232
- return user
233
-
234
-
235
- app = FastAPI()
236
-
237
- CORS_ORIGINS = os.environ.get("CORS_ORIGINS", "https://frontend-15-two.vercel.app,https://frontend-15-b3jpt0vcu-ashutoshs-projects-8cd9906b.vercel.app,http://localhost:5173,http://localhost:5174,http://localhost:7860").split(",")
238
- app.add_middleware(
239
- CORSMiddleware,
240
- allow_origins=CORS_ORIGINS,
241
- allow_credentials=True,
242
- allow_methods=["*"],
243
- allow_headers=["*"],
244
- )
245
-
246
- Base.metadata.create_all(bind=engine)
247
-
248
- @app.post("/auth/register")
249
- def register(req: RegisterRequest, db: Session = Depends(get_db)):
250
- if db.query(User).filter(User.username == req.username).first():
251
- raise HTTPException(400, "Username taken")
252
- if db.query(User).filter(User.email == req.email).first():
253
- raise HTTPException(400, "Email already registered")
254
- code = generate_otp()
255
- otp = OTP(
256
- email=req.email,
257
- code=code,
258
- purpose="register",
259
- expires_at=datetime.utcnow() + timedelta(minutes=5),
260
- username=req.username,
261
- password_hash=hash_password(req.password)
262
- )
263
- db.add(otp)
264
- db.commit()
265
- success = send_otp_email(req.email, code, "register")
266
- if not success:
267
- raise HTTPException(500, "Failed to send OTP email. Try again.")
268
- return {"message": "OTP sent to email. Verify to activate account."}
269
-
270
- @app.post("/auth/register/verify")
271
- def register_verify(req: VerifyRegisterRequest, db: Session = Depends(get_db)):
272
- otp = db.query(OTP).filter(
273
- OTP.email == req.email,
274
- OTP.code == req.otp,
275
- OTP.purpose == "register",
276
- OTP.is_used == False
277
- ).first()
278
- if not otp:
279
- raise HTTPException(400, "Invalid OTP")
280
- if otp.expires_at < datetime.utcnow():
281
- raise HTTPException(400, "OTP expired")
282
- user = User(
283
- username=otp.username,
284
- email=otp.email,
285
- password=otp.password_hash,
286
- is_activate=True
287
- )
288
- db.add(user)
289
- otp.is_used = True
290
- db.commit()
291
- return {"message": "Account verified. You can now login."}
292
-
293
- @app.post("/auth/login")
294
- def login(req: LoginRequest, db: Session = Depends(get_db)):
295
- user = db.query(User).filter(
296
- (User.username == req.username) | (User.email == req.username)
297
- ).first()
298
- if not user or not verify_password(req.password, user.password):
299
- raise HTTPException(401, "Wrong credentials")
300
- if not user.is_activate:
301
- raise HTTPException(401, "Account not verified. Check your email for OTP.")
302
- return {
303
- "access_token": create_access_token({"sub": user.id}),
304
- "refresh_token": create_refresh_token({"sub": user.id}),
305
- "user": {"id": user.id, "username": user.username, "email": user.email}
306
- }
307
-
308
- @app.get("/me")
309
- def me(current_user: User = Depends(get_current_user)):
310
- return {"id": current_user.id, "username": current_user.username, "email": current_user.email}
311
-
312
- @app.post("/auth/refresh", response_model=TokenResponse)
313
- def refresh(req: RefreshRequest, db: Session = Depends(get_db)):
314
- try:
315
- payload = jwt.decode(req.refresh_token, SECRET_KEY, algorithms=[ALGORITHM])
316
- if payload.get("type") != "refresh":
317
- raise HTTPException(401, "Invalid token")
318
- user_id = int(payload.get("sub"))
319
- except JWTError:
320
- raise HTTPException(401, "Invalid token")
321
-
322
- user = db.query(User).filter(User.id == user_id).first()
323
- if not user:
324
- raise HTTPException(401, "User not found")
325
-
326
- if not user.is_activate:
327
- raise HTTPException(401, "User deactivated")
328
-
329
- new_refresh_token = create_refresh_token({"sub": user.id})
330
-
331
- return {
332
- "access_token": create_access_token({"sub": user.id}),
333
- "refresh_token": new_refresh_token
334
- }
335
-
336
- @app.post("/users")
337
- def create_user(
338
- req: RegisterRequest,
339
- db: Session = Depends(get_db),
340
- current_user: User = Depends(get_current_user)
341
- ):
342
- if db.query(User).filter(User.username == req.username).first():
343
- raise HTTPException(400, "Username taken")
344
- user = User(
345
- username=req.username,
346
- email=req.email,
347
- password=hash_password(req.password)
348
- )
349
- db.add(user)
350
- db.commit()
351
- db.refresh(user)
352
- return {"id": user.id, "username": user.username, "email": user.email}
353
-
354
- @app.get("/users")
355
- def get_users(
356
- db: Session = Depends(get_db),
357
- current_user: User = Depends(get_current_user)
358
- ):
359
- return db.query(User).all()
360
-
361
- @app.get("/users/{user_id}")
362
- def get_user(
363
- user_id: int,
364
- db: Session = Depends(get_db),
365
- current_user: User = Depends(get_current_user)
366
- ):
367
- user = db.query(User).filter(User.id == user_id).first()
368
- if not user:
369
- raise HTTPException(status_code=404, detail="User not found")
370
- return user
371
-
372
- @app.put("/users/{user_id}")
373
- def update_user(
374
- user_id: int,
375
- username: str,
376
- db: Session = Depends(get_db),
377
- current_user: User = Depends(get_current_user)
378
- ):
379
- user = db.query(User).filter(User.id == user_id).first()
380
- if not user:
381
- raise HTTPException(status_code=404, detail="User not found")
382
- user.username = username
383
- db.commit()
384
- return user
385
-
386
- @app.delete("/users/{user_id}")
387
- def delete_user(
388
- user_id: int,
389
- db: Session = Depends(get_db),
390
- current_user: User = Depends(get_current_user)
391
- ):
392
- user = db.query(User).filter(User.id == user_id).first()
393
- if not user:
394
- raise HTTPException(status_code=404, detail="User not found")
395
- db.delete(user)
396
- db.commit()
397
- return {"message": "Deleted"}
398
-
399
- @app.post("/auth/forgot-password")
400
- def forgot_password(req: ForgotPasswordRequest, db: Session = Depends(get_db)):
401
- user = db.query(User).filter(User.email == req.email).first()
402
- if not user:
403
- raise HTTPException(404, "Email not found")
404
- code = create_otp(db, req.email, "forgot_password")
405
- success = send_otp_email(req.email, code, "forgot_password")
406
- if not success:
407
- raise HTTPException(500, "Failed to send OTP email. Try again.")
408
- return {"message": "OTP sent to email"}
409
-
410
- @app.post("/auth/forgot-password/verify")
411
- def forgot_password_verify(req: ForgotPasswordVerifyRequest, db: Session = Depends(get_db)):
412
- otp = db.query(OTP).filter(
413
- OTP.email == req.email,
414
- OTP.code == req.otp,
415
- OTP.purpose == "forgot_password",
416
- OTP.is_used == False
417
- ).first()
418
- if not otp:
419
- raise HTTPException(400, "Invalid OTP")
420
- if otp.expires_at < datetime.utcnow():
421
- raise HTTPException(400, "OTP expired")
422
- user = db.query(User).filter(User.email == req.email).first()
423
- if not user:
424
- raise HTTPException(404, "User not found")
425
- user.password = hash_password(req.new_password)
426
- otp.is_used = True
427
- db.commit()
428
- return {"message": "Password reset successful"}
429
-
430
- @app.get("/health")
431
- def health():
432
- return {"status": "ok"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt DELETED
@@ -1,6 +0,0 @@
1
- fastapi>=0.100.0
2
- uvicorn>=0.23.0
3
- python-jose[cryptography]>=3.3.0
4
- bcrypt>=4.0.0
5
- SQLAlchemy>=2.0.0
6
- python-multipart>=0.0.6
 
 
 
 
 
 
 
summary.md ADDED
@@ -0,0 +1,258 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Auth-System-SMTP β€” Summary of Important Questions & Concepts
2
+
3
+ ---
4
+
5
+ ## 1. Project Overview
6
+
7
+ **Q: What is this project?**
8
+ A full-stack authentication system with email verification and password reset using SMTP. Built with **FastAPI** (backend) + **React** (frontend).
9
+
10
+ **Q: What is the tech stack?**
11
+ | Layer | Technology |
12
+ |-------|-----------|
13
+ | Backend | FastAPI, SQLAlchemy, SQLite, bcrypt, JWT (python-jose) |
14
+ | Frontend | React 18, Vite, Axios, React Router v6 |
15
+ | Email | Python `smtplib` (STARTTLS on port 587) |
16
+ | Auth | Dual JWT tokens (access + refresh), bcrypt hashing, OTP verification |
17
+
18
+ ---
19
+
20
+ ## 2. Authentication Flow
21
+
22
+ **Q: What happens during registration?**
23
+ 1. User submits `username`, `email`, `password`
24
+ 2. Backend validates password strength (β‰₯8 chars, 1 letter, 1 digit)
25
+ 3. Password is hashed with bcrypt
26
+ 4. A 6-digit OTP is generated and stored in `verification_code`
27
+ 5. OTP is sent via email (SMTP or console fallback)
28
+ 6. User is redirected to `/verify-email`
29
+
30
+ **Q: How does email verification work?**
31
+ 1. User enters email + 6-digit OTP
32
+ 2. Backend matches OTP against `verification_code` column
33
+ 3. On success: `is_verified = True`, `verification_code = None`
34
+ 4. User can now log in
35
+
36
+ **Q: What happens during login?**
37
+ 1. User submits `username` + `password` (OAuth2 form body)
38
+ 2. Backend validates credentials with `bcrypt.checkpw()`
39
+ 3. Checks `is_active` and `is_verified` flags
40
+ 4. Issues two JWT tokens:
41
+ - **Access token**: 30-minute expiry, type `"access"`
42
+ - **Refresh token**: 7-day expiry, type `"refresh"`
43
+ 5. Frontend stores both in `sessionStorage`
44
+
45
+ **Q: How does token refresh work?**
46
+ 1. Axios interceptor detects a 401 response
47
+ 2. Sends refresh token to `/auth/refresh`
48
+ 3. Backend validates refresh token (not revoked, valid type, user active)
49
+ 4. **Revokes old refresh token** (adds to `revoked_tokens` table)
50
+ 5. Issues new access + refresh token pair (token rotation)
51
+ 6. Original request is retried with new token
52
+
53
+ **Q: How does logout work?**
54
+ 1. Frontend sends refresh token to `/auth/logout`
55
+ 2. Backend inserts both tokens into `revoked_tokens` table
56
+ 3. Frontend clears `sessionStorage` and resets user state
57
+
58
+ ---
59
+
60
+ ## 3. Password Reset Flow
61
+
62
+ **Q: How does forgot password work?**
63
+ - **Step 1**: User enters email β†’ backend generates 6-digit OTP β†’ stores in `reset_code` β†’ sends via email
64
+ - **Step 2**: User enters email + code + new password β†’ backend validates OTP β†’ hashes new password β†’ clears `reset_code`
65
+
66
+ ---
67
+
68
+ ## 4. JWT Token Structure
69
+
70
+ **Q: What claims are in the access token?**
71
+ ```json
72
+ {
73
+ "sub": "<user_id>",
74
+ "email": "<email>",
75
+ "type": "access",
76
+ "exp": "<datetime>"
77
+ }
78
+ ```
79
+
80
+ **Q: What claims are in the refresh token?**
81
+ ```json
82
+ {
83
+ "sub": "<user_id>",
84
+ "type": "refresh",
85
+ "exp": "<datetime>"
86
+ }
87
+ ```
88
+
89
+ **Q: Why use two token types?**
90
+ - Access token: Short-lived, used for API requests
91
+ - Refresh token: Long-lived, used only to get new access tokens
92
+ - Separation limits exposure if a token is compromised
93
+
94
+ ---
95
+
96
+ ## 5. Database Schema
97
+
98
+ **Q: What tables exist?**
99
+
100
+ ### `users` Table
101
+ | Column | Type | Purpose |
102
+ |--------|------|---------|
103
+ | `id` | Integer (PK) | User ID |
104
+ | `username` | String (UNIQUE) | Login username |
105
+ | `email` | String (UNIQUE) | User email |
106
+ | `password` | String | bcrypt hashed password |
107
+ | `is_active` | Boolean | Account enabled/disabled |
108
+ | `role` | String | `"user"` or `"admin"` |
109
+ | `created_at` | DateTime | Registration timestamp |
110
+ | `is_verified` | Boolean | Email verified flag |
111
+ | `verification_code` | String (nullable) | Active registration OTP |
112
+ | `reset_code` | String (nullable) | Active password reset OTP |
113
+
114
+ ### `revoked_tokens` Table
115
+ | Column | Type | Purpose |
116
+ |--------|------|---------|
117
+ | `id` | Integer (PK) | Record ID |
118
+ | `token` | String (UNIQUE) | Revoked JWT string |
119
+ | `revoked_at` | DateTime | When it was revoked |
120
+ | `expires_at` | DateTime | Original token expiry |
121
+
122
+ ---
123
+
124
+ ## 6. API Endpoints
125
+
126
+ **Q: What are all the endpoints?**
127
+
128
+ ### Auth Routes (`/auth`)
129
+ | Method | Endpoint | Description |
130
+ |--------|----------|-------------|
131
+ | POST | `/auth/register` | Register new user |
132
+ | POST | `/auth/verify-email` | Verify email with OTP |
133
+ | POST | `/auth/login` | Login, returns JWT tokens |
134
+ | POST | `/auth/forgot-password` | Request password reset OTP |
135
+ | POST | `/auth/reset-password` | Reset password with OTP |
136
+ | POST | `/auth/refresh` | Refresh access token |
137
+ | POST | `/auth/logout` | Revoke tokens |
138
+
139
+ ### User Routes
140
+ | Method | Endpoint | Description |
141
+ |--------|----------|-------------|
142
+ | GET | `/me` | Get current user profile |
143
+ | PUT | `/me` | Update profile |
144
+ | DELETE | `/me` | Delete own account |
145
+ | GET | `/admin/users` | List all users (admin only) |
146
+
147
+ ---
148
+
149
+ ## 7. Security Concepts
150
+
151
+ **Q: How are passwords stored?**
152
+ - Hashed with **bcrypt** using `bcrypt.gensalt()` + `bcrypt.hashpw()`
153
+ - Verified with `bcrypt.checkpw()` (constant-time comparison)
154
+
155
+ **Q: What is token revocation?**
156
+ - Tokens are inserted into `revoked_tokens` table
157
+ - Every authenticated request checks this table
158
+ - If token is found β†’ 401 Unauthorized
159
+
160
+ **Q: What is token rotation?**
161
+ - On each refresh, the old refresh token is revoked
162
+ - A new access + refresh token pair is issued
163
+ - Prevents long-lived refresh token abuse
164
+
165
+ **Q: How is rate limiting implemented?**
166
+ - Custom HTTP middleware in FastAPI
167
+ - Max 60 requests per 60-second window per client IP
168
+ - Returns HTTP 429 when exceeded
169
+
170
+ **Q: How is CORS configured?**
171
+ - Allows only `http://localhost:5173` and `http://localhost:3000`
172
+ - Allows credentials, all methods, all headers
173
+
174
+ **Q: What is the SMTP fallback mechanism?**
175
+ - If SMTP credentials are empty/missing β†’ prints OTP to console
176
+ - If SMTP sending fails β†’ falls back to console printing
177
+ - Allows development without a real mail server
178
+
179
+ ---
180
+
181
+ ## 8. Frontend Architecture
182
+
183
+ **Q: How does the frontend manage auth state?**
184
+ - `AuthContext` (React Context) holds `user` state
185
+ - On mount, checks `sessionStorage` for `access_token`
186
+ - Calls `GET /me` to validate and hydrate user state
187
+ - If validation fails β†’ clears session
188
+
189
+ **Q: How does the axios interceptor work?**
190
+ - **Request interceptor**: Attaches `Authorization: Bearer <token>` from `sessionStorage`
191
+ - **Response interceptor**: On 401 β†’ attempts refresh β†’ retries request β†’ on failure redirects to `/login`
192
+
193
+ **Q: What is the ProtectedRoute component?**
194
+ - Wraps routes that require authentication
195
+ - Checks `user` from AuthContext
196
+ - Redirects to `/login` if not authenticated
197
+ - Shows loading spinner while checking
198
+
199
+ ---
200
+
201
+ ## 9. Environment Variables
202
+
203
+ | Variable | Default | Purpose |
204
+ |----------|---------|---------|
205
+ | `SECRET_KEY` | `super_secret_dev_key_change_me_123456789` | JWT signing key |
206
+ | `ALGORITHM` | `HS256` | JWT algorithm |
207
+ | `ACCESS_TOKEN_EXPIRE_MINUTES` | `30` | Access token TTL |
208
+ | `REFRESH_TOKEN_EXPIRE_DAYS` | `7` | Refresh token TTL |
209
+ | `DATABASE_URL` | `sqlite:///./users.db` | Database connection |
210
+ | `SMTP_SERVER` | `""` | SMTP host (empty = simulation) |
211
+ | `SMTP_PORT` | `587` | SMTP port |
212
+ | `SMTP_USER` | `""` | SMTP login (empty = simulation) |
213
+ | `SMTP_PASSWORD` | `""` | SMTP password (empty = simulation) |
214
+
215
+ ---
216
+
217
+ ## 10. Important Security Notes & Weaknesses
218
+
219
+ **Q: What security concerns exist?**
220
+ 1. `.env` contains real Gmail credentials β€” must not be committed to version control
221
+ 2. `SECRET_KEY` is a weak dev key β€” must be changed in production
222
+ 3. OTP generated with `random.choices` (non-cryptographic PRNG) β€” should use `secrets` module
223
+ 4. `forgot_password` endpoint may expose whether an email exists (different error responses)
224
+ 5. No per-account brute-force protection β€” only global rate limiter
225
+ 6. No `.gitignore` file β€” secrets could be accidentally committed
226
+
227
+ **Q: What should be improved for production?**
228
+ 1. Use a production database (PostgreSQL, MySQL) instead of SQLite
229
+ 2. Add `.gitignore` to exclude `.env`, `users.db`, `__pycache__`, `node_modules`
230
+ 3. Use `secrets` module for OTP generation
231
+ 4. Implement account lockout after failed login attempts
232
+ 5. Add email rate limiting (prevent OTP spam)
233
+ 6. Use asymmetric JWT signing (RS256) for microservice architectures
234
+ 7. Add HTTPS enforcement
235
+ 8. Implement CSRF protection
236
+ 9. Add comprehensive logging and monitoring
237
+
238
+ ---
239
+
240
+ ## 11. Key Files Reference
241
+
242
+ | File | Purpose |
243
+ |------|---------|
244
+ | `backend/main.py` | FastAPI app entry, CORS, rate limiting, middleware |
245
+ | `backend/auth.py` | Password hashing, JWT creation, SMTP email sending |
246
+ | `backend/models.py` | SQLAlchemy ORM models (User, RevokedToken) |
247
+ | `backend/schemas.py` | Pydantic request/response schemas |
248
+ | `backend/config.py` | Centralized config from `.env` |
249
+ | `backend/database.py` | SQLAlchemy engine, session, Base |
250
+ | `backend/routes/auth_routes.py` | Auth endpoints |
251
+ | `backend/routes/user_routes.py` | User profile + admin endpoints |
252
+ | `frontend/src/context/AuthContext.jsx` | Auth state management |
253
+ | `frontend/src/api/axios.js` | Axios instance with interceptors |
254
+ | `frontend/src/components/ProtectedRoute.jsx` | Route guard |
255
+ | `frontend/src/pages/Login.jsx` | Login form |
256
+ | `frontend/src/pages/Register.jsx` | Registration form |
257
+ | `frontend/src/pages/VerifyEmail.jsx` | OTP verification form |
258
+ | `frontend/src/pages/Dashboard.jsx` | User dashboard + admin panel |