@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,206 @@
1
+ # Marshmallow Validation Rules
2
+
3
+ ## Activation
4
+
5
+ ```yaml
6
+ paths:
7
+ - "**/*.py"
8
+ ```
9
+
10
+ ## Schema Definition
11
+
12
+ ```python
13
+ # GOOD - Clear schema with validation
14
+ from marshmallow import Schema, fields, validate, validates, ValidationError, post_load
15
+
16
+ class UserCreateSchema(Schema):
17
+ email = fields.Email(required=True)
18
+ password = fields.Str(required=True, validate=validate.Length(min=8))
19
+ name = fields.Str(required=True, validate=validate.Length(min=1, max=100))
20
+ age = fields.Int(validate=validate.Range(min=0, max=150))
21
+
22
+ class UserResponseSchema(Schema):
23
+ id = fields.Int(dump_only=True)
24
+ email = fields.Email()
25
+ name = fields.Str()
26
+ created_at = fields.DateTime(dump_only=True)
27
+
28
+ # BAD - No validation
29
+ class UserSchema(Schema):
30
+ email = fields.Str() # Should be fields.Email
31
+ password = fields.Str() # No length validation
32
+ ```
33
+
34
+ ## Custom Validation
35
+
36
+ ```python
37
+ # GOOD - Field-level validation
38
+ class UserSchema(Schema):
39
+ username = fields.Str(required=True)
40
+
41
+ @validates("username")
42
+ def validate_username(self, value):
43
+ if not value.isalnum():
44
+ raise ValidationError("Username must be alphanumeric")
45
+ if len(value) < 3:
46
+ raise ValidationError("Username must be at least 3 characters")
47
+
48
+ # GOOD - Cross-field validation
49
+ from marshmallow import validates_schema
50
+
51
+ class PasswordChangeSchema(Schema):
52
+ password = fields.Str(required=True)
53
+ password_confirm = fields.Str(required=True)
54
+
55
+ @validates_schema
56
+ def validate_passwords_match(self, data, **kwargs):
57
+ if data.get("password") != data.get("password_confirm"):
58
+ raise ValidationError("Passwords must match", field_name="password_confirm")
59
+ ```
60
+
61
+ ## Load/Dump Hooks
62
+
63
+ ```python
64
+ from marshmallow import pre_load, post_load, post_dump
65
+
66
+ class UserSchema(Schema):
67
+ email = fields.Email(required=True)
68
+ name = fields.Str(required=True)
69
+
70
+ @pre_load
71
+ def normalize_email(self, data, **kwargs):
72
+ if "email" in data:
73
+ data["email"] = data["email"].lower().strip()
74
+ return data
75
+
76
+ @post_load
77
+ def make_user(self, data, **kwargs):
78
+ return User(**data)
79
+
80
+ @post_dump
81
+ def remove_nulls(self, data, **kwargs):
82
+ return {k: v for k, v in data.items() if v is not None}
83
+ ```
84
+
85
+ ## Nested Schemas
86
+
87
+ ```python
88
+ # GOOD - Nested relationships
89
+ class AddressSchema(Schema):
90
+ street = fields.Str(required=True)
91
+ city = fields.Str(required=True)
92
+ country = fields.Str(required=True)
93
+
94
+ class UserSchema(Schema):
95
+ name = fields.Str(required=True)
96
+ address = fields.Nested(AddressSchema)
97
+ addresses = fields.List(fields.Nested(AddressSchema))
98
+
99
+ # GOOD - Self-referential (e.g., comments with replies)
100
+ class CommentSchema(Schema):
101
+ id = fields.Int(dump_only=True)
102
+ text = fields.Str(required=True)
103
+ replies = fields.List(fields.Nested("self", exclude=("replies",)))
104
+ ```
105
+
106
+ ## Partial Loading
107
+
108
+ ```python
109
+ # GOOD - Partial updates
110
+ class UserUpdateSchema(Schema):
111
+ email = fields.Email()
112
+ name = fields.Str(validate=validate.Length(min=1, max=100))
113
+
114
+ @users_bp.route("/<int:user_id>", methods=["PATCH"])
115
+ def update_user(user_id: int):
116
+ schema = UserUpdateSchema()
117
+ data = schema.load(request.get_json(), partial=True)
118
+ user = UserService.update(user_id, data)
119
+ return jsonify(UserResponseSchema().dump(user))
120
+ ```
121
+
122
+ ## Schema Inheritance
123
+
124
+ ```python
125
+ # GOOD - Base schema with common fields
126
+ class BaseSchema(Schema):
127
+ class Meta:
128
+ strict = True
129
+
130
+ class TimestampMixin:
131
+ created_at = fields.DateTime(dump_only=True)
132
+ updated_at = fields.DateTime(dump_only=True)
133
+
134
+ class UserSchema(BaseSchema, TimestampMixin):
135
+ id = fields.Int(dump_only=True)
136
+ email = fields.Email(required=True)
137
+ name = fields.Str(required=True)
138
+ ```
139
+
140
+ ## Error Handling
141
+
142
+ ```python
143
+ # GOOD - Centralized error handling
144
+ from marshmallow import ValidationError
145
+ from flask import jsonify
146
+
147
+ @app.errorhandler(ValidationError)
148
+ def handle_validation_error(error):
149
+ return jsonify({
150
+ "error": "Validation Error",
151
+ "details": error.messages,
152
+ }), 400
153
+
154
+ # In routes
155
+ @users_bp.route("/", methods=["POST"])
156
+ def create_user():
157
+ schema = UserCreateSchema()
158
+ try:
159
+ data = schema.load(request.get_json())
160
+ except ValidationError as e:
161
+ return jsonify({"errors": e.messages}), 400
162
+
163
+ user = UserService.create(data)
164
+ return jsonify(UserResponseSchema().dump(user)), 201
165
+ ```
166
+
167
+ ## SQLAlchemy Integration
168
+
169
+ ```python
170
+ # GOOD - With marshmallow-sqlalchemy
171
+ from marshmallow_sqlalchemy import SQLAlchemyAutoSchema
172
+
173
+ class UserSchema(SQLAlchemyAutoSchema):
174
+ class Meta:
175
+ model = User
176
+ include_relationships = True
177
+ load_instance = True
178
+ exclude = ("hashed_password",)
179
+
180
+ # Manual approach (more control)
181
+ class UserSchema(Schema):
182
+ class Meta:
183
+ load_instance = True
184
+
185
+ id = fields.Int(dump_only=True)
186
+ email = fields.Email(required=True)
187
+
188
+ @post_load
189
+ def make_user(self, data, **kwargs):
190
+ return User(**data)
191
+ ```
192
+
193
+ ## Many Parameter
194
+
195
+ ```python
196
+ # GOOD - Serialize/deserialize collections
197
+ schema = UserSchema()
198
+
199
+ # Single object
200
+ user_data = schema.dump(user)
201
+ user = schema.load(data)
202
+
203
+ # Multiple objects
204
+ users_data = schema.dump(users, many=True)
205
+ users = schema.load(data_list, many=True)
206
+ ```
@@ -0,0 +1,267 @@
1
+ ---
2
+ paths:
3
+ - "**/*.py"
4
+ ---
5
+
6
+ # Flask Security Patterns
7
+
8
+ ## Password Hashing
9
+
10
+ ```python
11
+ from werkzeug.security import generate_password_hash, check_password_hash
12
+
13
+ class User(db.Model):
14
+ id: Mapped[int] = mapped_column(primary_key=True)
15
+ email: Mapped[str] = mapped_column(unique=True)
16
+ password_hash: Mapped[str]
17
+
18
+ def set_password(self, password: str):
19
+ self.password_hash = generate_password_hash(password)
20
+
21
+ def check_password(self, password: str) -> bool:
22
+ return check_password_hash(self.password_hash, password)
23
+
24
+ # Usage
25
+ user = User(email="test@example.com")
26
+ user.set_password("secure_password")
27
+
28
+ if user.check_password("secure_password"):
29
+ print("Password correct")
30
+ ```
31
+
32
+ ## CSRF Protection
33
+
34
+ ```python
35
+ from flask_wtf.csrf import CSRFProtect
36
+
37
+ csrf = CSRFProtect()
38
+ csrf.init_app(app)
39
+
40
+ # In templates
41
+ <form method="post">
42
+ {{ csrf_token() }}
43
+ <!-- or -->
44
+ <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
45
+ </form>
46
+
47
+ # For AJAX requests
48
+ <script>
49
+ fetch('/api/endpoint', {
50
+ method: 'POST',
51
+ headers: {
52
+ 'X-CSRFToken': '{{ csrf_token() }}'
53
+ },
54
+ body: JSON.stringify(data)
55
+ });
56
+ </script>
57
+
58
+ # Exempt API routes from CSRF
59
+ @csrf.exempt
60
+ @app.route("/api/webhook", methods=["POST"])
61
+ def webhook():
62
+ ...
63
+
64
+ # Or exempt entire blueprint
65
+ csrf.exempt(api_bp)
66
+ ```
67
+
68
+ ## Security Headers
69
+
70
+ ```python
71
+ from flask_talisman import Talisman
72
+
73
+ # Basic security headers
74
+ Talisman(app, force_https=True)
75
+
76
+ # Custom configuration
77
+ Talisman(
78
+ app,
79
+ force_https=True,
80
+ strict_transport_security=True,
81
+ strict_transport_security_max_age=31536000,
82
+ content_security_policy={
83
+ "default-src": "'self'",
84
+ "script-src": ["'self'", "cdn.example.com"],
85
+ "style-src": ["'self'", "'unsafe-inline'"],
86
+ "img-src": ["'self'", "data:", "*.example.com"],
87
+ },
88
+ )
89
+
90
+ # Manual headers
91
+ @app.after_request
92
+ def add_security_headers(response):
93
+ response.headers["X-Content-Type-Options"] = "nosniff"
94
+ response.headers["X-Frame-Options"] = "DENY"
95
+ response.headers["X-XSS-Protection"] = "1; mode=block"
96
+ return response
97
+ ```
98
+
99
+ ## Session Security
100
+
101
+ ```python
102
+ # Configuration
103
+ app.config.update(
104
+ SECRET_KEY=os.environ["SECRET_KEY"],
105
+ SESSION_COOKIE_SECURE=True, # HTTPS only
106
+ SESSION_COOKIE_HTTPONLY=True, # No JavaScript access
107
+ SESSION_COOKIE_SAMESITE="Lax", # CSRF protection
108
+ PERMANENT_SESSION_LIFETIME=timedelta(hours=24),
109
+ )
110
+
111
+ # Server-side sessions with Redis
112
+ from flask_session import Session
113
+
114
+ app.config["SESSION_TYPE"] = "redis"
115
+ app.config["SESSION_REDIS"] = redis.from_url(os.environ["REDIS_URL"])
116
+ Session(app)
117
+ ```
118
+
119
+ ## Input Validation
120
+
121
+ ```python
122
+ from markupsafe import escape
123
+ from marshmallow import Schema, fields, validate
124
+
125
+ # Always validate input
126
+ class UserInputSchema(Schema):
127
+ name = fields.Str(required=True, validate=validate.Length(min=1, max=100))
128
+ email = fields.Email(required=True)
129
+ bio = fields.Str(validate=validate.Length(max=500))
130
+
131
+ @users_bp.route("/", methods=["POST"])
132
+ def create_user():
133
+ schema = UserInputSchema()
134
+ data = schema.load(request.get_json()) # Validates and sanitizes
135
+ user = UserService.create(data)
136
+ return jsonify(UserSchema().dump(user)), 201
137
+
138
+ # Escape output for HTML
139
+ @app.route("/search")
140
+ def search():
141
+ query = request.args.get("q", "")
142
+ safe_query = escape(query) # Prevents XSS
143
+ return render_template("search.html", query=safe_query)
144
+ ```
145
+
146
+ ## SQL Injection Prevention
147
+
148
+ ```python
149
+ # GOOD - Use ORM or parameterized queries
150
+ user = User.query.filter_by(email=email).first()
151
+
152
+ # GOOD - Raw SQL with parameters
153
+ result = db.session.execute(
154
+ text("SELECT * FROM users WHERE email = :email"),
155
+ {"email": email}
156
+ )
157
+
158
+ # BAD - String interpolation (SQL injection vulnerable!)
159
+ result = db.session.execute(f"SELECT * FROM users WHERE email = '{email}'")
160
+ ```
161
+
162
+ ## API Key Authentication
163
+
164
+ ```python
165
+ from functools import wraps
166
+
167
+ def require_api_key(f):
168
+ @wraps(f)
169
+ def decorated(*args, **kwargs):
170
+ api_key = request.headers.get("X-API-Key")
171
+
172
+ if not api_key:
173
+ return jsonify({"error": "API key required"}), 401
174
+
175
+ # Constant-time comparison to prevent timing attacks
176
+ import hmac
177
+ valid_key = current_app.config["API_KEY"]
178
+ if not hmac.compare_digest(api_key, valid_key):
179
+ return jsonify({"error": "Invalid API key"}), 401
180
+
181
+ return f(*args, **kwargs)
182
+ return decorated
183
+
184
+ @api_bp.route("/data")
185
+ @require_api_key
186
+ def get_data():
187
+ return jsonify({"data": "sensitive"})
188
+ ```
189
+
190
+ ## Rate Limiting
191
+
192
+ ```python
193
+ from flask_limiter import Limiter
194
+ from flask_limiter.util import get_remote_address
195
+
196
+ limiter = Limiter(
197
+ key_func=get_remote_address,
198
+ default_limits=["200 per day", "50 per hour"],
199
+ storage_uri="redis://localhost:6379",
200
+ )
201
+
202
+ @auth_bp.route("/login", methods=["POST"])
203
+ @limiter.limit("5 per minute")
204
+ def login():
205
+ ...
206
+
207
+ # Per-user rate limiting
208
+ @limiter.limit("100 per hour", key_func=lambda: current_user.id)
209
+ def user_endpoint():
210
+ ...
211
+ ```
212
+
213
+ ## Secure File Uploads
214
+
215
+ ```python
216
+ from werkzeug.utils import secure_filename
217
+ import os
218
+
219
+ ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "gif", "pdf"}
220
+ MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB
221
+
222
+ app.config["MAX_CONTENT_LENGTH"] = MAX_CONTENT_LENGTH
223
+
224
+ def allowed_file(filename: str) -> bool:
225
+ return "." in filename and \
226
+ filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
227
+
228
+ @files_bp.route("/upload", methods=["POST"])
229
+ def upload_file():
230
+ if "file" not in request.files:
231
+ return jsonify({"error": "No file provided"}), 400
232
+
233
+ file = request.files["file"]
234
+
235
+ if file.filename == "":
236
+ return jsonify({"error": "No file selected"}), 400
237
+
238
+ if not allowed_file(file.filename):
239
+ return jsonify({"error": "File type not allowed"}), 400
240
+
241
+ # Secure the filename
242
+ filename = secure_filename(file.filename)
243
+
244
+ # Generate unique filename
245
+ unique_filename = f"{uuid.uuid4()}_{filename}"
246
+
247
+ # Save to secure location
248
+ file.save(os.path.join(app.config["UPLOAD_FOLDER"], unique_filename))
249
+
250
+ return jsonify({"filename": unique_filename}), 201
251
+ ```
252
+
253
+ ## Secrets Management
254
+
255
+ ```python
256
+ import os
257
+
258
+ # GOOD - Environment variables
259
+ app.config["SECRET_KEY"] = os.environ["SECRET_KEY"]
260
+ app.config["DATABASE_URL"] = os.environ["DATABASE_URL"]
261
+
262
+ # GOOD - Secrets file (not in repo)
263
+ # config/secrets.py (in .gitignore)
264
+
265
+ # BAD - Hardcoded secrets
266
+ app.config["SECRET_KEY"] = "hardcoded-secret" # NEVER do this
267
+ ```