Skip to content

Commit 403fa52

Browse files
authored
Merge pull request #26 from igorbenav/password-hidden-docs
Password hidden docs
2 parents af28623 + 4359e50 commit 403fa52

File tree

3 files changed

+105
-13
lines changed

3 files changed

+105
-13
lines changed

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
- [ ] Add mongoDB support
6666

6767
#### Security
68-
- [ ] FastAPI docs behind authentication and hidden based on the environment
68+
- [x] FastAPI docs behind authentication and hidden based on the environment
6969

7070
#### Structure
7171
- [ ] Remove python-decouple in favor of starlette.config
@@ -206,6 +206,16 @@ TEST_USERNAME="testeruser"
206206
TEST_PASSWORD="Str1ng$t"
207207
```
208208

209+
And Finally the environment:
210+
```
211+
# ------------- environment -------------
212+
ENVIRONMENT="local"
213+
```
214+
`ENVIRONMENT` can be one of `local`, `staging` and `production`, defaults to local, and changes the behavior of api `docs` endpoints:
215+
- **local:** `/docs`, `/redoc` and `/openapi.json` available
216+
- **staging:** `/docs`, `/redoc` and `/openapi.json` available for superusers
217+
- **production:** `/docs`, `/redoc` and `/openapi.json` not available
218+
209219
### 3.2 Docker Compose (preferred)
210220
To do it using docker compose, ensure you have docker and docker compose installed, then:
211221
While in the base project directory (FastAPI-boilerplate here), run:
@@ -763,6 +773,9 @@ Should be changed to:
763773
command: gunicorn app.main:app -w 4 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000
764774
```
765775

776+
> **Warning**
777+
> Do not forget to set the `ENVIRONMENT` in `.env` to `production` unless you want the API docs to be public.
778+
766779
More on running it in production later.
767780

768781
## 7. Testing

src/app/core/config.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from enum import Enum
2+
13
from decouple import config
24
from pydantic_settings import BaseSettings
35

@@ -79,6 +81,16 @@ class RedisQueueSettings(BaseSettings):
7981
REDIS_QUEUE_PORT: str = config("REDIS_QUEUE_PORT", default=6379)
8082

8183

84+
class EnvironmentOption(Enum):
85+
LOCAL = "local"
86+
STAGING = "staging"
87+
PRODUCTION = "production"
88+
89+
90+
class EnvironmentSettings(BaseSettings):
91+
ENVIRONMENT: EnvironmentOption = config("ENVIRONMENT", default="local")
92+
93+
8294
class Settings(
8395
AppSettings,
8496
PostgresSettings,
@@ -87,7 +99,8 @@ class Settings(
8799
TestSettings,
88100
RedisCacheSettings,
89101
ClientSideCacheSettings,
90-
RedisQueueSettings
102+
RedisQueueSettings,
103+
EnvironmentSettings
91104
):
92105
pass
93106

src/app/main.py

Lines changed: 77 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
from fastapi import FastAPI
1+
from fastapi import FastAPI, APIRouter, Depends
2+
from fastapi.openapi.docs import get_swagger_ui_html, get_redoc_html
3+
from fastapi.openapi.utils import get_openapi
24
import redis.asyncio as redis
35
from arq import create_pool
46
from arq.connections import RedisSettings
57

68
from app.api import router
9+
from app.api.dependencies import get_current_superuser
710
from app.core import cache, queue
811
from app.core.database import Base
912
from app.core.database import async_engine as engine
@@ -13,7 +16,9 @@
1316
RedisCacheSettings,
1417
AppSettings,
1518
ClientSideCacheSettings,
16-
RedisQueueSettings
19+
RedisQueueSettings,
20+
EnvironmentOption,
21+
EnvironmentSettings
1722
)
1823

1924
# -------------- database --------------
@@ -44,22 +49,60 @@ async def close_redis_queue_pool():
4449

4550

4651
# -------------- application --------------
47-
def create_application() -> FastAPI:
52+
def create_application(settings, **kwargs) -> FastAPI:
53+
"""
54+
Creates and configures a FastAPI application based on the provided keyword arguments.
55+
56+
The function initializes a FastAPI application and conditionally configures it
57+
with various settings and handlers. The configuration is determined by the type
58+
of settings object provided.
59+
60+
Parameters
61+
----------
62+
settings
63+
The settings object can be an instance of one or more of the following:
64+
- AppSettings: Configures basic app information like name, description, contact, and license info.
65+
- DatabaseSettings: Adds event handlers related to database tables during startup.
66+
- RedisCacheSettings: Adds event handlers for creating and closing Redis cache pool.
67+
- ClientSideCacheSettings: Adds middleware for client-side caching.
68+
- RedisQueueSettings: Adds event handlers for creating and closing Redis queue pool.
69+
- EnvironmentSettings: Sets documentation URLs and sets up custom routes for documentation.
70+
71+
**kwargs
72+
Additional keyword arguments that are passed directly to the FastAPI constructor.
73+
74+
Returns
75+
-------
76+
FastAPI: A configured FastAPI application instance.
77+
"""
78+
79+
# --- before creating application ---
4880
if isinstance(settings, AppSettings):
49-
application = FastAPI(
50-
title=settings.APP_NAME,
51-
description=settings.APP_DESCRIPTION,
52-
contact={
81+
to_update = {
82+
"title": settings.APP_NAME,
83+
"description": settings.APP_DESCRIPTION,
84+
"contact": {
5385
"name": settings.CONTACT_NAME,
5486
"email": settings.CONTACT_EMAIL
5587
},
56-
license_info={
88+
"license_info": {
5789
"name": settings.LICENSE_NAME
5890
}
91+
}
92+
kwargs.update(to_update)
93+
94+
if isinstance(settings, EnvironmentSettings):
95+
kwargs.update(
96+
{
97+
"docs_url": None,
98+
"redoc_url": None,
99+
"openapi_url": None
100+
}
59101
)
60-
else:
61-
application = FastAPI()
62102

103+
application = FastAPI(**kwargs)
104+
105+
# --- application created ---
63106
application.include_router(router)
64107

65108
if isinstance(settings, DatabaseSettings):
@@ -76,7 +119,30 @@ def create_application() -> FastAPI:
76119
application.add_event_handler("startup", create_redis_queue_pool)
77120
application.add_event_handler("shutdown", close_redis_queue_pool)
78121

122+
if isinstance(settings, EnvironmentSettings):
123+
if settings.ENVIRONMENT is not EnvironmentOption.PRODUCTION:
124+
docs_router = APIRouter()
125+
if settings.ENVIRONMENT is not EnvironmentOption.LOCAL:
126+
docs_router = APIRouter(dependencies=[Depends(get_current_superuser)])
127+
128+
@docs_router.get("/docs", include_in_schema=False)
129+
async def get_swagger_documentation():
130+
return get_swagger_ui_html(openapi_url="/openapi.json", title="docs")
131+
132+
133+
@docs_router.get("/redoc", include_in_schema=False)
134+
async def get_redoc_documentation():
135+
return get_redoc_html(openapi_url="/openapi.json", title="docs")
136+
137+
138+
@docs_router.get("/openapi.json", include_in_schema=False)
139+
async def openapi():
140+
return get_openapi(title=app.title, version=app.version, routes=app.routes)
141+
142+
143+
application.include_router(docs_router)
144+
79145
return application
80146

81147

82-
app = create_application()
148+
app = create_application(settings=settings)

0 commit comments

Comments
 (0)