autoworkflow 3.1.5 → 3.6.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.
- package/.claude/commands/analyze.md +19 -0
- package/.claude/commands/audit.md +26 -0
- package/.claude/commands/build.md +39 -0
- package/.claude/commands/commit.md +25 -0
- package/.claude/commands/fix.md +23 -0
- package/.claude/commands/plan.md +18 -0
- package/.claude/commands/suggest.md +23 -0
- package/.claude/commands/verify.md +18 -0
- package/.claude/hooks/post-bash-router.sh +20 -0
- package/.claude/hooks/post-commit.sh +140 -0
- package/.claude/hooks/post-edit.sh +190 -17
- package/.claude/hooks/pre-edit.sh +221 -0
- package/.claude/hooks/session-check.sh +90 -0
- package/.claude/settings.json +56 -6
- package/.claude/settings.local.json +5 -1
- package/.claude/skills/actix.md +337 -0
- package/.claude/skills/alembic.md +504 -0
- package/.claude/skills/angular.md +237 -0
- package/.claude/skills/api-design.md +187 -0
- package/.claude/skills/aspnet-core.md +377 -0
- package/.claude/skills/astro.md +245 -0
- package/.claude/skills/auth-clerk.md +327 -0
- package/.claude/skills/auth-firebase.md +367 -0
- package/.claude/skills/auth-nextauth.md +359 -0
- package/.claude/skills/auth-supabase.md +368 -0
- package/.claude/skills/axum.md +386 -0
- package/.claude/skills/blazor.md +456 -0
- package/.claude/skills/chi.md +348 -0
- package/.claude/skills/code-review.md +133 -0
- package/.claude/skills/csharp.md +296 -0
- package/.claude/skills/css-modules.md +325 -0
- package/.claude/skills/cypress.md +343 -0
- package/.claude/skills/debugging.md +133 -0
- package/.claude/skills/diesel.md +392 -0
- package/.claude/skills/django.md +301 -0
- package/.claude/skills/docker.md +319 -0
- package/.claude/skills/doctrine.md +473 -0
- package/.claude/skills/documentation.md +182 -0
- package/.claude/skills/dotnet.md +409 -0
- package/.claude/skills/drizzle.md +293 -0
- package/.claude/skills/echo.md +321 -0
- package/.claude/skills/eloquent.md +256 -0
- package/.claude/skills/emotion.md +426 -0
- package/.claude/skills/entity-framework.md +370 -0
- package/.claude/skills/express.md +316 -0
- package/.claude/skills/fastapi.md +329 -0
- package/.claude/skills/fastify.md +299 -0
- package/.claude/skills/fiber.md +315 -0
- package/.claude/skills/flask.md +322 -0
- package/.claude/skills/gin.md +342 -0
- package/.claude/skills/git.md +116 -0
- package/.claude/skills/github-actions.md +353 -0
- package/.claude/skills/go.md +377 -0
- package/.claude/skills/gorm.md +409 -0
- package/.claude/skills/graphql.md +478 -0
- package/.claude/skills/hibernate.md +379 -0
- package/.claude/skills/hono.md +306 -0
- package/.claude/skills/java.md +400 -0
- package/.claude/skills/jest.md +313 -0
- package/.claude/skills/jpa.md +282 -0
- package/.claude/skills/kotlin.md +347 -0
- package/.claude/skills/kubernetes.md +363 -0
- package/.claude/skills/laravel.md +414 -0
- package/.claude/skills/mcp-browser.md +320 -0
- package/.claude/skills/mcp-database.md +219 -0
- package/.claude/skills/mcp-fetch.md +241 -0
- package/.claude/skills/mcp-filesystem.md +204 -0
- package/.claude/skills/mcp-github.md +217 -0
- package/.claude/skills/mcp-memory.md +240 -0
- package/.claude/skills/mcp-search.md +218 -0
- package/.claude/skills/mcp-slack.md +262 -0
- package/.claude/skills/micronaut.md +388 -0
- package/.claude/skills/mongodb.md +319 -0
- package/.claude/skills/mongoose.md +355 -0
- package/.claude/skills/mysql.md +281 -0
- package/.claude/skills/nestjs.md +335 -0
- package/.claude/skills/nextjs-app-router.md +260 -0
- package/.claude/skills/nextjs-pages.md +172 -0
- package/.claude/skills/nuxt.md +202 -0
- package/.claude/skills/openapi.md +489 -0
- package/.claude/skills/performance.md +199 -0
- package/.claude/skills/php.md +398 -0
- package/.claude/skills/playwright.md +371 -0
- package/.claude/skills/postgresql.md +257 -0
- package/.claude/skills/prisma.md +293 -0
- package/.claude/skills/pydantic.md +304 -0
- package/.claude/skills/pytest.md +313 -0
- package/.claude/skills/python.md +272 -0
- package/.claude/skills/quarkus.md +377 -0
- package/.claude/skills/react.md +230 -0
- package/.claude/skills/redis.md +391 -0
- package/.claude/skills/refactoring.md +143 -0
- package/.claude/skills/remix.md +246 -0
- package/.claude/skills/rest-api.md +490 -0
- package/.claude/skills/rocket.md +366 -0
- package/.claude/skills/rust.md +341 -0
- package/.claude/skills/sass.md +380 -0
- package/.claude/skills/sea-orm.md +382 -0
- package/.claude/skills/security.md +167 -0
- package/.claude/skills/sequelize.md +395 -0
- package/.claude/skills/spring-boot.md +416 -0
- package/.claude/skills/sqlalchemy.md +269 -0
- package/.claude/skills/sqlx-rust.md +408 -0
- package/.claude/skills/state-jotai.md +346 -0
- package/.claude/skills/state-mobx.md +353 -0
- package/.claude/skills/state-pinia.md +431 -0
- package/.claude/skills/state-redux.md +337 -0
- package/.claude/skills/state-tanstack-query.md +434 -0
- package/.claude/skills/state-zustand.md +340 -0
- package/.claude/skills/styled-components.md +403 -0
- package/.claude/skills/svelte.md +238 -0
- package/.claude/skills/sveltekit.md +207 -0
- package/.claude/skills/symfony.md +437 -0
- package/.claude/skills/tailwind.md +279 -0
- package/.claude/skills/terraform.md +394 -0
- package/.claude/skills/testing-library.md +371 -0
- package/.claude/skills/trpc.md +426 -0
- package/.claude/skills/typeorm.md +368 -0
- package/.claude/skills/vitest.md +330 -0
- package/.claude/skills/vue.md +202 -0
- package/.claude/skills/warp.md +365 -0
- package/README.md +163 -52
- package/package.json +1 -1
- package/system/triggers.md +256 -17
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# Fiber Framework Skill
|
|
2
|
+
|
|
3
|
+
## Application Setup
|
|
4
|
+
\`\`\`go
|
|
5
|
+
package main
|
|
6
|
+
|
|
7
|
+
import (
|
|
8
|
+
"log"
|
|
9
|
+
"os"
|
|
10
|
+
"time"
|
|
11
|
+
|
|
12
|
+
"github.com/gofiber/fiber/v2"
|
|
13
|
+
"github.com/gofiber/fiber/v2/middleware/cors"
|
|
14
|
+
"github.com/gofiber/fiber/v2/middleware/limiter"
|
|
15
|
+
"github.com/gofiber/fiber/v2/middleware/logger"
|
|
16
|
+
"github.com/gofiber/fiber/v2/middleware/recover"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
func main() {
|
|
20
|
+
app := fiber.New(fiber.Config{
|
|
21
|
+
ErrorHandler: customErrorHandler,
|
|
22
|
+
DisableStartupMessage: os.Getenv("ENV") == "production",
|
|
23
|
+
ReadTimeout: 10 * time.Second,
|
|
24
|
+
WriteTimeout: 10 * time.Second,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
// Global middleware
|
|
28
|
+
app.Use(recover.New())
|
|
29
|
+
app.Use(logger.New())
|
|
30
|
+
app.Use(cors.New(cors.Config{
|
|
31
|
+
AllowOrigins: "*",
|
|
32
|
+
AllowMethods: "GET,POST,PUT,DELETE,OPTIONS",
|
|
33
|
+
AllowHeaders: "Authorization,Content-Type",
|
|
34
|
+
}))
|
|
35
|
+
|
|
36
|
+
// Health check
|
|
37
|
+
app.Get("/health", func(c *fiber.Ctx) error {
|
|
38
|
+
return c.JSON(fiber.Map{"status": "ok"})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
setupRoutes(app)
|
|
42
|
+
|
|
43
|
+
log.Fatal(app.Listen(":3000"))
|
|
44
|
+
}
|
|
45
|
+
\`\`\`
|
|
46
|
+
|
|
47
|
+
## Route Groups
|
|
48
|
+
\`\`\`go
|
|
49
|
+
func setupRoutes(app *fiber.App) {
|
|
50
|
+
api := app.Group("/api/v1")
|
|
51
|
+
|
|
52
|
+
// Public routes
|
|
53
|
+
auth := api.Group("/auth")
|
|
54
|
+
auth.Post("/login", loginHandler)
|
|
55
|
+
auth.Post("/register", registerHandler)
|
|
56
|
+
|
|
57
|
+
// Protected routes
|
|
58
|
+
protected := api.Group("", AuthMiddleware())
|
|
59
|
+
|
|
60
|
+
// Users
|
|
61
|
+
users := protected.Group("/users")
|
|
62
|
+
users.Get("/", listUsersHandler)
|
|
63
|
+
users.Get("/:id", getUserHandler)
|
|
64
|
+
users.Put("/:id", updateUserHandler)
|
|
65
|
+
users.Delete("/:id", deleteUserHandler)
|
|
66
|
+
|
|
67
|
+
// Posts with rate limiting
|
|
68
|
+
posts := protected.Group("/posts", limiter.New(limiter.Config{
|
|
69
|
+
Max: 10,
|
|
70
|
+
Expiration: time.Minute,
|
|
71
|
+
}))
|
|
72
|
+
posts.Get("/", listPostsHandler)
|
|
73
|
+
posts.Post("/", createPostHandler)
|
|
74
|
+
}
|
|
75
|
+
\`\`\`
|
|
76
|
+
|
|
77
|
+
## Request Parsing and Validation
|
|
78
|
+
\`\`\`go
|
|
79
|
+
type CreateUserRequest struct {
|
|
80
|
+
Email string \`json:"email" validate:"required,email"\`
|
|
81
|
+
Name string \`json:"name" validate:"required,min=2,max=100"\`
|
|
82
|
+
Password string \`json:"password" validate:"required,min=8"\`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
type QueryParams struct {
|
|
86
|
+
Page int \`query:"page"\`
|
|
87
|
+
PerPage int \`query:"per_page"\`
|
|
88
|
+
Sort string \`query:"sort"\`
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
func createUserHandler(c *fiber.Ctx) error {
|
|
92
|
+
var req CreateUserRequest
|
|
93
|
+
if err := c.BodyParser(&req); err != nil {
|
|
94
|
+
return fiber.NewError(fiber.StatusBadRequest, "invalid request body")
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Validate using go-playground/validator
|
|
98
|
+
if err := validate.Struct(&req); err != nil {
|
|
99
|
+
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
|
100
|
+
"error": "validation_error",
|
|
101
|
+
"details": formatValidationErrors(err),
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
user, err := userService.Create(c.Context(), req)
|
|
106
|
+
if err != nil {
|
|
107
|
+
return err // Handled by custom error handler
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return c.Status(fiber.StatusCreated).JSON(user)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
func listUsersHandler(c *fiber.Ctx) error {
|
|
114
|
+
var query QueryParams
|
|
115
|
+
if err := c.QueryParser(&query); err != nil {
|
|
116
|
+
return fiber.NewError(fiber.StatusBadRequest, "invalid query parameters")
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Defaults
|
|
120
|
+
if query.Page == 0 {
|
|
121
|
+
query.Page = 1
|
|
122
|
+
}
|
|
123
|
+
if query.PerPage == 0 {
|
|
124
|
+
query.PerPage = 20
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
users, total, err := userService.List(c.Context(), query)
|
|
128
|
+
if err != nil {
|
|
129
|
+
return err
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return c.JSON(fiber.Map{
|
|
133
|
+
"data": users,
|
|
134
|
+
"total": total,
|
|
135
|
+
"page": query.Page,
|
|
136
|
+
"per_page": query.PerPage,
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
func getUserHandler(c *fiber.Ctx) error {
|
|
141
|
+
id := c.Params("id")
|
|
142
|
+
|
|
143
|
+
user, err := userService.GetByID(c.Context(), id)
|
|
144
|
+
if err != nil {
|
|
145
|
+
return err
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return c.JSON(user)
|
|
149
|
+
}
|
|
150
|
+
\`\`\`
|
|
151
|
+
|
|
152
|
+
## Custom Middleware
|
|
153
|
+
\`\`\`go
|
|
154
|
+
// Auth Middleware
|
|
155
|
+
func AuthMiddleware() fiber.Handler {
|
|
156
|
+
return func(c *fiber.Ctx) error {
|
|
157
|
+
authHeader := c.Get("Authorization")
|
|
158
|
+
if authHeader == "" {
|
|
159
|
+
return fiber.NewError(fiber.StatusUnauthorized, "missing authorization header")
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
|
163
|
+
claims, err := validateToken(tokenString)
|
|
164
|
+
if err != nil {
|
|
165
|
+
return fiber.NewError(fiber.StatusUnauthorized, "invalid token")
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Store in Locals
|
|
169
|
+
c.Locals("userID", claims.UserID)
|
|
170
|
+
c.Locals("userRole", claims.Role)
|
|
171
|
+
|
|
172
|
+
return c.Next()
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Request ID Middleware
|
|
177
|
+
func RequestIDMiddleware() fiber.Handler {
|
|
178
|
+
return func(c *fiber.Ctx) error {
|
|
179
|
+
requestID := c.Get("X-Request-ID")
|
|
180
|
+
if requestID == "" {
|
|
181
|
+
requestID = uuid.New().String()
|
|
182
|
+
}
|
|
183
|
+
c.Locals("requestID", requestID)
|
|
184
|
+
c.Set("X-Request-ID", requestID)
|
|
185
|
+
return c.Next()
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Get user from context
|
|
190
|
+
func getUserFromContext(c *fiber.Ctx) string {
|
|
191
|
+
userID, ok := c.Locals("userID").(string)
|
|
192
|
+
if !ok {
|
|
193
|
+
return ""
|
|
194
|
+
}
|
|
195
|
+
return userID
|
|
196
|
+
}
|
|
197
|
+
\`\`\`
|
|
198
|
+
|
|
199
|
+
## Error Handling
|
|
200
|
+
\`\`\`go
|
|
201
|
+
// Custom error handler
|
|
202
|
+
func customErrorHandler(c *fiber.Ctx, err error) error {
|
|
203
|
+
// Default to 500
|
|
204
|
+
code := fiber.StatusInternalServerError
|
|
205
|
+
message := "internal server error"
|
|
206
|
+
|
|
207
|
+
// Check for fiber.Error
|
|
208
|
+
var e *fiber.Error
|
|
209
|
+
if errors.As(err, &e) {
|
|
210
|
+
code = e.Code
|
|
211
|
+
message = e.Message
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Check for custom errors
|
|
215
|
+
switch {
|
|
216
|
+
case errors.Is(err, ErrNotFound):
|
|
217
|
+
code = fiber.StatusNotFound
|
|
218
|
+
message = "resource not found"
|
|
219
|
+
case errors.Is(err, ErrUnauthorized):
|
|
220
|
+
code = fiber.StatusUnauthorized
|
|
221
|
+
message = "unauthorized"
|
|
222
|
+
case errors.Is(err, ErrForbidden):
|
|
223
|
+
code = fiber.StatusForbidden
|
|
224
|
+
message = "forbidden"
|
|
225
|
+
case errors.Is(err, ErrConflict):
|
|
226
|
+
code = fiber.StatusConflict
|
|
227
|
+
message = "resource already exists"
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Log internal errors
|
|
231
|
+
if code == fiber.StatusInternalServerError {
|
|
232
|
+
log.Printf("internal error: %v", err)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return c.Status(code).JSON(fiber.Map{
|
|
236
|
+
"error": message,
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
\`\`\`
|
|
240
|
+
|
|
241
|
+
## Testing
|
|
242
|
+
\`\`\`go
|
|
243
|
+
package handler_test
|
|
244
|
+
|
|
245
|
+
import (
|
|
246
|
+
"bytes"
|
|
247
|
+
"encoding/json"
|
|
248
|
+
"io"
|
|
249
|
+
"net/http/httptest"
|
|
250
|
+
"testing"
|
|
251
|
+
|
|
252
|
+
"github.com/gofiber/fiber/v2"
|
|
253
|
+
"github.com/stretchr/testify/assert"
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
func setupTestApp() *fiber.App {
|
|
257
|
+
app := fiber.New(fiber.Config{
|
|
258
|
+
ErrorHandler: customErrorHandler,
|
|
259
|
+
})
|
|
260
|
+
setupRoutes(app)
|
|
261
|
+
return app
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
func TestCreateUser(t *testing.T) {
|
|
265
|
+
app := setupTestApp()
|
|
266
|
+
|
|
267
|
+
tests := []struct {
|
|
268
|
+
name string
|
|
269
|
+
body map[string]interface{}
|
|
270
|
+
wantStatus int
|
|
271
|
+
}{
|
|
272
|
+
{
|
|
273
|
+
name: "valid user",
|
|
274
|
+
body: map[string]interface{}{
|
|
275
|
+
"email": "test@example.com",
|
|
276
|
+
"name": "Test User",
|
|
277
|
+
"password": "password123",
|
|
278
|
+
},
|
|
279
|
+
wantStatus: fiber.StatusCreated,
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
name: "missing email",
|
|
283
|
+
body: map[string]interface{}{
|
|
284
|
+
"name": "Test User",
|
|
285
|
+
"password": "password123",
|
|
286
|
+
},
|
|
287
|
+
wantStatus: fiber.StatusBadRequest,
|
|
288
|
+
},
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
for _, tt := range tests {
|
|
292
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
293
|
+
body, _ := json.Marshal(tt.body)
|
|
294
|
+
req := httptest.NewRequest("POST", "/api/v1/users", bytes.NewBuffer(body))
|
|
295
|
+
req.Header.Set("Content-Type", "application/json")
|
|
296
|
+
|
|
297
|
+
resp, _ := app.Test(req)
|
|
298
|
+
assert.Equal(t, tt.wantStatus, resp.StatusCode)
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
\`\`\`
|
|
303
|
+
|
|
304
|
+
## ✅ DO
|
|
305
|
+
- Use \`fiber.Config\` for app configuration
|
|
306
|
+
- Use \`c.Context()\` for passing context to services
|
|
307
|
+
- Return errors from handlers (let error handler process them)
|
|
308
|
+
- Use \`c.Locals()\` for request-scoped values
|
|
309
|
+
- Use built-in middleware (logger, recover, cors, limiter)
|
|
310
|
+
|
|
311
|
+
## ❌ DON'T
|
|
312
|
+
- Don't ignore errors from \`BodyParser\` or \`QueryParser\`
|
|
313
|
+
- Don't use \`c.Context()\` after handler returns (it's pooled)
|
|
314
|
+
- Don't modify response after calling \`c.Next()\` in middleware
|
|
315
|
+
- Don't store Fiber context in goroutines (copy needed values first)
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# Flask Skill
|
|
2
|
+
|
|
3
|
+
## Project Structure
|
|
4
|
+
\`\`\`
|
|
5
|
+
app/
|
|
6
|
+
├── __init__.py # Application factory
|
|
7
|
+
├── config.py # Configuration
|
|
8
|
+
├── extensions.py # Flask extensions
|
|
9
|
+
├── models/
|
|
10
|
+
│ └── user.py
|
|
11
|
+
├── routes/
|
|
12
|
+
│ ├── __init__.py
|
|
13
|
+
│ ├── auth.py
|
|
14
|
+
│ └── users.py
|
|
15
|
+
├── services/
|
|
16
|
+
│ └── user.py
|
|
17
|
+
└── templates/
|
|
18
|
+
└── base.html
|
|
19
|
+
\`\`\`
|
|
20
|
+
|
|
21
|
+
## Application Factory
|
|
22
|
+
\`\`\`python
|
|
23
|
+
# __init__.py
|
|
24
|
+
from flask import Flask
|
|
25
|
+
from app.extensions import db, migrate, jwt
|
|
26
|
+
from app.config import config
|
|
27
|
+
|
|
28
|
+
def create_app(config_name='development'):
|
|
29
|
+
app = Flask(__name__)
|
|
30
|
+
app.config.from_object(config[config_name])
|
|
31
|
+
|
|
32
|
+
# Initialize extensions
|
|
33
|
+
db.init_app(app)
|
|
34
|
+
migrate.init_app(app, db)
|
|
35
|
+
jwt.init_app(app)
|
|
36
|
+
|
|
37
|
+
# Register blueprints
|
|
38
|
+
from app.routes.auth import auth_bp
|
|
39
|
+
from app.routes.users import users_bp
|
|
40
|
+
|
|
41
|
+
app.register_blueprint(auth_bp, url_prefix='/auth')
|
|
42
|
+
app.register_blueprint(users_bp, url_prefix='/api/users')
|
|
43
|
+
|
|
44
|
+
# Register error handlers
|
|
45
|
+
register_error_handlers(app)
|
|
46
|
+
|
|
47
|
+
return app
|
|
48
|
+
|
|
49
|
+
def register_error_handlers(app):
|
|
50
|
+
@app.errorhandler(404)
|
|
51
|
+
def not_found(error):
|
|
52
|
+
return {'error': 'Not found'}, 404
|
|
53
|
+
|
|
54
|
+
@app.errorhandler(500)
|
|
55
|
+
def internal_error(error):
|
|
56
|
+
db.session.rollback()
|
|
57
|
+
return {'error': 'Internal server error'}, 500
|
|
58
|
+
|
|
59
|
+
# extensions.py
|
|
60
|
+
from flask_sqlalchemy import SQLAlchemy
|
|
61
|
+
from flask_migrate import Migrate
|
|
62
|
+
from flask_jwt_extended import JWTManager
|
|
63
|
+
|
|
64
|
+
db = SQLAlchemy()
|
|
65
|
+
migrate = Migrate()
|
|
66
|
+
jwt = JWTManager()
|
|
67
|
+
\`\`\`
|
|
68
|
+
|
|
69
|
+
## Configuration
|
|
70
|
+
\`\`\`python
|
|
71
|
+
# config.py
|
|
72
|
+
import os
|
|
73
|
+
|
|
74
|
+
class Config:
|
|
75
|
+
SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-secret')
|
|
76
|
+
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
|
77
|
+
JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY', 'jwt-secret')
|
|
78
|
+
|
|
79
|
+
class DevelopmentConfig(Config):
|
|
80
|
+
DEBUG = True
|
|
81
|
+
SQLALCHEMY_DATABASE_URI = 'sqlite:///dev.db'
|
|
82
|
+
|
|
83
|
+
class ProductionConfig(Config):
|
|
84
|
+
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')
|
|
85
|
+
|
|
86
|
+
class TestingConfig(Config):
|
|
87
|
+
TESTING = True
|
|
88
|
+
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
|
|
89
|
+
|
|
90
|
+
config = {
|
|
91
|
+
'development': DevelopmentConfig,
|
|
92
|
+
'production': ProductionConfig,
|
|
93
|
+
'testing': TestingConfig,
|
|
94
|
+
}
|
|
95
|
+
\`\`\`
|
|
96
|
+
|
|
97
|
+
## Blueprints & Routes
|
|
98
|
+
\`\`\`python
|
|
99
|
+
# routes/users.py
|
|
100
|
+
from flask import Blueprint, request, jsonify
|
|
101
|
+
from flask_jwt_extended import jwt_required, get_jwt_identity
|
|
102
|
+
from app.models.user import User
|
|
103
|
+
from app.extensions import db
|
|
104
|
+
from app.schemas import UserSchema, UserCreateSchema
|
|
105
|
+
|
|
106
|
+
users_bp = Blueprint('users', __name__)
|
|
107
|
+
|
|
108
|
+
@users_bp.route('/', methods=['GET'])
|
|
109
|
+
def get_users():
|
|
110
|
+
page = request.args.get('page', 1, type=int)
|
|
111
|
+
per_page = request.args.get('per_page', 10, type=int)
|
|
112
|
+
|
|
113
|
+
pagination = User.query.paginate(page=page, per_page=per_page)
|
|
114
|
+
|
|
115
|
+
return jsonify({
|
|
116
|
+
'users': UserSchema(many=True).dump(pagination.items),
|
|
117
|
+
'total': pagination.total,
|
|
118
|
+
'pages': pagination.pages,
|
|
119
|
+
'page': page,
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
@users_bp.route('/<int:user_id>', methods=['GET'])
|
|
123
|
+
def get_user(user_id):
|
|
124
|
+
user = User.query.get_or_404(user_id)
|
|
125
|
+
return jsonify(UserSchema().dump(user))
|
|
126
|
+
|
|
127
|
+
@users_bp.route('/', methods=['POST'])
|
|
128
|
+
def create_user():
|
|
129
|
+
schema = UserCreateSchema()
|
|
130
|
+
data = schema.load(request.json)
|
|
131
|
+
|
|
132
|
+
user = User(**data)
|
|
133
|
+
db.session.add(user)
|
|
134
|
+
db.session.commit()
|
|
135
|
+
|
|
136
|
+
return jsonify(UserSchema().dump(user)), 201
|
|
137
|
+
|
|
138
|
+
@users_bp.route('/<int:user_id>', methods=['PUT'])
|
|
139
|
+
@jwt_required()
|
|
140
|
+
def update_user(user_id):
|
|
141
|
+
current_user_id = get_jwt_identity()
|
|
142
|
+
if current_user_id != user_id:
|
|
143
|
+
return jsonify({'error': 'Forbidden'}), 403
|
|
144
|
+
|
|
145
|
+
user = User.query.get_or_404(user_id)
|
|
146
|
+
data = request.json
|
|
147
|
+
|
|
148
|
+
for key, value in data.items():
|
|
149
|
+
if hasattr(user, key):
|
|
150
|
+
setattr(user, key, value)
|
|
151
|
+
|
|
152
|
+
db.session.commit()
|
|
153
|
+
return jsonify(UserSchema().dump(user))
|
|
154
|
+
|
|
155
|
+
@users_bp.route('/<int:user_id>', methods=['DELETE'])
|
|
156
|
+
@jwt_required()
|
|
157
|
+
def delete_user(user_id):
|
|
158
|
+
user = User.query.get_or_404(user_id)
|
|
159
|
+
db.session.delete(user)
|
|
160
|
+
db.session.commit()
|
|
161
|
+
return '', 204
|
|
162
|
+
\`\`\`
|
|
163
|
+
|
|
164
|
+
## Authentication
|
|
165
|
+
\`\`\`python
|
|
166
|
+
# routes/auth.py
|
|
167
|
+
from flask import Blueprint, request, jsonify
|
|
168
|
+
from flask_jwt_extended import (
|
|
169
|
+
create_access_token,
|
|
170
|
+
create_refresh_token,
|
|
171
|
+
jwt_required,
|
|
172
|
+
get_jwt_identity,
|
|
173
|
+
)
|
|
174
|
+
from werkzeug.security import check_password_hash
|
|
175
|
+
from app.models.user import User
|
|
176
|
+
|
|
177
|
+
auth_bp = Blueprint('auth', __name__)
|
|
178
|
+
|
|
179
|
+
@auth_bp.route('/login', methods=['POST'])
|
|
180
|
+
def login():
|
|
181
|
+
email = request.json.get('email')
|
|
182
|
+
password = request.json.get('password')
|
|
183
|
+
|
|
184
|
+
user = User.query.filter_by(email=email).first()
|
|
185
|
+
|
|
186
|
+
if not user or not check_password_hash(user.password_hash, password):
|
|
187
|
+
return jsonify({'error': 'Invalid credentials'}), 401
|
|
188
|
+
|
|
189
|
+
access_token = create_access_token(identity=user.id)
|
|
190
|
+
refresh_token = create_refresh_token(identity=user.id)
|
|
191
|
+
|
|
192
|
+
return jsonify({
|
|
193
|
+
'access_token': access_token,
|
|
194
|
+
'refresh_token': refresh_token,
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
@auth_bp.route('/refresh', methods=['POST'])
|
|
198
|
+
@jwt_required(refresh=True)
|
|
199
|
+
def refresh():
|
|
200
|
+
identity = get_jwt_identity()
|
|
201
|
+
access_token = create_access_token(identity=identity)
|
|
202
|
+
return jsonify({'access_token': access_token})
|
|
203
|
+
|
|
204
|
+
@auth_bp.route('/me', methods=['GET'])
|
|
205
|
+
@jwt_required()
|
|
206
|
+
def me():
|
|
207
|
+
user_id = get_jwt_identity()
|
|
208
|
+
user = User.query.get(user_id)
|
|
209
|
+
return jsonify(UserSchema().dump(user))
|
|
210
|
+
\`\`\`
|
|
211
|
+
|
|
212
|
+
## Error Handling
|
|
213
|
+
\`\`\`python
|
|
214
|
+
from flask import jsonify
|
|
215
|
+
from marshmallow import ValidationError
|
|
216
|
+
|
|
217
|
+
def register_error_handlers(app):
|
|
218
|
+
@app.errorhandler(ValidationError)
|
|
219
|
+
def handle_validation_error(error):
|
|
220
|
+
return jsonify({'error': 'Validation failed', 'details': error.messages}), 400
|
|
221
|
+
|
|
222
|
+
@app.errorhandler(404)
|
|
223
|
+
def not_found(error):
|
|
224
|
+
return jsonify({'error': 'Resource not found'}), 404
|
|
225
|
+
|
|
226
|
+
@app.errorhandler(401)
|
|
227
|
+
def unauthorized(error):
|
|
228
|
+
return jsonify({'error': 'Unauthorized'}), 401
|
|
229
|
+
|
|
230
|
+
@app.errorhandler(Exception)
|
|
231
|
+
def handle_exception(error):
|
|
232
|
+
app.logger.error(f'Unhandled exception: {error}')
|
|
233
|
+
return jsonify({'error': 'Internal server error'}), 500
|
|
234
|
+
\`\`\`
|
|
235
|
+
|
|
236
|
+
## Request Validation (Marshmallow)
|
|
237
|
+
\`\`\`python
|
|
238
|
+
# schemas.py
|
|
239
|
+
from marshmallow import Schema, fields, validate, post_load
|
|
240
|
+
|
|
241
|
+
class UserSchema(Schema):
|
|
242
|
+
id = fields.Int(dump_only=True)
|
|
243
|
+
email = fields.Email(required=True)
|
|
244
|
+
name = fields.Str(required=True, validate=validate.Length(min=1, max=100))
|
|
245
|
+
created_at = fields.DateTime(dump_only=True)
|
|
246
|
+
|
|
247
|
+
class UserCreateSchema(Schema):
|
|
248
|
+
email = fields.Email(required=True)
|
|
249
|
+
name = fields.Str(required=True)
|
|
250
|
+
password = fields.Str(required=True, load_only=True, validate=validate.Length(min=8))
|
|
251
|
+
|
|
252
|
+
@post_load
|
|
253
|
+
def hash_password(self, data, **kwargs):
|
|
254
|
+
from werkzeug.security import generate_password_hash
|
|
255
|
+
data['password_hash'] = generate_password_hash(data.pop('password'))
|
|
256
|
+
return data
|
|
257
|
+
\`\`\`
|
|
258
|
+
|
|
259
|
+
## Testing
|
|
260
|
+
\`\`\`python
|
|
261
|
+
# tests/conftest.py
|
|
262
|
+
import pytest
|
|
263
|
+
from app import create_app
|
|
264
|
+
from app.extensions import db
|
|
265
|
+
|
|
266
|
+
@pytest.fixture
|
|
267
|
+
def app():
|
|
268
|
+
app = create_app('testing')
|
|
269
|
+
with app.app_context():
|
|
270
|
+
db.create_all()
|
|
271
|
+
yield app
|
|
272
|
+
db.drop_all()
|
|
273
|
+
|
|
274
|
+
@pytest.fixture
|
|
275
|
+
def client(app):
|
|
276
|
+
return app.test_client()
|
|
277
|
+
|
|
278
|
+
@pytest.fixture
|
|
279
|
+
def auth_headers(client):
|
|
280
|
+
# Create user and login
|
|
281
|
+
client.post('/auth/register', json={
|
|
282
|
+
'email': 'test@example.com',
|
|
283
|
+
'password': 'password123',
|
|
284
|
+
'name': 'Test User',
|
|
285
|
+
})
|
|
286
|
+
response = client.post('/auth/login', json={
|
|
287
|
+
'email': 'test@example.com',
|
|
288
|
+
'password': 'password123',
|
|
289
|
+
})
|
|
290
|
+
token = response.json['access_token']
|
|
291
|
+
return {'Authorization': f'Bearer {token}'}
|
|
292
|
+
|
|
293
|
+
# tests/test_users.py
|
|
294
|
+
def test_get_users(client):
|
|
295
|
+
response = client.get('/api/users/')
|
|
296
|
+
assert response.status_code == 200
|
|
297
|
+
assert 'users' in response.json
|
|
298
|
+
|
|
299
|
+
def test_create_user(client):
|
|
300
|
+
response = client.post('/api/users/', json={
|
|
301
|
+
'email': 'new@example.com',
|
|
302
|
+
'name': 'New User',
|
|
303
|
+
'password': 'password123',
|
|
304
|
+
})
|
|
305
|
+
assert response.status_code == 201
|
|
306
|
+
assert response.json['email'] == 'new@example.com'
|
|
307
|
+
\`\`\`
|
|
308
|
+
|
|
309
|
+
## ❌ DON'T
|
|
310
|
+
- Use app.run() in production (use gunicorn)
|
|
311
|
+
- Store secrets in code
|
|
312
|
+
- Forget to use application factory
|
|
313
|
+
- Skip request validation
|
|
314
|
+
|
|
315
|
+
## ✅ DO
|
|
316
|
+
- Use blueprints for organization
|
|
317
|
+
- Use Flask-SQLAlchemy for ORM
|
|
318
|
+
- Use Flask-Migrate for migrations
|
|
319
|
+
- Use Flask-JWT-Extended for auth
|
|
320
|
+
- Use Marshmallow for validation
|
|
321
|
+
- Use application factory pattern
|
|
322
|
+
- Handle errors globally
|