summaryrefslogtreecommitdiffstats
path: root/docs/superpowers/specs/2026-04-03-gin-migration-design.md
diff options
context:
space:
mode:
authorivar <i@oiee.no>2026-04-03 14:22:23 +0200
committerivar <i@oiee.no>2026-04-03 14:22:23 +0200
commit33310be68544d3381ac6e9899790f4a106e17e8f (patch)
tree385102a7216ffba17d054f11032dae4447371d52 /docs/superpowers/specs/2026-04-03-gin-migration-design.md
parenta8914a8f18c345e934bce93b37845a9dfe0ad73e (diff)
downloadnebbet.no-33310be68544d3381ac6e9899790f4a106e17e8f.tar.xz
nebbet.no-33310be68544d3381ac6e9899790f4a106e17e8f.zip
refactor: convert admin handlers to Gin context-based signatures
- Remove old ServeHTTP method (no longer needed with Gin routing) - Update all 6 handler methods to use *gin.Context instead of http.ResponseWriter, *http.Request - Convert handler signatures: handleList, handleNew, handleNewPost, handleEdit, handleDelete - Remove render() helper (use c.HTML() directly) - Update renderError() to accept gin.Context instead of http.ResponseWriter - Update postFromForm() to extract form data from gin.Context using c.PostForm() - Update main.go to use adminSrv.NewServer() and adminSrv.Engine() - All handlers now use Gin methods: c.HTML(), c.PostForm(), c.Param(), c.Redirect() - Path parameters now extracted via c.Param("slug") instead of function arguments - HTTP status codes and error handling fully migrated to Gin patterns Build verified: go build ./cmd/nebbet succeeds Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Diffstat (limited to 'docs/superpowers/specs/2026-04-03-gin-migration-design.md')
-rw-r--r--docs/superpowers/specs/2026-04-03-gin-migration-design.md198
1 files changed, 198 insertions, 0 deletions
diff --git a/docs/superpowers/specs/2026-04-03-gin-migration-design.md b/docs/superpowers/specs/2026-04-03-gin-migration-design.md
new file mode 100644
index 0000000..41163a3
--- /dev/null
+++ b/docs/superpowers/specs/2026-04-03-gin-migration-design.md
@@ -0,0 +1,198 @@
+---
+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