@malamute/ai-rules 1.0.0 → 1.2.0

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.
Files changed (133) hide show
  1. package/README.md +270 -121
  2. package/bin/cli.js +5 -2
  3. package/configs/_shared/.claude/rules/conventions/documentation.md +324 -0
  4. package/configs/_shared/.claude/rules/conventions/git.md +265 -0
  5. package/configs/_shared/.claude/rules/{performance.md → conventions/performance.md} +1 -1
  6. package/configs/_shared/.claude/rules/conventions/principles.md +334 -0
  7. package/configs/_shared/.claude/rules/devops/ci-cd.md +262 -0
  8. package/configs/_shared/.claude/rules/devops/docker.md +275 -0
  9. package/configs/_shared/.claude/rules/devops/nx.md +194 -0
  10. package/configs/_shared/.claude/rules/domain/backend/api-design.md +203 -0
  11. package/configs/_shared/.claude/rules/lang/csharp/async.md +220 -0
  12. package/configs/_shared/.claude/rules/lang/csharp/csharp.md +314 -0
  13. package/configs/_shared/.claude/rules/lang/csharp/linq.md +210 -0
  14. package/configs/_shared/.claude/rules/lang/python/async.md +337 -0
  15. package/configs/_shared/.claude/rules/lang/python/celery.md +476 -0
  16. package/configs/_shared/.claude/rules/lang/python/config.md +339 -0
  17. package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/database/sqlalchemy.md +6 -1
  18. package/configs/_shared/.claude/rules/lang/python/deployment.md +523 -0
  19. package/configs/_shared/.claude/rules/lang/python/error-handling.md +330 -0
  20. package/configs/_shared/.claude/rules/lang/python/migrations.md +421 -0
  21. package/configs/_shared/.claude/rules/lang/python/python.md +172 -0
  22. package/configs/_shared/.claude/rules/lang/python/repository.md +383 -0
  23. package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/testing.md +2 -69
  24. package/configs/_shared/.claude/rules/lang/typescript/async.md +447 -0
  25. package/configs/_shared/.claude/rules/lang/typescript/generics.md +356 -0
  26. package/configs/_shared/.claude/rules/lang/typescript/typescript.md +212 -0
  27. package/configs/_shared/.claude/rules/quality/error-handling.md +48 -0
  28. package/configs/_shared/.claude/rules/quality/logging.md +45 -0
  29. package/configs/_shared/.claude/rules/quality/observability.md +240 -0
  30. package/configs/_shared/.claude/rules/quality/testing-patterns.md +65 -0
  31. package/configs/_shared/.claude/rules/security/secrets-management.md +222 -0
  32. package/configs/_shared/.claude/skills/analysis/explore/SKILL.md +257 -0
  33. package/configs/_shared/.claude/skills/analysis/security-audit/SKILL.md +184 -0
  34. package/configs/_shared/.claude/skills/dev/api-endpoint/SKILL.md +126 -0
  35. package/configs/_shared/.claude/{commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
  36. package/configs/_shared/.claude/{commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
  37. package/configs/_shared/.claude/{commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
  38. package/configs/_shared/.claude/skills/infra/deploy/SKILL.md +139 -0
  39. package/configs/_shared/.claude/skills/infra/docker/SKILL.md +95 -0
  40. package/configs/_shared/.claude/skills/infra/migration/SKILL.md +158 -0
  41. package/configs/_shared/.claude/skills/nx/nx-affected/SKILL.md +72 -0
  42. package/configs/_shared/.claude/skills/nx/nx-lib/SKILL.md +375 -0
  43. package/configs/_shared/CLAUDE.md +52 -149
  44. package/configs/angular/.claude/rules/{components.md → core/components.md} +69 -15
  45. package/configs/angular/.claude/rules/core/resource.md +285 -0
  46. package/configs/angular/.claude/rules/core/signals.md +323 -0
  47. package/configs/angular/.claude/rules/http.md +338 -0
  48. package/configs/angular/.claude/rules/routing.md +291 -0
  49. package/configs/angular/.claude/rules/ssr.md +312 -0
  50. package/configs/angular/.claude/rules/state/signal-store.md +408 -0
  51. package/configs/angular/.claude/rules/{state.md → state/state.md} +2 -2
  52. package/configs/angular/.claude/rules/testing.md +7 -7
  53. package/configs/angular/.claude/rules/ui/aria.md +422 -0
  54. package/configs/angular/.claude/rules/ui/forms.md +424 -0
  55. package/configs/angular/.claude/rules/ui/pipes-directives.md +335 -0
  56. package/configs/angular/.claude/settings.json +1 -0
  57. package/configs/angular/.claude/skills/ngrx-slice/SKILL.md +362 -0
  58. package/configs/angular/.claude/skills/signal-store/SKILL.md +445 -0
  59. package/configs/angular/CLAUDE.md +24 -216
  60. package/configs/dotnet/.claude/rules/background-services.md +552 -0
  61. package/configs/dotnet/.claude/rules/configuration.md +426 -0
  62. package/configs/dotnet/.claude/rules/ddd.md +447 -0
  63. package/configs/dotnet/.claude/rules/dependency-injection.md +343 -0
  64. package/configs/dotnet/.claude/rules/mediatr.md +320 -0
  65. package/configs/dotnet/.claude/rules/middleware.md +489 -0
  66. package/configs/dotnet/.claude/rules/result-pattern.md +363 -0
  67. package/configs/dotnet/.claude/rules/validation.md +388 -0
  68. package/configs/dotnet/.claude/settings.json +21 -3
  69. package/configs/dotnet/CLAUDE.md +53 -286
  70. package/configs/fastapi/.claude/rules/background-tasks.md +254 -0
  71. package/configs/fastapi/.claude/rules/dependencies.md +170 -0
  72. package/configs/{python → fastapi}/.claude/rules/fastapi.md +61 -1
  73. package/configs/fastapi/.claude/rules/lifespan.md +274 -0
  74. package/configs/fastapi/.claude/rules/middleware.md +229 -0
  75. package/configs/fastapi/.claude/rules/pydantic.md +433 -0
  76. package/configs/fastapi/.claude/rules/responses.md +251 -0
  77. package/configs/fastapi/.claude/rules/routers.md +202 -0
  78. package/configs/fastapi/.claude/rules/security.md +222 -0
  79. package/configs/fastapi/.claude/rules/testing.md +251 -0
  80. package/configs/fastapi/.claude/rules/websockets.md +298 -0
  81. package/configs/fastapi/.claude/settings.json +33 -0
  82. package/configs/fastapi/CLAUDE.md +144 -0
  83. package/configs/flask/.claude/rules/blueprints.md +208 -0
  84. package/configs/flask/.claude/rules/cli.md +285 -0
  85. package/configs/flask/.claude/rules/configuration.md +281 -0
  86. package/configs/flask/.claude/rules/context.md +238 -0
  87. package/configs/flask/.claude/rules/error-handlers.md +278 -0
  88. package/configs/flask/.claude/rules/extensions.md +278 -0
  89. package/configs/flask/.claude/rules/flask.md +171 -0
  90. package/configs/flask/.claude/rules/marshmallow.md +206 -0
  91. package/configs/flask/.claude/rules/security.md +267 -0
  92. package/configs/flask/.claude/rules/testing.md +284 -0
  93. package/configs/flask/.claude/settings.json +33 -0
  94. package/configs/flask/CLAUDE.md +166 -0
  95. package/configs/nestjs/.claude/rules/common-patterns.md +300 -0
  96. package/configs/nestjs/.claude/rules/filters.md +376 -0
  97. package/configs/nestjs/.claude/rules/interceptors.md +317 -0
  98. package/configs/nestjs/.claude/rules/middleware.md +321 -0
  99. package/configs/nestjs/.claude/rules/modules.md +26 -0
  100. package/configs/nestjs/.claude/rules/pipes.md +351 -0
  101. package/configs/nestjs/.claude/rules/websockets.md +451 -0
  102. package/configs/nestjs/.claude/settings.json +16 -2
  103. package/configs/nestjs/CLAUDE.md +57 -215
  104. package/configs/nextjs/.claude/rules/api-routes.md +358 -0
  105. package/configs/nextjs/.claude/rules/authentication.md +355 -0
  106. package/configs/nextjs/.claude/rules/components.md +52 -0
  107. package/configs/nextjs/.claude/rules/data-fetching.md +249 -0
  108. package/configs/nextjs/.claude/rules/database.md +400 -0
  109. package/configs/nextjs/.claude/rules/middleware.md +303 -0
  110. package/configs/nextjs/.claude/rules/routing.md +324 -0
  111. package/configs/nextjs/.claude/rules/seo.md +350 -0
  112. package/configs/nextjs/.claude/rules/server-actions.md +353 -0
  113. package/configs/nextjs/.claude/rules/state/zustand.md +6 -6
  114. package/configs/nextjs/.claude/settings.json +5 -0
  115. package/configs/nextjs/CLAUDE.md +69 -331
  116. package/package.json +23 -9
  117. package/src/cli.js +220 -0
  118. package/src/config.js +29 -0
  119. package/src/index.js +13 -0
  120. package/src/installer.js +361 -0
  121. package/src/merge.js +116 -0
  122. package/src/tech-config.json +29 -0
  123. package/src/utils.js +96 -0
  124. package/configs/python/.claude/rules/flask.md +0 -332
  125. package/configs/python/.claude/settings.json +0 -18
  126. package/configs/python/CLAUDE.md +0 -273
  127. package/src/install.js +0 -315
  128. /package/configs/_shared/.claude/rules/{accessibility.md → domain/frontend/accessibility.md} +0 -0
  129. /package/configs/_shared/.claude/rules/{security.md → security/security.md} +0 -0
  130. /package/configs/_shared/.claude/skills/{debug → dev/debug}/SKILL.md +0 -0
  131. /package/configs/_shared/.claude/skills/{learning → dev/learning}/SKILL.md +0 -0
  132. /package/configs/_shared/.claude/skills/{spec → dev/spec}/SKILL.md +0 -0
  133. /package/configs/_shared/.claude/skills/{review → git/review}/SKILL.md +0 -0
@@ -0,0 +1,251 @@
1
+ ---
2
+ paths:
3
+ - "**/*.py"
4
+ ---
5
+
6
+ # FastAPI Response Patterns
7
+
8
+ ## Response Models
9
+
10
+ ```python
11
+ from pydantic import BaseModel, ConfigDict
12
+
13
+ # GOOD - explicit response model
14
+ class UserResponse(BaseModel):
15
+ id: int
16
+ email: str
17
+ name: str
18
+ created_at: datetime
19
+
20
+ model_config = ConfigDict(from_attributes=True)
21
+
22
+ @router.get("/users/{user_id}", response_model=UserResponse)
23
+ async def get_user(user_id: int, db: DbSession) -> User:
24
+ user = await db.get(User, user_id)
25
+ if not user:
26
+ raise HTTPException(404, "User not found")
27
+ return user # Automatically serialized to UserResponse
28
+ ```
29
+
30
+ ## Pagination Response
31
+
32
+ ```python
33
+ from typing import Generic, TypeVar
34
+
35
+ T = TypeVar("T")
36
+
37
+ class Page(BaseModel, Generic[T]):
38
+ items: list[T]
39
+ total: int
40
+ page: int
41
+ size: int
42
+ pages: int
43
+
44
+ @classmethod
45
+ def create(cls, items: list[T], total: int, page: int, size: int) -> "Page[T]":
46
+ return cls(
47
+ items=items,
48
+ total=total,
49
+ page=page,
50
+ size=size,
51
+ pages=(total + size - 1) // size,
52
+ )
53
+
54
+ @router.get("/users", response_model=Page[UserResponse])
55
+ async def list_users(
56
+ page: int = Query(1, ge=1),
57
+ size: int = Query(20, ge=1, le=100),
58
+ db: DbSession,
59
+ ) -> Page[User]:
60
+ total = await db.scalar(select(func.count()).select_from(User))
61
+
62
+ users = await db.scalars(
63
+ select(User)
64
+ .offset((page - 1) * size)
65
+ .limit(size)
66
+ )
67
+
68
+ return Page.create(list(users), total, page, size)
69
+ ```
70
+
71
+ ## Response Classes
72
+
73
+ ```python
74
+ from fastapi.responses import (
75
+ JSONResponse,
76
+ HTMLResponse,
77
+ PlainTextResponse,
78
+ RedirectResponse,
79
+ StreamingResponse,
80
+ FileResponse,
81
+ )
82
+
83
+ # JSON with custom status and headers
84
+ @router.post("/users")
85
+ async def create_user(data: UserCreate) -> JSONResponse:
86
+ user = await user_service.create(data)
87
+ return JSONResponse(
88
+ content={"id": user.id, "message": "User created"},
89
+ status_code=201,
90
+ headers={"X-Custom-Header": "value"},
91
+ )
92
+
93
+ # HTML response
94
+ @router.get("/page", response_class=HTMLResponse)
95
+ async def get_page() -> str:
96
+ return "<html><body><h1>Hello</h1></body></html>"
97
+
98
+ # Redirect
99
+ @router.get("/old-path")
100
+ async def redirect() -> RedirectResponse:
101
+ return RedirectResponse(url="/new-path", status_code=301)
102
+
103
+ # File download
104
+ @router.get("/download/{filename}")
105
+ async def download_file(filename: str) -> FileResponse:
106
+ return FileResponse(
107
+ path=f"files/{filename}",
108
+ filename=filename,
109
+ media_type="application/octet-stream",
110
+ )
111
+ ```
112
+
113
+ ## Streaming Response
114
+
115
+ ```python
116
+ import asyncio
117
+ from typing import AsyncGenerator
118
+
119
+ async def generate_data() -> AsyncGenerator[bytes, None]:
120
+ for i in range(100):
121
+ yield f"data: {i}\n\n".encode()
122
+ await asyncio.sleep(0.1)
123
+
124
+ @router.get("/stream")
125
+ async def stream_data() -> StreamingResponse:
126
+ return StreamingResponse(
127
+ generate_data(),
128
+ media_type="text/event-stream",
129
+ )
130
+
131
+ # Stream large file
132
+ @router.get("/large-file")
133
+ async def stream_file() -> StreamingResponse:
134
+ async def iterfile():
135
+ async with aiofiles.open("large_file.csv", "rb") as f:
136
+ while chunk := await f.read(8192):
137
+ yield chunk
138
+
139
+ return StreamingResponse(
140
+ iterfile(),
141
+ media_type="text/csv",
142
+ headers={"Content-Disposition": "attachment; filename=data.csv"},
143
+ )
144
+ ```
145
+
146
+ ## Error Responses
147
+
148
+ ```python
149
+ from fastapi import HTTPException
150
+ from fastapi.responses import JSONResponse
151
+
152
+ # Standard HTTP exception
153
+ @router.get("/users/{user_id}")
154
+ async def get_user(user_id: int) -> UserResponse:
155
+ user = await db.get(User, user_id)
156
+ if not user:
157
+ raise HTTPException(
158
+ status_code=404,
159
+ detail="User not found",
160
+ headers={"X-Error-Code": "USER_NOT_FOUND"},
161
+ )
162
+ return user
163
+
164
+ # Custom error response model
165
+ class ErrorResponse(BaseModel):
166
+ error: str
167
+ detail: str
168
+ request_id: str | None = None
169
+
170
+ @router.get(
171
+ "/users/{user_id}",
172
+ responses={
173
+ 200: {"model": UserResponse},
174
+ 404: {"model": ErrorResponse, "description": "User not found"},
175
+ 500: {"model": ErrorResponse, "description": "Internal server error"},
176
+ },
177
+ )
178
+ async def get_user(user_id: int) -> UserResponse:
179
+ ...
180
+
181
+ # Global exception handler
182
+ @app.exception_handler(ValueError)
183
+ async def value_error_handler(request: Request, exc: ValueError) -> JSONResponse:
184
+ return JSONResponse(
185
+ status_code=400,
186
+ content={"error": "Bad Request", "detail": str(exc)},
187
+ )
188
+ ```
189
+
190
+ ## Response Headers
191
+
192
+ ```python
193
+ from fastapi import Response
194
+
195
+ @router.get("/data")
196
+ async def get_data(response: Response) -> dict:
197
+ response.headers["X-Custom-Header"] = "custom-value"
198
+ response.headers["Cache-Control"] = "max-age=3600"
199
+ return {"data": "value"}
200
+
201
+ # Set cookies
202
+ @router.post("/login")
203
+ async def login(response: Response) -> dict:
204
+ response.set_cookie(
205
+ key="session_id",
206
+ value="abc123",
207
+ httponly=True,
208
+ secure=True,
209
+ samesite="lax",
210
+ max_age=3600,
211
+ )
212
+ return {"status": "logged_in"}
213
+
214
+ # Delete cookie
215
+ @router.post("/logout")
216
+ async def logout(response: Response) -> dict:
217
+ response.delete_cookie("session_id")
218
+ return {"status": "logged_out"}
219
+ ```
220
+
221
+ ## Background Response
222
+
223
+ ```python
224
+ from fastapi import BackgroundTasks
225
+
226
+ @router.post("/send-email")
227
+ async def send_email(
228
+ email: EmailSchema,
229
+ background_tasks: BackgroundTasks,
230
+ ) -> dict:
231
+ # Return immediately
232
+ background_tasks.add_task(send_email_async, email.to, email.subject, email.body)
233
+ return {"message": "Email queued"}
234
+ ```
235
+
236
+ ## No Content Response
237
+
238
+ ```python
239
+ from fastapi import Response, status
240
+
241
+ @router.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
242
+ async def delete_user(user_id: int, db: DbSession) -> Response:
243
+ user = await db.get(User, user_id)
244
+ if not user:
245
+ raise HTTPException(404, "User not found")
246
+
247
+ await db.delete(user)
248
+ await db.commit()
249
+
250
+ return Response(status_code=status.HTTP_204_NO_CONTENT)
251
+ ```
@@ -0,0 +1,202 @@
1
+ ---
2
+ paths:
3
+ - "**/routers/**/*.py"
4
+ - "**/routes/**/*.py"
5
+ - "**/api/**/*.py"
6
+ - "**/endpoints/**/*.py"
7
+ ---
8
+
9
+ # FastAPI Router Patterns
10
+
11
+ ## Router Organization
12
+
13
+ ```python
14
+ # GOOD - organized router structure
15
+ # app/users/router.py
16
+ from fastapi import APIRouter, status
17
+
18
+ router = APIRouter(
19
+ prefix="/users",
20
+ tags=["Users"],
21
+ responses={404: {"description": "User not found"}},
22
+ )
23
+
24
+ @router.get("/", response_model=list[UserResponse])
25
+ async def list_users(db: DbSession) -> list[User]:
26
+ """List all active users."""
27
+ return await db.scalars(select(User).where(User.is_active))
28
+
29
+ @router.post("/", status_code=status.HTTP_201_CREATED)
30
+ async def create_user(data: UserCreate, db: DbSession) -> UserResponse:
31
+ """Create a new user."""
32
+ user = User(**data.model_dump())
33
+ db.add(user)
34
+ await db.commit()
35
+ return user
36
+
37
+ @router.get("/{user_id}")
38
+ async def get_user(user_id: int, db: DbSession) -> UserResponse:
39
+ """Get user by ID."""
40
+ user = await db.get(User, user_id)
41
+ if not user:
42
+ raise HTTPException(404, "User not found")
43
+ return user
44
+ ```
45
+
46
+ ## Router Registration
47
+
48
+ ```python
49
+ # app/main.py
50
+ from fastapi import FastAPI
51
+ from app.users.router import router as users_router
52
+ from app.auth.router import router as auth_router
53
+ from app.products.router import router as products_router
54
+
55
+ app = FastAPI(title="My API", version="1.0.0")
56
+
57
+ # Version prefix
58
+ api_v1 = APIRouter(prefix="/api/v1")
59
+ api_v1.include_router(users_router)
60
+ api_v1.include_router(auth_router)
61
+ api_v1.include_router(products_router)
62
+
63
+ app.include_router(api_v1)
64
+ ```
65
+
66
+ ## Path Parameters
67
+
68
+ ```python
69
+ # GOOD - typed path parameters with validation
70
+ @router.get("/{user_id}")
71
+ async def get_user(
72
+ user_id: Annotated[int, Path(ge=1, description="User ID")],
73
+ ) -> UserResponse:
74
+ ...
75
+
76
+ # UUID path parameter
77
+ @router.get("/{item_id}")
78
+ async def get_item(item_id: UUID) -> ItemResponse:
79
+ ...
80
+
81
+ # Multiple path parameters
82
+ @router.get("/{user_id}/orders/{order_id}")
83
+ async def get_user_order(user_id: int, order_id: int) -> OrderResponse:
84
+ ...
85
+ ```
86
+
87
+ ## Query Parameters
88
+
89
+ ```python
90
+ # GOOD - query parameters with defaults and validation
91
+ @router.get("/")
92
+ async def list_items(
93
+ skip: int = Query(0, ge=0),
94
+ limit: int = Query(20, ge=1, le=100),
95
+ search: str | None = Query(None, min_length=1, max_length=100),
96
+ status: ItemStatus | None = None,
97
+ sort_by: Literal["name", "created_at", "price"] = "created_at",
98
+ order: Literal["asc", "desc"] = "desc",
99
+ ) -> Page[ItemResponse]:
100
+ ...
101
+
102
+ # List query parameter
103
+ @router.get("/")
104
+ async def get_items(
105
+ ids: Annotated[list[int], Query()] = [],
106
+ ) -> list[ItemResponse]:
107
+ ...
108
+ ```
109
+
110
+ ## Request Body
111
+
112
+ ```python
113
+ # GOOD - explicit body parameter
114
+ @router.post("/")
115
+ async def create_item(
116
+ item: Annotated[ItemCreate, Body(embed=True)],
117
+ ) -> ItemResponse:
118
+ ...
119
+
120
+ # Multiple body parameters
121
+ @router.post("/transfer")
122
+ async def transfer(
123
+ source: Annotated[Account, Body()],
124
+ destination: Annotated[Account, Body()],
125
+ amount: Annotated[Decimal, Body(gt=0)],
126
+ ) -> TransferResult:
127
+ ...
128
+ ```
129
+
130
+ ## Response Configuration
131
+
132
+ ```python
133
+ # GOOD - explicit response models and status codes
134
+ @router.post(
135
+ "/",
136
+ response_model=UserResponse,
137
+ status_code=status.HTTP_201_CREATED,
138
+ responses={
139
+ 201: {"description": "User created successfully"},
140
+ 400: {"model": ErrorResponse, "description": "Validation error"},
141
+ 409: {"model": ErrorResponse, "description": "Email already exists"},
142
+ },
143
+ )
144
+ async def create_user(data: UserCreate) -> User:
145
+ ...
146
+
147
+ # Exclude fields from response
148
+ @router.get("/", response_model_exclude={"password", "secret"})
149
+ async def get_user() -> User:
150
+ ...
151
+
152
+ # Dynamic response model
153
+ @router.get("/", response_model=UserResponse | AdminResponse)
154
+ async def get_current(user: CurrentUser) -> User:
155
+ return user
156
+ ```
157
+
158
+ ## File Uploads
159
+
160
+ ```python
161
+ from fastapi import File, UploadFile
162
+
163
+ @router.post("/upload")
164
+ async def upload_file(
165
+ file: Annotated[UploadFile, File(description="File to upload")],
166
+ ) -> dict:
167
+ contents = await file.read()
168
+ return {"filename": file.filename, "size": len(contents)}
169
+
170
+ # Multiple files
171
+ @router.post("/upload-many")
172
+ async def upload_files(
173
+ files: Annotated[list[UploadFile], File()],
174
+ ) -> list[dict]:
175
+ return [{"filename": f.filename} for f in files]
176
+ ```
177
+
178
+ ## Form Data
179
+
180
+ ```python
181
+ from fastapi import Form
182
+
183
+ @router.post("/login")
184
+ async def login(
185
+ username: Annotated[str, Form()],
186
+ password: Annotated[str, Form()],
187
+ ) -> Token:
188
+ ...
189
+ ```
190
+
191
+ ## Headers and Cookies
192
+
193
+ ```python
194
+ from fastapi import Header, Cookie
195
+
196
+ @router.get("/")
197
+ async def get_items(
198
+ x_token: Annotated[str, Header()],
199
+ session_id: Annotated[str | None, Cookie()] = None,
200
+ ) -> list[Item]:
201
+ ...
202
+ ```
@@ -0,0 +1,222 @@
1
+ ---
2
+ paths:
3
+ - "**/*.py"
4
+ ---
5
+
6
+ # FastAPI Security Patterns
7
+
8
+ ## OAuth2 Password Flow
9
+
10
+ ```python
11
+ from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
12
+
13
+ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token")
14
+
15
+ @router.post("/token")
16
+ async def login(
17
+ form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
18
+ db: DbSession,
19
+ ) -> Token:
20
+ user = await authenticate_user(db, form_data.username, form_data.password)
21
+ if not user:
22
+ raise HTTPException(
23
+ status_code=401,
24
+ detail="Incorrect username or password",
25
+ headers={"WWW-Authenticate": "Bearer"},
26
+ )
27
+
28
+ access_token = create_access_token(data={"sub": str(user.id)})
29
+ return Token(access_token=access_token, token_type="bearer")
30
+ ```
31
+
32
+ ## JWT Authentication
33
+
34
+ ```python
35
+ from jose import JWTError, jwt
36
+ from datetime import datetime, timedelta
37
+
38
+ SECRET_KEY = settings.secret_key
39
+ ALGORITHM = "HS256"
40
+ ACCESS_TOKEN_EXPIRE_MINUTES = 30
41
+
42
+ def create_access_token(data: dict) -> str:
43
+ to_encode = data.copy()
44
+ expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
45
+ to_encode.update({"exp": expire})
46
+ return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
47
+
48
+ async def get_current_user(
49
+ token: Annotated[str, Depends(oauth2_scheme)],
50
+ db: DbSession,
51
+ ) -> User:
52
+ credentials_exception = HTTPException(
53
+ status_code=401,
54
+ detail="Could not validate credentials",
55
+ headers={"WWW-Authenticate": "Bearer"},
56
+ )
57
+
58
+ try:
59
+ payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
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 = await db.get(User, int(user_id))
67
+ if user is None:
68
+ raise credentials_exception
69
+
70
+ return user
71
+
72
+ CurrentUser = Annotated[User, Depends(get_current_user)]
73
+ ```
74
+
75
+ ## API Key Authentication
76
+
77
+ ```python
78
+ from fastapi.security import APIKeyHeader, APIKeyQuery
79
+
80
+ api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
81
+ api_key_query = APIKeyQuery(name="api_key", auto_error=False)
82
+
83
+ async def get_api_key(
84
+ api_key_header: str | None = Depends(api_key_header),
85
+ api_key_query: str | None = Depends(api_key_query),
86
+ ) -> str:
87
+ api_key = api_key_header or api_key_query
88
+ if not api_key:
89
+ raise HTTPException(403, "API key required")
90
+
91
+ if not await verify_api_key(api_key):
92
+ raise HTTPException(403, "Invalid API key")
93
+
94
+ return api_key
95
+
96
+ @router.get("/data", dependencies=[Depends(get_api_key)])
97
+ async def get_data() -> dict:
98
+ ...
99
+ ```
100
+
101
+ ## Role-Based Access Control
102
+
103
+ ```python
104
+ from enum import Enum
105
+ from functools import wraps
106
+
107
+ class Role(str, Enum):
108
+ USER = "user"
109
+ ADMIN = "admin"
110
+ MODERATOR = "moderator"
111
+
112
+ def require_roles(*roles: Role):
113
+ async def dependency(user: CurrentUser) -> User:
114
+ if user.role not in roles:
115
+ raise HTTPException(
116
+ status_code=403,
117
+ detail=f"Required roles: {', '.join(r.value for r in roles)}",
118
+ )
119
+ return user
120
+ return dependency
121
+
122
+ AdminOnly = Annotated[User, Depends(require_roles(Role.ADMIN))]
123
+ ModeratorOrAdmin = Annotated[User, Depends(require_roles(Role.ADMIN, Role.MODERATOR))]
124
+
125
+ @router.delete("/{user_id}")
126
+ async def delete_user(user_id: int, admin: AdminOnly) -> None:
127
+ ...
128
+ ```
129
+
130
+ ## Permission-Based Access
131
+
132
+ ```python
133
+ class Permission(str, Enum):
134
+ READ_USERS = "read:users"
135
+ WRITE_USERS = "write:users"
136
+ DELETE_USERS = "delete:users"
137
+
138
+ def require_permissions(*permissions: Permission):
139
+ async def dependency(user: CurrentUser) -> User:
140
+ user_permissions = set(user.permissions)
141
+ required = set(permissions)
142
+
143
+ if not required.issubset(user_permissions):
144
+ missing = required - user_permissions
145
+ raise HTTPException(
146
+ status_code=403,
147
+ detail=f"Missing permissions: {', '.join(p.value for p in missing)}",
148
+ )
149
+ return user
150
+ return dependency
151
+
152
+ CanDeleteUsers = Annotated[User, Depends(require_permissions(Permission.DELETE_USERS))]
153
+ ```
154
+
155
+ ## Password Hashing
156
+
157
+ ```python
158
+ from passlib.context import CryptContext
159
+
160
+ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
161
+
162
+ def hash_password(password: str) -> str:
163
+ return pwd_context.hash(password)
164
+
165
+ def verify_password(plain_password: str, hashed_password: str) -> bool:
166
+ return pwd_context.verify(plain_password, hashed_password)
167
+
168
+ async def authenticate_user(db: DbSession, email: str, password: str) -> User | None:
169
+ user = await db.scalar(select(User).where(User.email == email))
170
+ if not user:
171
+ return None
172
+ if not verify_password(password, user.hashed_password):
173
+ return None
174
+ return user
175
+ ```
176
+
177
+ ## CORS Configuration
178
+
179
+ ```python
180
+ from fastapi.middleware.cors import CORSMiddleware
181
+
182
+ app.add_middleware(
183
+ CORSMiddleware,
184
+ allow_origins=settings.cors_origins, # ["https://example.com"]
185
+ allow_credentials=True,
186
+ allow_methods=["GET", "POST", "PUT", "DELETE"],
187
+ allow_headers=["*"],
188
+ expose_headers=["X-Request-ID"],
189
+ )
190
+ ```
191
+
192
+ ## Security Headers Middleware
193
+
194
+ ```python
195
+ from starlette.middleware.base import BaseHTTPMiddleware
196
+
197
+ class SecurityHeadersMiddleware(BaseHTTPMiddleware):
198
+ async def dispatch(self, request: Request, call_next):
199
+ response = await call_next(request)
200
+ response.headers["X-Content-Type-Options"] = "nosniff"
201
+ response.headers["X-Frame-Options"] = "DENY"
202
+ response.headers["X-XSS-Protection"] = "1; mode=block"
203
+ response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
204
+ return response
205
+
206
+ app.add_middleware(SecurityHeadersMiddleware)
207
+ ```
208
+
209
+ ## Rate Limiting
210
+
211
+ ```python
212
+ from slowapi import Limiter
213
+ from slowapi.util import get_remote_address
214
+
215
+ limiter = Limiter(key_func=get_remote_address)
216
+ app.state.limiter = limiter
217
+
218
+ @router.get("/")
219
+ @limiter.limit("10/minute")
220
+ async def get_items(request: Request) -> list[Item]:
221
+ ...
222
+ ```