--- name: Gin Migration Design description: Migrate admin server from net/http to Gin framework with RESTful routes and middleware-based auth type: implementation date: 2026-04-03 --- # Gin Migration Design ## Overview Migrate the nebbet admin server from Go's `net/http` package to the Gin web framework. The migration will: - Replace manual route handling with Gin's declarative routing - Reorganize routes to follow RESTful conventions - Extract auth into reusable middleware - Preserve all existing functionality (auth, template rendering, post management) - Keep UI behavior identical from the user's perspective ## Goals 1. **Cleaner routing:** Replace manual `ServeHTTP` switch with Gin's route declarations 2. **Better framework:** Leverage Gin for middleware, error handling, and future extensibility 3. **RESTful design:** Modernize route structure while keeping templates server-rendered ## Non-Goals - Rewriting the frontend to a JS framework - Adding new features beyond the migration - Changing auth format or password file structure - Changing how posts are stored or built ## Architecture ### Current State The `admin.Server` implements `http.Handler` with manual routing in `ServeHTTP()`: ``` GET/POST /admin → list posts GET/POST /admin/new → create form / create post GET/POST /admin/{slug}/edit → edit form / update post POST /admin/{slug}/delete → delete post ``` Auth is checked manually via `checkAuth()` for every request. ### Target State Replace with Gin routing and middleware: - `Server` struct holds a `*gin.Engine` - `NewServer()` initializes Gin with routes and middleware - Auth middleware wraps all admin routes - Handlers call `gin.Context` instead of `http.ResponseWriter` **New route structure (RESTful, under `/admin` namespace):** ``` GET /admin/ → list posts GET /admin/new → create form POST /admin/ → create post GET /admin/:slug → edit form POST /admin/:slug → update post DELETE /admin/:slug → delete post ``` ## Implementation Details ### File: `internal/admin/server.go` #### Server Struct - Add field: `engine *gin.Engine` - Keep existing fields: `PostsDir`, `AuthFile`, `Builder`, `tmpl` #### Constructor: `NewServer(postsDir, authFile string, builder *builder.Builder) *Server` - Create and configure Gin engine - Register middleware (auth) - Register all routes - Return `*Server` #### Auth Middleware - Extracted from `checkAuth()` into a middleware function - Signature: `func (s *Server) authMiddleware() gin.HandlerFunc` - Logic: - Skip auth if `AuthFile` is empty or doesn't exist - Extract Basic Auth credentials - Call `auth.Verify(username, password)` - Send 401 + `WWW-Authenticate` header on failure - Call `c.Next()` to proceed on success #### Route Handlers Keep existing handler functions, update signatures: - `handleList(c *gin.Context)` — render post list - `handleNew(c *gin.Context)` — GET shows form, POST creates post - `handleNewPost(c *gin.Context)` — handle POST /new (merge with `handleNew` or keep separate) - `handleEdit(c *gin.Context)` — GET shows form, POST updates post - `handleDelete(c *gin.Context)` — DELETE removes post For GET requests that show forms, continue using `c.HTML()` with the template and data map. For POST requests, validate form data, write to disk, rebuild, and redirect to `/`. #### Helper Functions Keep existing: - `listPosts()` — read `.md` files from PostsDir - `render(c *gin.Context, name string, data map[string]any)` — render template (adapt to use `c.HTML()`) - `renderError(c *gin.Context, msg string)` — render error template - `postFromForm(c *gin.Context) Post` — extract form values from `c.PostForm()` - `readPostFile()`, `writePostFile()`, `slugify()` — unchanged - `mustParseTemplates()` — unchanged ### File: `cmd/nebbet/main.go` #### Changes in `cmdServe()` Instead of: ```go adminSrv := &admin.Server{...} mux := http.NewServeMux() mux.Handle("/admin/", http.StripPrefix("/admin", adminSrv)) mux.Handle("/lib/", ...) mux.Handle("/", ...) http.ListenAndServe(addr, mux) ``` Change to: ```go adminSrv := admin.NewServer(postsDir, passwordFile, b) engine := adminSrv.Engine() // or keep internal, add to main router // Either: // 1. Use adminSrv.Engine directly (if it handles all routes) // 2. Create a main Gin router and nest adminSrv's routes // Handle /lib/ and static files as Gin routes or separate handlers ``` Exact approach depends on whether we keep `/lib` and public site serving separate or consolidate into one Gin router. **Recommended:** single Gin router for consistency. ## Route Mapping | Old Route | New Route | Method | Action | |-----------|-----------|--------|--------| | `/admin/` | `/admin/` | GET | List posts | | `/admin/new` | `/admin/new` | GET | Show create form | | `/admin/new` | `/admin/` | POST | Create post | | `/admin/{slug}/edit` | `/admin/{slug}` | GET | Show edit form | | `/admin/{slug}/edit` | `/admin/{slug}` | POST | Update post | | `/admin/{slug}/delete` | `/admin/{slug}` | DELETE | Delete post | **HTML Form Handling:** Standard HTML forms only support GET/POST. For DELETE: - **Option A:** Keep POST with custom handling (check method override or hidden field) - **Option B:** Use POST `/admin/{slug}?method=delete` and parse in handler Recommended: keep as POST for form compatibility, or use POST with Gin's `c.PostForm("_method")` check. ## Dependencies - Add `github.com/gin-gonic/gin` to `go.mod` - No changes to other dependencies ## Backwards Compatibility - **URLs:** Admin routes change (listed above). Bookmarks will break, but it's an internal admin interface. - **Auth:** No changes. Password file format and Basic Auth remain the same. - **Posts:** No changes. Markdown files, frontmatter, build process unchanged. - **Public site:** No changes. Static files served the same way. ## Testing Strategy - **Manual:** Create/edit/delete posts via the admin UI, verify rebuilds work - **Auth:** Test Basic Auth with valid/invalid credentials - **Forms:** Verify all form fields (title, date, tags, content) are captured and saved correctly - **Errors:** Verify error handling for missing posts, invalid input, file write failures ## Implementation Order 1. Add Gin dependency to `go.mod` 2. Create `NewServer()` constructor and auth middleware in `server.go` 3. Register routes in `NewServer()` 4. Convert handler signatures to `*gin.Context` 5. Update `render()` and helper functions to work with Gin 6. Update `cmd/nebbet/main.go` to use `NewServer()` 7. Verify all routes work locally 8. Test admin UI end-to-end ## Risks & Mitigation | Risk | Mitigation | |------|-----------| | Breaking the admin UI during migration | Test each route in a browser after updating | | Form submission issues (e.g., multipart/form-data) | Use `c.PostForm()` and test file uploads if used | | Auth middleware interfering with static files | Apply middleware only to admin routes, not `/lib` or `/` | | Template rendering errors | Keep template loading and execution the same | ## Success Criteria - ✓ All admin routes work with Gin - ✓ Auth middleware blocks unauthorized access - ✓ Create/edit/delete posts works end-to-end - ✓ Posts rebuild after create/edit/delete - ✓ No changes to password file format or post storage - ✓ Static file serving (`/lib`, public site) unchanged