fastapi-forge-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastapi_forge/__init__.py +7 -0
- fastapi_forge/__main__.py +6 -0
- fastapi_forge/cli.py +211 -0
- fastapi_forge/templates/with_rbac/Dockerfile +31 -0
- fastapi_forge/templates/with_rbac/README.md +121 -0
- fastapi_forge/templates/with_rbac/_dockerignore +16 -0
- fastapi_forge/templates/with_rbac/_github/workflows/ci.yml +23 -0
- fastapi_forge/templates/with_rbac/_gitignore +19 -0
- fastapi_forge/templates/with_rbac/alembic/README +1 -0
- fastapi_forge/templates/with_rbac/alembic/__init__.py +1 -0
- fastapi_forge/templates/with_rbac/alembic/env.py +51 -0
- fastapi_forge/templates/with_rbac/alembic/script.py.mako +28 -0
- fastapi_forge/templates/with_rbac/alembic/versions/2255ba4f9604_fresh_baseline.py +204 -0
- fastapi_forge/templates/with_rbac/alembic.ini +35 -0
- fastapi_forge/templates/with_rbac/app/__init__.py +1 -0
- fastapi_forge/templates/with_rbac/app/api/__init__.py +1 -0
- fastapi_forge/templates/with_rbac/app/api/v1/__init__.py +1 -0
- fastapi_forge/templates/with_rbac/app/api/v1/api.py +34 -0
- fastapi_forge/templates/with_rbac/app/api/v1/audit_logs/__init__.py +1 -0
- fastapi_forge/templates/with_rbac/app/api/v1/audit_logs/repository.py +38 -0
- fastapi_forge/templates/with_rbac/app/api/v1/audit_logs/router.py +50 -0
- fastapi_forge/templates/with_rbac/app/api/v1/audit_logs/schema.py +21 -0
- fastapi_forge/templates/with_rbac/app/api/v1/auth/__init__.py +0 -0
- fastapi_forge/templates/with_rbac/app/api/v1/auth/repository.py +179 -0
- fastapi_forge/templates/with_rbac/app/api/v1/auth/router.py +209 -0
- fastapi_forge/templates/with_rbac/app/api/v1/auth/schema.py +98 -0
- fastapi_forge/templates/with_rbac/app/api/v1/auth/service.py +383 -0
- fastapi_forge/templates/with_rbac/app/api/v1/health/__init__.py +3 -0
- fastapi_forge/templates/with_rbac/app/api/v1/health/router.py +23 -0
- fastapi_forge/templates/with_rbac/app/api/v1/health/schema.py +5 -0
- fastapi_forge/templates/with_rbac/app/api/v1/health/service.py +25 -0
- fastapi_forge/templates/with_rbac/app/api/v1/permissions/__init__.py +0 -0
- fastapi_forge/templates/with_rbac/app/api/v1/permissions/repository.py +80 -0
- fastapi_forge/templates/with_rbac/app/api/v1/permissions/router.py +151 -0
- fastapi_forge/templates/with_rbac/app/api/v1/permissions/schema.py +40 -0
- fastapi_forge/templates/with_rbac/app/api/v1/permissions/service.py +156 -0
- fastapi_forge/templates/with_rbac/app/api/v1/roles/__init__.py +1 -0
- fastapi_forge/templates/with_rbac/app/api/v1/roles/repository.py +161 -0
- fastapi_forge/templates/with_rbac/app/api/v1/roles/router.py +169 -0
- fastapi_forge/templates/with_rbac/app/api/v1/roles/schema.py +51 -0
- fastapi_forge/templates/with_rbac/app/api/v1/roles/service.py +319 -0
- fastapi_forge/templates/with_rbac/app/api/v1/schema.py +7 -0
- fastapi_forge/templates/with_rbac/app/api/v1/users/__init__.py +1 -0
- fastapi_forge/templates/with_rbac/app/api/v1/users/repository.py +181 -0
- fastapi_forge/templates/with_rbac/app/api/v1/users/router.py +146 -0
- fastapi_forge/templates/with_rbac/app/api/v1/users/schema.py +112 -0
- fastapi_forge/templates/with_rbac/app/api/v1/users/service.py +291 -0
- fastapi_forge/templates/with_rbac/app/core/__init__.py +1 -0
- fastapi_forge/templates/with_rbac/app/core/config.py +131 -0
- fastapi_forge/templates/with_rbac/app/core/dependencies.py +131 -0
- fastapi_forge/templates/with_rbac/app/core/exceptions.py +162 -0
- fastapi_forge/templates/with_rbac/app/core/logging.py +231 -0
- fastapi_forge/templates/with_rbac/app/core/middleware.py +188 -0
- fastapi_forge/templates/with_rbac/app/core/responses.py +108 -0
- fastapi_forge/templates/with_rbac/app/core/security.py +115 -0
- fastapi_forge/templates/with_rbac/app/db/__init__.py +1 -0
- fastapi_forge/templates/with_rbac/app/db/base.py +5 -0
- fastapi_forge/templates/with_rbac/app/db/models/__init__.py +16 -0
- fastapi_forge/templates/with_rbac/app/db/models/audit_log.py +58 -0
- fastapi_forge/templates/with_rbac/app/db/models/auth_token.py +72 -0
- fastapi_forge/templates/with_rbac/app/db/models/notification.py +49 -0
- fastapi_forge/templates/with_rbac/app/db/models/permission.py +174 -0
- fastapi_forge/templates/with_rbac/app/db/models/revoked_token.py +21 -0
- fastapi_forge/templates/with_rbac/app/db/models/user.py +53 -0
- fastapi_forge/templates/with_rbac/app/db/schemas/__init__.py +8 -0
- fastapi_forge/templates/with_rbac/app/db/schemas/common.py +70 -0
- fastapi_forge/templates/with_rbac/app/db/schemas/names.py +9 -0
- fastapi_forge/templates/with_rbac/app/db/session.py +86 -0
- fastapi_forge/templates/with_rbac/app/helper/__init__.py +1 -0
- fastapi_forge/templates/with_rbac/app/helper/pagination_helper.py +44 -0
- fastapi_forge/templates/with_rbac/app/helper/search.py +51 -0
- fastapi_forge/templates/with_rbac/app/helper/sorting.py +77 -0
- fastapi_forge/templates/with_rbac/app/main.py +66 -0
- fastapi_forge/templates/with_rbac/app/repositories/__init__.py +1 -0
- fastapi_forge/templates/with_rbac/app/repositories/base.py +347 -0
- fastapi_forge/templates/with_rbac/app/services/__init__.py +1 -0
- fastapi_forge/templates/with_rbac/app/services/audit.py +58 -0
- fastapi_forge/templates/with_rbac/app/services/email.py +118 -0
- fastapi_forge/templates/with_rbac/app/services/notification.py +82 -0
- fastapi_forge/templates/with_rbac/app/templates/email/notification.html +7 -0
- fastapi_forge/templates/with_rbac/app/templates/email/password_reset.html +7 -0
- fastapi_forge/templates/with_rbac/app/templates/email/verify_email.html +7 -0
- fastapi_forge/templates/with_rbac/app/templates/email/welcome.html +6 -0
- fastapi_forge/templates/with_rbac/app/utils/casing.py +31 -0
- fastapi_forge/templates/with_rbac/compose.yaml +33 -0
- fastapi_forge/templates/with_rbac/pyproject.toml +14 -0
- fastapi_forge/templates/with_rbac/requirements-dev.txt +5 -0
- fastapi_forge/templates/with_rbac/requirements.txt +16 -0
- fastapi_forge/templates/with_rbac/sample.env +42 -0
- fastapi_forge/templates/with_rbac/scripts/seed_first_user.py +166 -0
- fastapi_forge/templates/with_rbac/tests/test_audit.py +42 -0
- fastapi_forge/templates/with_rbac/tests/test_config.py +27 -0
- fastapi_forge/templates/with_rbac/tests/test_generator.py +20 -0
- fastapi_forge/templates/with_rbac/tests/test_permissions.py +36 -0
- fastapi_forge/templates/with_rbac/tests/test_security.py +68 -0
- fastapi_forge/templates/without_rbac/Dockerfile +31 -0
- fastapi_forge/templates/without_rbac/README.md +106 -0
- fastapi_forge/templates/without_rbac/_dockerignore +16 -0
- fastapi_forge/templates/without_rbac/_github/workflows/ci.yml +23 -0
- fastapi_forge/templates/without_rbac/_gitignore +19 -0
- fastapi_forge/templates/without_rbac/alembic/README +1 -0
- fastapi_forge/templates/without_rbac/alembic/__init__.py +1 -0
- fastapi_forge/templates/without_rbac/alembic/env.py +51 -0
- fastapi_forge/templates/without_rbac/alembic/script.py.mako +28 -0
- fastapi_forge/templates/without_rbac/alembic/versions/2255ba4f9604_fresh_baseline.py +125 -0
- fastapi_forge/templates/without_rbac/alembic.ini +35 -0
- fastapi_forge/templates/without_rbac/app/__init__.py +1 -0
- fastapi_forge/templates/without_rbac/app/api/__init__.py +1 -0
- fastapi_forge/templates/without_rbac/app/api/v1/__init__.py +1 -0
- fastapi_forge/templates/without_rbac/app/api/v1/api.py +29 -0
- fastapi_forge/templates/without_rbac/app/api/v1/audit_logs/__init__.py +1 -0
- fastapi_forge/templates/without_rbac/app/api/v1/audit_logs/repository.py +38 -0
- fastapi_forge/templates/without_rbac/app/api/v1/audit_logs/router.py +45 -0
- fastapi_forge/templates/without_rbac/app/api/v1/audit_logs/schema.py +21 -0
- fastapi_forge/templates/without_rbac/app/api/v1/auth/__init__.py +0 -0
- fastapi_forge/templates/without_rbac/app/api/v1/auth/repository.py +127 -0
- fastapi_forge/templates/without_rbac/app/api/v1/auth/router.py +207 -0
- fastapi_forge/templates/without_rbac/app/api/v1/auth/schema.py +96 -0
- fastapi_forge/templates/without_rbac/app/api/v1/auth/service.py +373 -0
- fastapi_forge/templates/without_rbac/app/api/v1/health/__init__.py +3 -0
- fastapi_forge/templates/without_rbac/app/api/v1/health/router.py +23 -0
- fastapi_forge/templates/without_rbac/app/api/v1/health/schema.py +5 -0
- fastapi_forge/templates/without_rbac/app/api/v1/health/service.py +25 -0
- fastapi_forge/templates/without_rbac/app/api/v1/schema.py +7 -0
- fastapi_forge/templates/without_rbac/app/api/v1/users/__init__.py +1 -0
- fastapi_forge/templates/without_rbac/app/api/v1/users/repository.py +69 -0
- fastapi_forge/templates/without_rbac/app/api/v1/users/router.py +94 -0
- fastapi_forge/templates/without_rbac/app/api/v1/users/schema.py +50 -0
- fastapi_forge/templates/without_rbac/app/api/v1/users/service.py +92 -0
- fastapi_forge/templates/without_rbac/app/core/__init__.py +1 -0
- fastapi_forge/templates/without_rbac/app/core/config.py +131 -0
- fastapi_forge/templates/without_rbac/app/core/dependencies.py +71 -0
- fastapi_forge/templates/without_rbac/app/core/exceptions.py +162 -0
- fastapi_forge/templates/without_rbac/app/core/logging.py +231 -0
- fastapi_forge/templates/without_rbac/app/core/middleware.py +188 -0
- fastapi_forge/templates/without_rbac/app/core/responses.py +108 -0
- fastapi_forge/templates/without_rbac/app/core/security.py +115 -0
- fastapi_forge/templates/without_rbac/app/db/__init__.py +1 -0
- fastapi_forge/templates/without_rbac/app/db/base.py +5 -0
- fastapi_forge/templates/without_rbac/app/db/models/__init__.py +10 -0
- fastapi_forge/templates/without_rbac/app/db/models/audit_log.py +58 -0
- fastapi_forge/templates/without_rbac/app/db/models/auth_token.py +72 -0
- fastapi_forge/templates/without_rbac/app/db/models/notification.py +49 -0
- fastapi_forge/templates/without_rbac/app/db/models/revoked_token.py +21 -0
- fastapi_forge/templates/without_rbac/app/db/models/user.py +33 -0
- fastapi_forge/templates/without_rbac/app/db/schemas/__init__.py +8 -0
- fastapi_forge/templates/without_rbac/app/db/schemas/common.py +70 -0
- fastapi_forge/templates/without_rbac/app/db/schemas/names.py +5 -0
- fastapi_forge/templates/without_rbac/app/db/session.py +86 -0
- fastapi_forge/templates/without_rbac/app/helper/__init__.py +1 -0
- fastapi_forge/templates/without_rbac/app/helper/pagination_helper.py +44 -0
- fastapi_forge/templates/without_rbac/app/helper/search.py +51 -0
- fastapi_forge/templates/without_rbac/app/helper/sorting.py +77 -0
- fastapi_forge/templates/without_rbac/app/main.py +66 -0
- fastapi_forge/templates/without_rbac/app/repositories/__init__.py +1 -0
- fastapi_forge/templates/without_rbac/app/repositories/base.py +347 -0
- fastapi_forge/templates/without_rbac/app/services/__init__.py +1 -0
- fastapi_forge/templates/without_rbac/app/services/audit.py +58 -0
- fastapi_forge/templates/without_rbac/app/services/email.py +118 -0
- fastapi_forge/templates/without_rbac/app/services/notification.py +82 -0
- fastapi_forge/templates/without_rbac/app/templates/email/notification.html +7 -0
- fastapi_forge/templates/without_rbac/app/templates/email/password_reset.html +7 -0
- fastapi_forge/templates/without_rbac/app/templates/email/verify_email.html +7 -0
- fastapi_forge/templates/without_rbac/app/templates/email/welcome.html +6 -0
- fastapi_forge/templates/without_rbac/app/utils/casing.py +31 -0
- fastapi_forge/templates/without_rbac/compose.yaml +33 -0
- fastapi_forge/templates/without_rbac/pyproject.toml +14 -0
- fastapi_forge/templates/without_rbac/requirements-dev.txt +5 -0
- fastapi_forge/templates/without_rbac/requirements.txt +16 -0
- fastapi_forge/templates/without_rbac/sample.env +42 -0
- fastapi_forge/templates/without_rbac/scripts/seed_first_user.py +51 -0
- fastapi_forge/templates/without_rbac/tests/test_audit.py +42 -0
- fastapi_forge/templates/without_rbac/tests/test_config.py +27 -0
- fastapi_forge/templates/without_rbac/tests/test_generator.py +20 -0
- fastapi_forge/templates/without_rbac/tests/test_security.py +68 -0
- fastapi_forge_cli-0.1.0.dist-info/METADATA +225 -0
- fastapi_forge_cli-0.1.0.dist-info/RECORD +181 -0
- fastapi_forge_cli-0.1.0.dist-info/WHEEL +5 -0
- fastapi_forge_cli-0.1.0.dist-info/entry_points.txt +2 -0
- fastapi_forge_cli-0.1.0.dist-info/licenses/LICENSE +18 -0
- fastapi_forge_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
services:
|
|
2
|
+
db:
|
|
3
|
+
image: postgres:16-alpine
|
|
4
|
+
environment:
|
|
5
|
+
POSTGRES_DB: project_name
|
|
6
|
+
POSTGRES_USER: postgres
|
|
7
|
+
POSTGRES_PASSWORD: postgres
|
|
8
|
+
healthcheck:
|
|
9
|
+
test: ["CMD-SHELL", "pg_isready -U postgres -d project_name"]
|
|
10
|
+
interval: 5s
|
|
11
|
+
timeout: 5s
|
|
12
|
+
retries: 10
|
|
13
|
+
volumes:
|
|
14
|
+
- postgres-data:/var/lib/postgresql/data
|
|
15
|
+
|
|
16
|
+
api:
|
|
17
|
+
build: .
|
|
18
|
+
env_file: .env
|
|
19
|
+
environment:
|
|
20
|
+
PG_HOST: db
|
|
21
|
+
PG_SSLMODE: disable
|
|
22
|
+
command: ["sh", "-c", "alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload"]
|
|
23
|
+
depends_on:
|
|
24
|
+
db:
|
|
25
|
+
condition: service_healthy
|
|
26
|
+
ports:
|
|
27
|
+
- "8000:8000"
|
|
28
|
+
volumes:
|
|
29
|
+
- .:/app
|
|
30
|
+
|
|
31
|
+
volumes:
|
|
32
|
+
postgres-data:
|
|
33
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
[tool.pytest.ini_options]
|
|
2
|
+
testpaths = ["tests"]
|
|
3
|
+
asyncio_mode = "auto"
|
|
4
|
+
pythonpath = ["."]
|
|
5
|
+
|
|
6
|
+
[tool.ruff]
|
|
7
|
+
target-version = "py312"
|
|
8
|
+
line-length = 100
|
|
9
|
+
|
|
10
|
+
[tool.ruff.lint]
|
|
11
|
+
select = ["E9", "F63", "F7", "F82"]
|
|
12
|
+
|
|
13
|
+
[tool.ruff.lint.per-file-ignores]
|
|
14
|
+
"app/db/models/*.py" = ["F821"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
alembic>=1.13.0
|
|
2
|
+
fastapi>=0.111.0
|
|
3
|
+
asyncpg>=0.30.0
|
|
4
|
+
psycopg[binary]>=3.2.0
|
|
5
|
+
pydantic-settings>=2.0.0
|
|
6
|
+
pydantic[email]>=2.0.0
|
|
7
|
+
uvicorn[standard]>=0.29.0
|
|
8
|
+
SQLAlchemy>=2.0.30
|
|
9
|
+
python-jose[cryptography]>=3.3.0
|
|
10
|
+
passlib[bcrypt]>=1.7.4
|
|
11
|
+
bcrypt==4.0.1
|
|
12
|
+
python-multipart>=0.0.9
|
|
13
|
+
slowapi>=0.1.9
|
|
14
|
+
redis>=5.0.0
|
|
15
|
+
fastapi-mail>=1.4.1
|
|
16
|
+
structlog>=24.0.0
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
APP_NAME=__PROJECT_NAME__
|
|
2
|
+
APP_VERSION=1.0.0
|
|
3
|
+
APP_ENV=development
|
|
4
|
+
APP_DEBUG=false
|
|
5
|
+
SECRET_KEY=replace-with-a-long-random-secret
|
|
6
|
+
|
|
7
|
+
PG_HOST=localhost
|
|
8
|
+
PG_PORT=5432
|
|
9
|
+
PG_DATABASE=project_name
|
|
10
|
+
PG_USER=postgres
|
|
11
|
+
PG_PASSWORD=postgres
|
|
12
|
+
PG_SSLMODE=prefer
|
|
13
|
+
|
|
14
|
+
ACCESS_TOKEN_EXPIRE_MINUTES=30
|
|
15
|
+
REFRESH_TOKEN_EXPIRE_DAYS=7
|
|
16
|
+
PASSWORD_RESET_TOKEN_EXPIRE_MINUTES=15
|
|
17
|
+
ALGORITHM=HS256
|
|
18
|
+
|
|
19
|
+
ALLOWED_ORIGINS=http://localhost:3000
|
|
20
|
+
ALLOWED_HOSTS=localhost,127.0.0.1
|
|
21
|
+
|
|
22
|
+
USE_REDIS=false
|
|
23
|
+
REDIS_URL=
|
|
24
|
+
RATE_LIMIT_PER_MINUTE=60
|
|
25
|
+
LOG_DIR=logs
|
|
26
|
+
LOG_TO_FILES=false
|
|
27
|
+
|
|
28
|
+
# Database pool tuning
|
|
29
|
+
DATABASE_POOL_SIZE=10
|
|
30
|
+
DATABASE_MAX_OVERFLOW=20
|
|
31
|
+
DATABASE_POOL_TIMEOUT=30
|
|
32
|
+
DATABASE_DEBUG=false
|
|
33
|
+
|
|
34
|
+
MAIL_USERNAME=
|
|
35
|
+
MAIL_PASSWORD=
|
|
36
|
+
MAIL_FROM=noreply@example.com
|
|
37
|
+
MAIL_PORT=587
|
|
38
|
+
MAIL_SERVER=smtp.example.com
|
|
39
|
+
MAIL_STARTTLS=true
|
|
40
|
+
MAIL_SSL_TLS=false
|
|
41
|
+
MAIL_FROM_NAME=__PROJECT_NAME__
|
|
42
|
+
FRONTEND_URL=http://localhost:3000
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
6
|
+
if str(PROJECT_ROOT) not in sys.path:
|
|
7
|
+
sys.path.insert(0, str(PROJECT_ROOT))
|
|
8
|
+
|
|
9
|
+
from sqlalchemy import select
|
|
10
|
+
|
|
11
|
+
from app.core.security import hash_password
|
|
12
|
+
from app.db.models import User
|
|
13
|
+
from app.db.session import SyncSessionLocal
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def username_from_email(email: str) -> str:
|
|
17
|
+
return email.split("@", 1)[0].replace(".", "_").replace("-", "_").lower()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main() -> None:
|
|
21
|
+
parser = argparse.ArgumentParser(description="Create the first superuser.")
|
|
22
|
+
parser.add_argument("--email", required=True)
|
|
23
|
+
parser.add_argument("--password", required=True)
|
|
24
|
+
parser.add_argument("--full-name", required=True)
|
|
25
|
+
args = parser.parse_args()
|
|
26
|
+
|
|
27
|
+
with SyncSessionLocal() as session:
|
|
28
|
+
user = session.scalar(
|
|
29
|
+
select(User).where(User.email == args.email.lower())
|
|
30
|
+
)
|
|
31
|
+
if user is None:
|
|
32
|
+
user = User(
|
|
33
|
+
email=args.email.lower(),
|
|
34
|
+
username=username_from_email(args.email),
|
|
35
|
+
full_name=args.full_name,
|
|
36
|
+
hashed_password=hash_password(args.password),
|
|
37
|
+
)
|
|
38
|
+
session.add(user)
|
|
39
|
+
user.full_name = args.full_name
|
|
40
|
+
user.hashed_password = hash_password(args.password)
|
|
41
|
+
user.is_active = True
|
|
42
|
+
user.is_superuser = True
|
|
43
|
+
user.is_verified = True
|
|
44
|
+
user.restore()
|
|
45
|
+
session.commit()
|
|
46
|
+
print(f"SUPERUSER ready: {user.email}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__":
|
|
50
|
+
main()
|
|
51
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from uuid import uuid4
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from app.db.models.audit_log import AuditLog
|
|
6
|
+
from app.services.audit import AuditService
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FakeSession:
|
|
10
|
+
def __init__(self):
|
|
11
|
+
self.added = []
|
|
12
|
+
self.flushed = False
|
|
13
|
+
|
|
14
|
+
def add(self, value):
|
|
15
|
+
self.added.append(value)
|
|
16
|
+
|
|
17
|
+
async def flush(self):
|
|
18
|
+
self.flushed = True
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.mark.asyncio
|
|
22
|
+
async def test_audit_service_persists_context():
|
|
23
|
+
session = FakeSession()
|
|
24
|
+
user_id = uuid4()
|
|
25
|
+
|
|
26
|
+
entry = await AuditService(session).log(
|
|
27
|
+
action="users.update",
|
|
28
|
+
resource="users",
|
|
29
|
+
resource_id="target-user",
|
|
30
|
+
user_id=user_id,
|
|
31
|
+
old_values={"is_active": True},
|
|
32
|
+
new_values={"is_active": False},
|
|
33
|
+
metadata={"reason": "offboarding"},
|
|
34
|
+
request_id="request-123",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
assert isinstance(entry, AuditLog)
|
|
38
|
+
assert entry.user_id == user_id
|
|
39
|
+
assert entry.log_metadata == {"reason": "offboarding"}
|
|
40
|
+
assert entry.request_id == "request-123"
|
|
41
|
+
assert session.added == [entry]
|
|
42
|
+
assert session.flushed
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from pydantic import ValidationError
|
|
3
|
+
|
|
4
|
+
from app.core.config import Settings
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_database_urls_escape_credentials_and_enable_tls():
|
|
8
|
+
configured = Settings(pg_password="p@ss", pg_sslmode="require")
|
|
9
|
+
|
|
10
|
+
assert "%40" in configured.postgres_url
|
|
11
|
+
assert "ssl=require" in configured.postgres_async_url
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_production_rejects_unsafe_defaults():
|
|
15
|
+
with pytest.raises(ValidationError):
|
|
16
|
+
Settings(APP_ENV="production", _env_file=None)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_production_accepts_explicit_secure_settings():
|
|
20
|
+
Settings(
|
|
21
|
+
APP_ENV="production",
|
|
22
|
+
SECRET_KEY="x" * 48,
|
|
23
|
+
ALLOWED_HOSTS=["api.example.com"],
|
|
24
|
+
ALLOWED_ORIGINS=["https://example.com"],
|
|
25
|
+
pg_sslmode="verify-full",
|
|
26
|
+
_env_file=None,
|
|
27
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_generated_non_rbac_project_structure():
|
|
9
|
+
assert not (PROJECT_ROOT / "app/api/v1/roles").exists()
|
|
10
|
+
assert not (PROJECT_ROOT / "app/api/v1/permissions").exists()
|
|
11
|
+
assert not (PROJECT_ROOT / "app/db/models/permission.py").exists()
|
|
12
|
+
assert (PROJECT_ROOT / "Dockerfile").exists()
|
|
13
|
+
assert (PROJECT_ROOT / ".github/workflows/ci.yml").exists()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_python_sources_compile():
|
|
17
|
+
for path in PROJECT_ROOT.rglob("*.py"):
|
|
18
|
+
source = path.read_text(encoding="utf-8")
|
|
19
|
+
ast.parse(source, filename=str(path))
|
|
20
|
+
compile(source, str(path), "exec")
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from app.core.security import (
|
|
2
|
+
create_access_token,
|
|
3
|
+
create_refresh_token,
|
|
4
|
+
decode_token,
|
|
5
|
+
hash_password,
|
|
6
|
+
hash_opaque_token,
|
|
7
|
+
verify_password,
|
|
8
|
+
)
|
|
9
|
+
from app.api.v1.auth.schema import TokenResponse, UserResponse
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_password_hash_round_trip():
|
|
13
|
+
password = "StrongPassword123!"
|
|
14
|
+
hashed = hash_password(password)
|
|
15
|
+
|
|
16
|
+
assert hashed != password
|
|
17
|
+
assert verify_password(password, hashed)
|
|
18
|
+
assert not verify_password("wrong-password", hashed)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_access_token_round_trip():
|
|
22
|
+
token, _ = create_access_token(
|
|
23
|
+
"user-123",
|
|
24
|
+
{"permissions": ["users:read"], "role": "ADMIN"},
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
payload = decode_token(token, expected_type="access")
|
|
28
|
+
|
|
29
|
+
assert payload["sub"] == "user-123"
|
|
30
|
+
assert payload["permissions"] == ["users:read"]
|
|
31
|
+
assert payload["role"] == "ADMIN"
|
|
32
|
+
assert payload["jti"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_refresh_token_round_trip():
|
|
36
|
+
token, _ = create_refresh_token("user-123")
|
|
37
|
+
|
|
38
|
+
payload = decode_token(token, expected_type="refresh")
|
|
39
|
+
|
|
40
|
+
assert payload["sub"] == "user-123"
|
|
41
|
+
assert payload["type"] == "refresh"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_opaque_token_hash_is_deterministic_and_one_way():
|
|
45
|
+
token = "secret-reset-token"
|
|
46
|
+
|
|
47
|
+
assert hash_opaque_token(token) == hash_opaque_token(token)
|
|
48
|
+
assert hash_opaque_token(token) != token
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_refresh_token_is_excluded_from_serialized_login_response():
|
|
52
|
+
user = UserResponse(
|
|
53
|
+
id="user-123",
|
|
54
|
+
email="user@example.com",
|
|
55
|
+
username="user",
|
|
56
|
+
full_name="Example User",
|
|
57
|
+
is_active=True,
|
|
58
|
+
is_superuser=False,
|
|
59
|
+
is_verified=True,
|
|
60
|
+
)
|
|
61
|
+
response = TokenResponse(
|
|
62
|
+
access_token="access",
|
|
63
|
+
refresh_token="refresh-secret",
|
|
64
|
+
expires_in=1800,
|
|
65
|
+
user=user,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
assert "refresh_token" not in response.model_dump()
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastapi-forge-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generate production-oriented FastAPI projects with optional RBAC.
|
|
5
|
+
Author: FastAPI Forge Contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: fastapi,boilerplate,generator,rbac,cli
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Framework :: FastAPI
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: build<2.0,>=1.2; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest<9.0,>=8.0; extra == "dev"
|
|
22
|
+
Requires-Dist: ruff<1.0,>=0.8; extra == "dev"
|
|
23
|
+
Requires-Dist: twine<7.0,>=5.1; extra == "dev"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# FastAPI Forge
|
|
27
|
+
|
|
28
|
+
FastAPI Forge is a zero-runtime-dependency CLI that generates production-oriented FastAPI projects with PostgreSQL, Alembic migrations, JWT authentication, audit logging, and optional role-based access control (RBAC).
|
|
29
|
+
|
|
30
|
+
## Generated features
|
|
31
|
+
|
|
32
|
+
- Access and rotating refresh tokens, revocation, and session management
|
|
33
|
+
- Password reset and email verification flows
|
|
34
|
+
- PostgreSQL with sync and async SQLAlchemy sessions
|
|
35
|
+
- Alembic migrations and an idempotent first-user seed command
|
|
36
|
+
- Structured logs, request IDs, rate limiting, CORS, trusted hosts, and security headers
|
|
37
|
+
- Liveness, readiness, and database health endpoints
|
|
38
|
+
- A multi-stage, non-root Docker image and local Compose stack
|
|
39
|
+
- Pytest tests, Ruff checks, and GitHub Actions CI
|
|
40
|
+
|
|
41
|
+
The RBAC variant adds hierarchical roles, permissions, per-user permissions, protected administration APIs, and RBAC seed data. The non-RBAC variant uses `is_superuser` for administrative authorization.
|
|
42
|
+
|
|
43
|
+
## Requirements
|
|
44
|
+
|
|
45
|
+
- Python 3.9+ for the generator
|
|
46
|
+
- Python 3.12+ and PostgreSQL 14+ for generated applications
|
|
47
|
+
- Docker with Compose v2 for the optional container workflow
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
Install the CLI in an isolated environment from a published package:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pipx install fastapi-forge-cli
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Or install it into the current Python environment:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
python -m pip install fastapi-forge-cli
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The PyPI distribution is named `fastapi-forge-cli` because `fastapi-forge` is
|
|
64
|
+
already used by another project. The installed terminal command remains
|
|
65
|
+
`fastapi-forge`, and the Python import remains `fastapi_forge`.
|
|
66
|
+
|
|
67
|
+
For development from a source checkout:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
cd fastapi-forge
|
|
71
|
+
python3 -m venv .venv
|
|
72
|
+
source .venv/bin/activate
|
|
73
|
+
python -m pip install --upgrade pip
|
|
74
|
+
pip install -e .
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
For generator development, run `pip install -e ".[dev]"`. On Windows PowerShell, activate with `.venv\\Scripts\\Activate.ps1`.
|
|
78
|
+
|
|
79
|
+
## Generate a project
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# With RBAC
|
|
83
|
+
fastapi-forge create "Inventory API" --with-rbac
|
|
84
|
+
|
|
85
|
+
# Without RBAC
|
|
86
|
+
fastapi-forge create "Inventory API" --without-rbac
|
|
87
|
+
|
|
88
|
+
# Select a parent directory
|
|
89
|
+
fastapi-forge create "Inventory API" --with-rbac --output-dir ./projects
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The same generator is available as a Python library:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from pathlib import Path
|
|
96
|
+
|
|
97
|
+
from fastapi_forge import create_project
|
|
98
|
+
|
|
99
|
+
project = create_project(
|
|
100
|
+
name="Inventory API",
|
|
101
|
+
output_dir=Path("./projects"),
|
|
102
|
+
with_rbac=True,
|
|
103
|
+
)
|
|
104
|
+
print(project)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Generation is staged before installation at the destination. Existing projects are
|
|
108
|
+
preserved unless `--force` is explicitly supplied, and symbolic-link destinations
|
|
109
|
+
are never replaced.
|
|
110
|
+
|
|
111
|
+
If neither RBAC option is supplied, an interactive terminal asks which variant to use. Non-interactive environments default to RBAC. Existing destinations are protected; `--force` permanently replaces the matching generated-project directory.
|
|
112
|
+
|
|
113
|
+
```text
|
|
114
|
+
usage: fastapi-forge create [-h] [--output-dir OUTPUT_DIR]
|
|
115
|
+
[--with-rbac | --without-rbac] [--force]
|
|
116
|
+
name
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Getting help
|
|
120
|
+
|
|
121
|
+
Discover available commands:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
fastapi-forge --help
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
See every project-generation option:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
fastapi-forge create --help
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Print copy-ready examples:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
fastapi-forge examples
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Common commands include:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
fastapi-forge create "Inventory API" --with-rbac
|
|
143
|
+
fastapi-forge create "Public API" --without-rbac
|
|
144
|
+
fastapi-forge create "Billing API" --with-rbac --output-dir ./projects
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Run a generated project
|
|
148
|
+
|
|
149
|
+
### Python
|
|
150
|
+
|
|
151
|
+
Create the PostgreSQL database described by `sample.env`, then:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
cd inventory-api
|
|
155
|
+
python3.12 -m venv .venv
|
|
156
|
+
source .venv/bin/activate
|
|
157
|
+
python -m pip install --upgrade pip
|
|
158
|
+
pip install -r requirements-dev.txt
|
|
159
|
+
cp sample.env .env
|
|
160
|
+
alembic upgrade head
|
|
161
|
+
python scripts/seed_first_user.py \
|
|
162
|
+
--email admin@example.com \
|
|
163
|
+
--password 'ChangeMe123!' \
|
|
164
|
+
--full-name 'System Administrator'
|
|
165
|
+
uvicorn app.main:app --reload
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Open `http://localhost:8000/docs`. Run `pytest` and `ruff check .` before committing.
|
|
169
|
+
|
|
170
|
+
### Docker
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
cd inventory-api
|
|
174
|
+
cp sample.env .env
|
|
175
|
+
docker compose up --build
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
In another terminal:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
docker compose exec api python scripts/seed_first_user.py \
|
|
182
|
+
--email admin@example.com \
|
|
183
|
+
--password 'ChangeMe123!' \
|
|
184
|
+
--full-name 'System Administrator'
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The Compose file is for local development; it uses development credentials and a bind mount.
|
|
188
|
+
|
|
189
|
+
## Production deployment checklist
|
|
190
|
+
|
|
191
|
+
1. Set `APP_ENV=production`.
|
|
192
|
+
2. Supply a unique `SECRET_KEY` of at least 32 characters from a secret manager.
|
|
193
|
+
3. Set explicit `ALLOWED_HOSTS` and HTTPS `ALLOWED_ORIGINS` values.
|
|
194
|
+
4. Use managed PostgreSQL credentials and `PG_SSLMODE=require`, `verify-ca`, or `verify-full`.
|
|
195
|
+
5. Configure Redis when rate limits must be shared across replicas.
|
|
196
|
+
6. Configure SMTP and `FRONTEND_URL` for account emails.
|
|
197
|
+
7. Run `alembic upgrade head` as one release job before starting new replicas.
|
|
198
|
+
8. Terminate TLS at a trusted load balancer or reverse proxy.
|
|
199
|
+
9. Send stdout/stderr to centralized logging. Enable file logs only with persistent storage.
|
|
200
|
+
10. Monitor `/api/v1/health/live` and `/api/v1/health/ready`.
|
|
201
|
+
11. Back up PostgreSQL, test restores, rotate secrets, and define rollback procedures.
|
|
202
|
+
|
|
203
|
+
Production mode refuses startup with a default/short secret, wildcard hosts or origins, or a non-TLS database mode. Interactive API documentation is disabled.
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
docker build -t inventory-api:1.0.0 .
|
|
207
|
+
docker run --rm --env-file .env inventory-api:1.0.0 alembic upgrade head
|
|
208
|
+
docker run --rm --env-file .env -p 8000:8000 inventory-api:1.0.0
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Do not run migrations independently from every application replica.
|
|
212
|
+
|
|
213
|
+
## Generator development and release
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
pytest
|
|
217
|
+
python -m build
|
|
218
|
+
python -m twine check dist/*
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Review generated output from both variants whenever templates change.
|
|
222
|
+
|
|
223
|
+
## License
|
|
224
|
+
|
|
225
|
+
MIT
|