├── .gitignore ├── app ├── api │ └── routes.py ├── auth.py ├── config.py ├── database.py └── models.py ├── commands ├── create-user.txt ├── login-user.txt └── test-user.txt ├── configs ├── default.txt └── fastapi.conf ├── main.py ├── notes.md └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | env/ 2 | __pycache__/ 3 | .env -------------------------------------------------------------------------------- /app/api/routes.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends, HTTPException, Request, Form 2 | from fastapi.security import OAuth2PasswordRequestForm 3 | from datetime import timedelta 4 | from app.models import Token, User, UserResponse 5 | from app.auth import authenticate_user, create_access_token, get_current_user 6 | from app.config import settings, limiter 7 | from app.database import get_user, add_user 8 | from app.auth import pwd_context 9 | import logging 10 | 11 | logging.basicConfig(level=logging.DEBUG) 12 | logger = logging.getLogger(__name__) 13 | 14 | router = APIRouter() 15 | 16 | def rate_limit_key_func(request: Request): 17 | user = request.state.user if hasattr(request.state, 'user') else None 18 | return f"{request.client.host}:{user.username if user else 'anonymous'}" 19 | 20 | @router.post("/token", response_model=Token) 21 | @limiter.limit("5/minute") 22 | async def login_for_access_token( 23 | request: Request, 24 | form_data: OAuth2PasswordRequestForm = Depends() 25 | ): 26 | user = authenticate_user(form_data.username, form_data.password) 27 | if not user: 28 | raise HTTPException( 29 | status_code=401, 30 | detail="Incorrect username or password", 31 | headers={"WWW-Authenticate": "Bearer"}, 32 | ) 33 | access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) 34 | access_token = create_access_token( 35 | data={"sub": user.username}, expires_delta=access_token_expires 36 | ) 37 | return {"access_token": access_token, "token_type": "bearer"} 38 | 39 | @router.post("/register", response_model=UserResponse) 40 | @limiter.limit("3/hour") 41 | async def register( 42 | request: Request, 43 | username: str = Form(...), 44 | password: str = Form(...) 45 | ): 46 | if get_user(username): 47 | raise HTTPException( 48 | status_code=400, 49 | detail="Username already registered" 50 | ) 51 | 52 | hashed_password = pwd_context.hash(password) 53 | logger.debug(f"Adding user: {username}") 54 | new_user = add_user(username, hashed_password) 55 | logger.debug(f"New user: {new_user}") 56 | 57 | if new_user is None: 58 | logger.error("Failed to add user") 59 | raise HTTPException( 60 | status_code=500, 61 | detail="Failed to create user" 62 | ) 63 | 64 | return UserResponse(username=new_user.username) 65 | 66 | @router.get("/users/me") 67 | @limiter.limit("10/minute", key_func=rate_limit_key_func) 68 | async def read_users_me(request: Request, current_user: User = Depends(get_current_user)): 69 | request.state.user = current_user 70 | return current_user 71 | 72 | @router.get("/") 73 | @limiter.limit("5/minute") 74 | async def root(request: Request): 75 | return {"message": "Hello World"} 76 | -------------------------------------------------------------------------------- /app/auth.py: -------------------------------------------------------------------------------- 1 | from fastapi import Depends, HTTPException, status 2 | from fastapi.security import OAuth2PasswordBearer 3 | from jose import JWTError, jwt 4 | from passlib.context import CryptContext 5 | from datetime import datetime, timedelta 6 | from typing import Optional 7 | from app.models import User 8 | from app.database import get_user 9 | from app.config import settings 10 | import logging 11 | 12 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 13 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") 14 | 15 | logging.basicConfig(level=logging.DEBUG) 16 | logger = logging.getLogger(__name__) 17 | 18 | def verify_password(plain_password, hashed_password): 19 | return pwd_context.verify(plain_password, hashed_password) 20 | 21 | def authenticate_user(username: str, password: str): 22 | user = get_user(username) 23 | if not user: 24 | return False 25 | if not verify_password(password, user["hashed_password"]): 26 | return False 27 | return User(**user) 28 | 29 | def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): 30 | to_encode = data.copy() 31 | if expires_delta: 32 | expire = datetime.now() + expires_delta 33 | else: 34 | expire = datetime.now() + timedelta(minutes=15) 35 | to_encode.update({"exp": expire}) 36 | encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM) 37 | return encoded_jwt 38 | 39 | async def get_current_user(token: str = Depends(oauth2_scheme)): 40 | credentials_exception = HTTPException( 41 | status_code=status.HTTP_401_UNAUTHORIZED, 42 | detail="Could not validate credentials", 43 | headers={"WWW-Authenticate": "Bearer"}, 44 | ) 45 | try: 46 | logger.debug(f"Received token: {token}") 47 | payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) 48 | logger.debug(f"Decoded payload: {payload}") 49 | username: str = payload.get("sub") 50 | user = get_user(username) 51 | logger.debug(f"Retrieved user: {user}") 52 | except JWTError as e: 53 | logger.error(f"JWT Error: {str(e)}") 54 | raise credentials_exception 55 | 56 | if user is None: 57 | logger.error("User not found") 58 | raise credentials_exception 59 | 60 | return User(**user) 61 | -------------------------------------------------------------------------------- /app/config.py: -------------------------------------------------------------------------------- 1 | from pydantic_settings import BaseSettings, SettingsConfigDict 2 | from slowapi import Limiter 3 | from slowapi.util import get_remote_address 4 | from dotenv import load_dotenv 5 | load_dotenv() 6 | 7 | class Settings(BaseSettings): 8 | # Secret key for JWT encoding/decoding 9 | SECRET_KEY: str = "key" 10 | ALGORITHM: str = "HS256" 11 | ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 12 | 13 | # Redis configuration 14 | REDIS_HOST: str = 'localhost' 15 | REDIS_PORT: int = 6379 16 | REDIS_DB: int = 0 17 | 18 | # Rate limiter configuration 19 | RATE_LIMIT_STORAGE_URI: str = "redis://{redis_host}:{redis_port}" 20 | 21 | model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8") 22 | 23 | @property 24 | def rate_limit_storage_uri(self) -> str: 25 | return self.RATE_LIMIT_STORAGE_URI.format(redis_host=self.REDIS_HOST, redis_port=self.REDIS_PORT) 26 | 27 | 28 | # Create a global instance of the Settings 29 | settings = Settings() 30 | 31 | limiter = Limiter( 32 | key_func=get_remote_address, 33 | storage_uri=settings.rate_limit_storage_uri 34 | ) -------------------------------------------------------------------------------- /app/database.py: -------------------------------------------------------------------------------- 1 | import redis 2 | import json 3 | from app.config import settings 4 | from app.models import User 5 | 6 | # Initialize Redis client 7 | redis_client = redis.Redis(host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=settings.REDIS_DB) 8 | 9 | def get_user(username: str): 10 | user_data = redis_client.get(username) 11 | if user_data: 12 | return json.loads(user_data) 13 | return None 14 | 15 | def add_user(username: str, hashed_password: str): 16 | user_data = { 17 | "username": username, 18 | "hashed_password": hashed_password 19 | } 20 | redis_client.set(username, json.dumps(user_data)) 21 | 22 | return User(username=username, hashed_password=hashed_password) -------------------------------------------------------------------------------- /app/models.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | class User(BaseModel): 4 | username: str 5 | hashed_password: str 6 | 7 | class UserResponse(BaseModel): 8 | username: str 9 | 10 | class Token(BaseModel): 11 | access_token: str 12 | token_type: str 13 | 14 | -------------------------------------------------------------------------------- /commands/create-user.txt: -------------------------------------------------------------------------------- 1 | curl -X POST http://localhost:8000/register \ 2 | -H "Content-Type: application/x-www-form-urlencoded" \ 3 | -d "username=testuser&password=testpassword123" 4 | 5 | -------------------------------------------------------------------------------- /commands/login-user.txt: -------------------------------------------------------------------------------- 1 | curl -X POST http://localhost:8000/token \ 2 | -H "Content-Type: application/x-www-form-urlencoded" \ 3 | -d "username=testuser&password=testpassword123" 4 | 5 | -------------------------------------------------------------------------------- /commands/test-user.txt: -------------------------------------------------------------------------------- 1 | # Replace with the actual token received from the login command 2 | curl -X GET http://localhost:8000/users/me \ 3 | -H "Authorization: Bearer " 4 | 5 | -------------------------------------------------------------------------------- /configs/default.txt: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | 4 | location / { 5 | proxy_pass http://127.0.0.1:8000; 6 | proxy_set_header Host $host; 7 | proxy_set_header X-Real-IP $remote_addr; 8 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 9 | proxy_set_header X-Forwarded-Proto $scheme; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /configs/fastapi.conf: -------------------------------------------------------------------------------- 1 | [program:fastapi] 2 | command=/path/venv/bin/uvicorn main:app --host 127.0.0.1 --port 8000 3 | directory=/path/to/your/app 4 | user=tim 5 | autostart=true 6 | autorestart=true 7 | stdout_logfile=/var/log/fastapi.log 8 | stderr_logfile=/var/log/fastapi.err.log 9 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from slowapi.middleware import SlowAPIMiddleware 3 | from app.api.routes import router 4 | from app.config import limiter 5 | 6 | app = FastAPI() 7 | app.state.limiter = limiter 8 | app.add_middleware(SlowAPIMiddleware) 9 | 10 | app.include_router(router) 11 | 12 | # if __name__ == "__main__": 13 | # import uvicorn 14 | # uvicorn.run(app, host="0.0.0.0", port=8000) 15 | -------------------------------------------------------------------------------- /notes.md: -------------------------------------------------------------------------------- 1 | # Server Setup and FastAPI Deployment Guide 2 | 3 | ## 1. VSCode Setup 4 | 5 | 1. Install the "Remote - SSH" extension in VSCode 6 | 2. Install the "Python" extension in VSCode 7 | 3. Open a folder using Remote - SSH 8 | 9 | ## 2. Secure Your Server 10 | 11 | ### Disable Root Access 12 | 13 | 1. Create a new user: 14 | ``` 15 | adduser newusername 16 | ``` 17 | 18 | 2. Grant sudo privileges to the new user: 19 | ``` 20 | usermod -aG sudo newusername 21 | ``` 22 | 23 | 3. Edit the SSH configuration: 24 | ``` 25 | sudo nano /etc/ssh/sshd_config 26 | ``` 27 | 28 | 4. Locate the line `PermitRootLogin yes` and change it to: 29 | ``` 30 | PermitRootLogin no 31 | ``` 32 | 33 | 5. Save and exit the editor: 34 | - Press `Ctrl+X` 35 | - Press `Y` to confirm 36 | - Press `Enter` to save 37 | 38 | 6. Restart the SSH service: 39 | ``` 40 | sudo systemctl restart ssh 41 | ``` 42 | 43 | 7. Exit the current session: 44 | ``` 45 | exit 46 | ``` 47 | 48 | 8. Log in as the new user: 49 | ``` 50 | ssh newusername@ 51 | ``` 52 | 53 | ## 3. Install and Configure Redis 54 | 55 | 1. Update and upgrade your system: 56 | ``` 57 | sudo apt update && sudo apt upgrade 58 | ``` 59 | 60 | 2. Install Redis: 61 | ``` 62 | sudo apt install redis-server 63 | ``` 64 | 65 | 3. Start the Redis server: 66 | ``` 67 | sudo service redis-server start 68 | ``` 69 | 70 | 4. Verify Redis is running: 71 | ``` 72 | redis-cli ping 73 | ``` 74 | You should receive a "PONG" response. 75 | 76 | ### Redis Commands 77 | 78 | To clear the entire Redis database: 79 | ``` 80 | redis-cli FLUSHALL 81 | ``` 82 | 83 | ## 4. Python Environment Setup 84 | 85 | 1. Create and navigate to the project directory: 86 | ``` 87 | mkdir fastapi && cd fastapi 88 | ``` 89 | 90 | 2. Install Python virtual environment: 91 | ``` 92 | sudo apt install python3.11-venv 93 | ``` 94 | 95 | 3. Create a virtual environment: 96 | ``` 97 | python3 -m venv env 98 | ``` 99 | 100 | 4. Activate the virtual environment: 101 | ``` 102 | source env/bin/activate 103 | ``` 104 | 105 | 5. Create a requirements file: 106 | ``` 107 | touch requirements.txt 108 | ``` 109 | 110 | 6. Add the following content to `requirements.txt`: 111 | ``` 112 | fastapi 113 | uvicorn 114 | redis 115 | slowapi 116 | pydantic-settings 117 | python-dotenv 118 | python-jose 119 | passlib 120 | python-multipart 121 | ``` 122 | 123 | 7. Install the required packages: 124 | ``` 125 | pip3 install -r requirements.txt 126 | ``` 127 | 128 | ## 5. FastAPI Setup 129 | 130 | (Assuming you have already set up your FastAPI application files) 131 | 132 | ## 6. Running the FastAPI Application 133 | 134 | 1. Ensure you're in the project directory with the virtual environment activated. 135 | 2. Start the application: 136 | ``` 137 | python3 main.py 138 | ``` 139 | 3. Your application will be accessible at `http://localhost:8000`. 140 | 141 | ## 7. Testing the Application 142 | 143 | Use these curl commands to test your API: 144 | 145 | 1. Create a user: See [create-user.txt](commands/create-user.txt) 146 | 2. Login and get an access token: See [login-user.txt](commands/login-user.txt) 147 | 3. Test authenticated access: See [test-user.txt](commands/test-user.txt) 148 | 149 | **Note**: Replace `` in the test-user command with the actual token received from the login command. 150 | 151 | ## 8. Deployment 152 | 153 | 1. Install Nginx: 154 | ``` 155 | sudo apt install nginx 156 | ``` 157 | 158 | 2. Configure Nginx: 159 | ``` 160 | sudo nano /etc/nginx/sites-available/default 161 | ``` 162 | Replace the default config with the content from [default.txt](configs/default.txt) 163 | 164 | 3. Restart Nginx: 165 | ``` 166 | sudo systemctl restart nginx 167 | ``` 168 | 169 | 4. Install Supervisor: 170 | ``` 171 | sudo apt install supervisor 172 | ``` 173 | 174 | 5. Configure Supervisor: 175 | ``` 176 | sudo nano /etc/supervisor/conf.d/fastapi.conf 177 | ``` 178 | Replace the contents with [fastapi.conf](configs/fastapi.conf) 179 | - **Important**: Replace `user=tim` with the new user you created 180 | - **Important**: Replace `/path/venv/bin/uvicorn` with the path to your virtual environment 181 | - **Important**: Replace `/path/to/your/app` with the path to your application root dir 182 | 183 | command=/path/venv/bin/uvicorn main:app --host 127.0.0.1 --port 8000 184 | directory=/path/to/your/app 185 | 186 | 6. Update Supervisor: 187 | ``` 188 | sudo supervisorctl reread 189 | sudo supervisorctl update 190 | ``` 191 | 192 | 7. Start your FastAPI application: 193 | ``` 194 | sudo supervisorctl start fastapi 195 | ``` 196 | 197 | ## 9. Test Your Public API 198 | 199 | Your API should now be accessible via the public IP address of your Linode/Akamai instance. 200 | 201 | To test, replace `` with your actual server IP in the curl commands from step 7. 202 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | redis 4 | slowapi 5 | pydantic-settings 6 | python-dotenv 7 | python-jose 8 | passlib 9 | python-multipart --------------------------------------------------------------------------------