apps/cellar/handlers_admin.go 7.7 K raw
1
package main
2
3
import (
4
	"io"
5
	"net/http"
6
	"net/url"
7
8
	"github.com/stevedylandev/andromeda/pkg/auth"
9
	"github.com/stevedylandev/andromeda/pkg/web"
10
)
11
12
func (a *App) loginGet(w http.ResponseWriter, r *http.Request) {
13
	q := r.URL.Query()
14
	a.renderPage(w, "login.html", loginPageData{Error: q.Get("error"), Next: q.Get("next")})
15
}
16
17
func (a *App) loginPost(w http.ResponseWriter, r *http.Request) {
18
	next := r.URL.Query().Get("next")
19
	if next == "" {
20
		next = "/admin"
21
	}
22
	if err := r.ParseForm(); err != nil {
23
		http.Redirect(w, r, "/admin/login?error=Bad+request", http.StatusSeeOther)
24
		return
25
	}
26
	if !auth.VerifyPassword(r.FormValue("password"), a.AppPassword) {
27
		http.Redirect(w, r, "/admin/login?error=Invalid+password&next="+url.QueryEscape(next), http.StatusSeeOther)
28
		return
29
	}
30
	token, err := a.Sessions.Create()
31
	if err != nil {
32
		a.Log.Error("create session failed", "err", err)
33
		http.Redirect(w, r, "/admin/login?error=Server+error", http.StatusSeeOther)
34
		return
35
	}
36
	a.Sessions.PruneExpired()
37
	http.SetCookie(w, a.Sessions.SessionCookie(token))
38
	target := "/admin"
39
	if len(next) > 0 && next[0] == '/' {
40
		target = next
41
	}
42
	http.Redirect(w, r, target, http.StatusSeeOther)
43
}
44
45
func (a *App) logout(w http.ResponseWriter, r *http.Request) {
46
	if c, err := r.Cookie(a.Sessions.CookieName); err == nil && c.Value != "" {
47
		a.Sessions.Delete(c.Value)
48
	}
49
	http.SetCookie(w, a.Sessions.ClearCookie())
50
	http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
51
}
52
53
func (a *App) adminIndex(w http.ResponseWriter, r *http.Request) {
54
	wines, err := getCellarWines(a.DB)
55
	if err != nil {
56
		a.Log.Error("list wines", "err", err)
57
		http.Error(w, "Server error", http.StatusInternalServerError)
58
		return
59
	}
60
	a.renderPage(w, "admin.html", adminPageData{Wines: wines})
61
}
62
63
func (a *App) newWineGet(w http.ResponseWriter, r *http.Request) {
64
	a.renderPage(w, "wine_form.html", wineFormPageData{Error: r.URL.Query().Get("error"), HasAnthropicKey: a.AnthropicAPIKey != ""})
65
}
66
67
func (a *App) editWineGet(w http.ResponseWriter, r *http.Request) {
68
	shortID := r.PathValue("short_id")
69
	wine, err := getWineByShortID(a.DB, shortID)
70
	if err != nil {
71
		http.Error(w, "Server error", http.StatusInternalServerError)
72
		return
73
	}
74
	if wine == nil {
75
		http.Error(w, "Wine not found", http.StatusNotFound)
76
		return
77
	}
78
	a.renderPage(w, "wine_form.html", wineFormPageData{Wine: wine, Error: r.URL.Query().Get("error"), HasAnthropicKey: a.AnthropicAPIKey != ""})
79
}
80
81
func (a *App) newWinePost(w http.ResponseWriter, r *http.Request) {
82
	data, err := parseWineMultipart(r)
83
	if err != nil {
84
		http.Redirect(w, r, "/admin/new?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
85
		return
86
	}
87
	wine, err := createWine(a.DB, formToInput(data), false)
88
	if err != nil {
89
		a.Log.Error("create wine", "err", err)
90
		http.Redirect(w, r, "/admin/new?error=Failed+to+create+wine", http.StatusSeeOther)
91
		return
92
	}
93
	if len(data.Image) > 0 {
94
		if err := updateWineImage(a.DB, wine.ShortID, data.Image, data.ImageMime); err != nil {
95
			a.Log.Error("set wine image", "err", err)
96
		}
97
	}
98
	http.Redirect(w, r, "/wines/"+wine.ShortID, http.StatusSeeOther)
99
}
100
101
func (a *App) editWinePost(w http.ResponseWriter, r *http.Request) {
102
	shortID := r.PathValue("short_id")
103
	data, err := parseWineMultipart(r)
104
	if err != nil {
105
		http.Redirect(w, r, "/admin/edit/"+shortID+"?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
106
		return
107
	}
108
	wine, err := updateWine(a.DB, shortID, formToInput(data))
109
	if err != nil {
110
		a.Log.Error("update wine", "err", err)
111
		http.Redirect(w, r, "/admin/edit/"+shortID+"?error=Failed+to+update+wine", http.StatusSeeOther)
112
		return
113
	}
114
	if wine == nil {
115
		http.Error(w, "Wine not found", http.StatusNotFound)
116
		return
117
	}
118
	if len(data.Image) > 0 {
119
		if err := updateWineImage(a.DB, shortID, data.Image, data.ImageMime); err != nil {
120
			a.Log.Error("update wine image", "err", err)
121
		}
122
	}
123
	http.Redirect(w, r, "/wines/"+shortID, http.StatusSeeOther)
124
}
125
126
func (a *App) deleteWinePost(w http.ResponseWriter, r *http.Request) {
127
	_ = deleteWine(a.DB, r.PathValue("short_id"))
128
	http.Redirect(w, r, "/admin", http.StatusSeeOther)
129
}
130
131
func (a *App) newWishlistGet(w http.ResponseWriter, r *http.Request) {
132
	a.renderPage(w, "wishlist_form.html", wineFormPageData{Error: r.URL.Query().Get("error"), HasAnthropicKey: a.AnthropicAPIKey != ""})
133
}
134
135
func (a *App) editWishlistGet(w http.ResponseWriter, r *http.Request) {
136
	wine, err := getWineByShortID(a.DB, r.PathValue("short_id"))
137
	if err != nil {
138
		http.Error(w, "Server error", http.StatusInternalServerError)
139
		return
140
	}
141
	if wine == nil {
142
		http.Error(w, "Wine not found", http.StatusNotFound)
143
		return
144
	}
145
	a.renderPage(w, "wishlist_form.html", wineFormPageData{Wine: wine, Error: r.URL.Query().Get("error"), HasAnthropicKey: a.AnthropicAPIKey != ""})
146
}
147
148
func (a *App) newWishlistPost(w http.ResponseWriter, r *http.Request) {
149
	data, err := parseWishlistMultipart(r)
150
	if err != nil {
151
		http.Redirect(w, r, "/admin/wishlist/new?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
152
		return
153
	}
154
	wine, err := createWine(a.DB, formToInput(data), true)
155
	if err != nil {
156
		a.Log.Error("create wishlist wine", "err", err)
157
		http.Redirect(w, r, "/admin/wishlist/new?error=Failed+to+create+wine", http.StatusSeeOther)
158
		return
159
	}
160
	if len(data.Image) > 0 {
161
		_ = updateWineImage(a.DB, wine.ShortID, data.Image, data.ImageMime)
162
	}
163
	http.Redirect(w, r, "/wishlist", http.StatusSeeOther)
164
}
165
166
func (a *App) editWishlistPost(w http.ResponseWriter, r *http.Request) {
167
	shortID := r.PathValue("short_id")
168
	data, err := parseWishlistMultipart(r)
169
	if err != nil {
170
		http.Redirect(w, r, "/admin/wishlist/edit/"+shortID+"?error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
171
		return
172
	}
173
	wine, err := updateWishlistWine(a.DB, shortID, data.Name, data.Origin, data.Grape, data.Notes, data.Background)
174
	if err != nil {
175
		a.Log.Error("update wishlist wine", "err", err)
176
		http.Redirect(w, r, "/admin/wishlist/edit/"+shortID+"?error=Failed+to+update+wine", http.StatusSeeOther)
177
		return
178
	}
179
	if wine == nil {
180
		http.Error(w, "Wine not found", http.StatusNotFound)
181
		return
182
	}
183
	if len(data.Image) > 0 {
184
		_ = updateWineImage(a.DB, shortID, data.Image, data.ImageMime)
185
	}
186
	http.Redirect(w, r, "/wishlist", http.StatusSeeOther)
187
}
188
189
func (a *App) deleteWishlistPost(w http.ResponseWriter, r *http.Request) {
190
	_ = deleteWine(a.DB, r.PathValue("short_id"))
191
	http.Redirect(w, r, "/wishlist", http.StatusSeeOther)
192
}
193
194
func (a *App) promoteWinePost(w http.ResponseWriter, r *http.Request) {
195
	shortID := r.PathValue("short_id")
196
	ok, err := promoteWine(a.DB, shortID)
197
	if err != nil {
198
		http.Redirect(w, r, "/wishlist", http.StatusSeeOther)
199
		return
200
	}
201
	if !ok {
202
		http.Error(w, "Wine not found", http.StatusNotFound)
203
		return
204
	}
205
	http.Redirect(w, r, "/admin/edit/"+shortID, http.StatusSeeOther)
206
}
207
208
func (a *App) analyzeImage(w http.ResponseWriter, r *http.Request) {
209
	if a.AnthropicAPIKey == "" {
210
		web.WriteError(w, http.StatusBadRequest, "No API key configured")
211
		return
212
	}
213
	r.Body = http.MaxBytesReader(w, r.Body, maxUploadBytes)
214
	if err := r.ParseMultipartForm(maxUploadBytes); err != nil {
215
		web.WriteError(w, http.StatusBadRequest, err.Error())
216
		return
217
	}
218
	file, header, err := r.FormFile("image")
219
	if err != nil {
220
		web.WriteError(w, http.StatusBadRequest, "No image provided")
221
		return
222
	}
223
	defer file.Close()
224
	raw, err := io.ReadAll(file)
225
	if err != nil || len(raw) == 0 {
226
		web.WriteError(w, http.StatusBadRequest, "No image provided")
227
		return
228
	}
229
	mediaType := "image/jpeg"
230
	if header != nil && header.Header.Get("Content-Type") != "" {
231
		mediaType = header.Header.Get("Content-Type")
232
	}
233
	result, err := analyzeWineImage(r.Context(), a.AnthropicAPIKey, raw, mediaType)
234
	if err != nil {
235
		a.Log.Error("Claude analysis failed", "err", err)
236
		web.WriteError(w, http.StatusInternalServerError, err.Error())
237
		return
238
	}
239
	web.WriteJSON(w, http.StatusOK, result)
240
}