summaryrefslogtreecommitdiffstats
path: root/docs/superpowers/specs/2026-04-03-gin-migration-design.md
blob: 41163a30a2727987d1de9b5299b06286e6969d14 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
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