home to your local SPACEGIRL 馃挮
arimelody.space
1package admin
2
3import (
4 "database/sql"
5 "fmt"
6 "net/http"
7 "net/url"
8 "os"
9
10 "arimelody-web/admin/templates"
11 "arimelody-web/controller"
12 "arimelody-web/log"
13 "arimelody-web/model"
14
15 "golang.org/x/crypto/bcrypt"
16)
17
18func accountHandler(app *model.AppState) http.Handler {
19 mux := http.NewServeMux()
20
21 mux.Handle("/account/totp-setup", totpSetupHandler(app))
22 mux.Handle("/account/totp-confirm", totpConfirmHandler(app))
23 mux.Handle("/account/totp-delete", totpDeleteHandler(app))
24
25 mux.Handle("/account/password", changePasswordHandler(app))
26 mux.Handle("/account/delete", deleteAccountHandler(app))
27
28 return mux
29}
30
31func accountIndexHandler(app *model.AppState) http.Handler {
32 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
33 session := r.Context().Value("session").(*model.Session)
34
35 dbTOTPs, err := controller.GetTOTPsForAccount(app.DB, session.Account.ID)
36 if err != nil {
37 fmt.Printf("WARN: Failed to fetch TOTPs: %v\n", err)
38 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
39 }
40
41 type (
42 TOTP struct {
43 model.TOTP
44 CreatedAtString string
45 }
46
47 accountResponse struct {
48 adminPageData
49 TOTPs []TOTP
50 }
51 )
52
53 totps := []TOTP{}
54 for _, totp := range dbTOTPs {
55 totps = append(totps, TOTP{
56 TOTP: totp,
57 CreatedAtString: totp.CreatedAt.Format("02 Jan 2006, 15:04:05"),
58 })
59 }
60
61 sessionMessage := session.Message
62 sessionError := session.Error
63 controller.SetSessionMessage(app.DB, session, "")
64 controller.SetSessionError(app.DB, session, "")
65 session.Message = sessionMessage
66 session.Error = sessionError
67
68 err = templates.AccountTemplate.Execute(w, accountResponse{
69 adminPageData: adminPageData{ Path: r.URL.Path, Session: session },
70 TOTPs: totps,
71 })
72 if err != nil {
73 fmt.Printf("WARN: Failed to render admin account page: %v\n", err)
74 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
75 }
76 })
77}
78
79func changePasswordHandler(app *model.AppState) http.Handler {
80 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
81 if r.Method != http.MethodPost {
82 http.NotFound(w, r)
83 return
84 }
85
86 session := r.Context().Value("session").(*model.Session)
87
88 controller.SetSessionMessage(app.DB, session, "")
89 controller.SetSessionError(app.DB, session, "")
90
91 r.ParseForm()
92
93 currentPassword := r.Form.Get("current-password")
94 if err := bcrypt.CompareHashAndPassword([]byte(session.Account.Password), []byte(currentPassword)); err != nil {
95 controller.SetSessionError(app.DB, session, "Incorrect password.")
96 http.Redirect(w, r, "/admin/account", http.StatusFound)
97 return
98 }
99
100 newPassword := r.Form.Get("new-password")
101
102 hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
103 if err != nil {
104 fmt.Fprintf(os.Stderr, "WARN: Failed to generate password hash: %v\n", err)
105 controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
106 http.Redirect(w, r, "/admin/account", http.StatusFound)
107 return
108 }
109
110 session.Account.Password = string(hashedPassword)
111 err = controller.UpdateAccount(app.DB, session.Account)
112 if err != nil {
113 fmt.Fprintf(os.Stderr, "WARN: Failed to update account password: %v\n", err)
114 controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
115 http.Redirect(w, r, "/admin/account", http.StatusFound)
116 return
117 }
118
119 app.Log.Info(log.TYPE_ACCOUNT, "\"%s\" changed password by user request. (%s)", session.Account.Username, controller.ResolveIP(app, r))
120
121 controller.SetSessionError(app.DB, session, "")
122 controller.SetSessionMessage(app.DB, session, "Password updated successfully.")
123 http.Redirect(w, r, "/admin/account", http.StatusFound)
124 })
125}
126
127func deleteAccountHandler(app *model.AppState) http.Handler {
128 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
129 if r.Method != http.MethodPost {
130 http.NotFound(w, r)
131 return
132 }
133
134 err := r.ParseForm()
135 if err != nil {
136 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
137 return
138 }
139
140 if !r.Form.Has("password") {
141 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
142 return
143 }
144
145 session := r.Context().Value("session").(*model.Session)
146
147 // check password
148 if err := bcrypt.CompareHashAndPassword([]byte(session.Account.Password), []byte(r.Form.Get("password"))); err != nil {
149 app.Log.Warn(log.TYPE_ACCOUNT, "Account \"%s\" attempted account deletion with incorrect password. (%s)", session.Account.Username, controller.ResolveIP(app, r))
150 controller.SetSessionError(app.DB, session, "Incorrect password.")
151 http.Redirect(w, r, "/admin/account", http.StatusFound)
152 return
153 }
154
155 err = controller.DeleteAccount(app.DB, session.Account.ID)
156 if err != nil {
157 fmt.Fprintf(os.Stderr, "Failed to delete account: %v\n", err)
158 controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
159 http.Redirect(w, r, "/admin/account", http.StatusFound)
160 return
161 }
162
163 app.Log.Info(log.TYPE_ACCOUNT, "Account \"%s\" deleted by user request. (%s)", session.Account.Username, controller.ResolveIP(app, r))
164
165 controller.SetSessionAccount(app.DB, session, nil)
166 controller.SetSessionError(app.DB, session, "")
167 controller.SetSessionMessage(app.DB, session, "Account deleted successfully.")
168 http.Redirect(w, r, "/admin/login", http.StatusFound)
169 })
170}
171
172type totpConfirmData struct {
173 adminPageData
174 TOTP *model.TOTP
175 NameEscaped string
176 QRBase64Image string
177}
178
179func totpSetupHandler(app *model.AppState) http.Handler {
180 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
181 if r.Method == http.MethodGet {
182 session := r.Context().Value("session").(*model.Session)
183
184 err := templates.TOTPSetupTemplate.Execute(w, adminPageData{ Path: "/account", Session: session })
185 if err != nil {
186 fmt.Printf("WARN: Failed to render TOTP setup page: %s\n", err)
187 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
188 }
189 return
190 }
191
192 if r.Method != http.MethodPost {
193 http.NotFound(w, r)
194 return
195 }
196
197 err := r.ParseForm()
198 if err != nil {
199 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
200 return
201 }
202
203 name := r.FormValue("totp-name")
204 if len(name) == 0 {
205 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
206 return
207 }
208
209 session := r.Context().Value("session").(*model.Session)
210
211 secret := controller.GenerateTOTPSecret(controller.TOTP_SECRET_LENGTH)
212 totp := model.TOTP {
213 AccountID: session.Account.ID,
214 Name: name,
215 Secret: string(secret),
216 }
217 err = controller.CreateTOTP(app.DB, &totp)
218 if err != nil {
219 fmt.Printf("WARN: Failed to create TOTP method: %s\n", err)
220 controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
221 err := templates.TOTPSetupTemplate.Execute(w, totpConfirmData{
222 adminPageData: adminPageData{ Path: r.URL.Path, Session: session },
223 })
224 if err != nil {
225 fmt.Printf("WARN: Failed to render TOTP setup page: %s\n", err)
226 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
227 }
228 return
229 }
230
231 qrBase64Image, err := controller.GenerateQRCode(
232 controller.GenerateTOTPURI(session.Account.Username, totp.Secret))
233 if err != nil {
234 fmt.Fprintf(os.Stderr, "WARN: Failed to generate TOTP QR code: %v\n", err)
235 }
236
237 err = templates.TOTPConfirmTemplate.Execute(w, totpConfirmData{
238 adminPageData: adminPageData{ Path: r.URL.Path, Session: session },
239 TOTP: &totp,
240 NameEscaped: url.PathEscape(totp.Name),
241 QRBase64Image: qrBase64Image,
242 })
243 if err != nil {
244 fmt.Printf("WARN: Failed to render TOTP confirm page: %s\n", err)
245 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
246 }
247 })
248}
249
250func totpConfirmHandler(app *model.AppState) http.Handler {
251 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
252 if r.Method != http.MethodPost {
253 http.NotFound(w, r)
254 return
255 }
256
257 session := r.Context().Value("session").(*model.Session)
258
259 err := r.ParseForm()
260 if err != nil {
261 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
262 return
263 }
264 name := r.FormValue("totp-name")
265 if len(name) == 0 {
266 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
267 return
268 }
269
270 totp, err := controller.GetTOTP(app.DB, session.Account.ID, name)
271 if err != nil {
272 fmt.Printf("WARN: Failed to fetch TOTP method: %v\n", err)
273 controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
274 http.Redirect(w, r, "/admin/account", http.StatusFound)
275 return
276 }
277 if totp == nil {
278 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
279 return
280 }
281
282 qrBase64Image, err := controller.GenerateQRCode(
283 controller.GenerateTOTPURI(session.Account.Username, totp.Secret))
284 if err != nil {
285 fmt.Fprintf(os.Stderr, "WARN: Failed to generate TOTP QR code: %v\n", err)
286 }
287
288 code := r.FormValue("totp")
289 confirmCode := controller.GenerateTOTP(totp.Secret, 0)
290 confirmCodeOffset := controller.GenerateTOTP(totp.Secret, 1)
291 if len(code) != controller.TOTP_CODE_LENGTH || (code != confirmCode && code != confirmCodeOffset) {
292 session.Error = sql.NullString{ Valid: true, String: "Incorrect TOTP code. Please try again." }
293 err = templates.TOTPConfirmTemplate.Execute(w, totpConfirmData{
294 adminPageData: adminPageData{ Path: r.URL.Path, Session: session },
295 TOTP: totp,
296 NameEscaped: url.PathEscape(totp.Name),
297 QRBase64Image: qrBase64Image,
298 })
299 if err != nil {
300 fmt.Fprintf(os.Stderr, "WARN: Failed to render TOTP setup page: %v\n", err)
301 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
302 }
303 return
304 }
305
306 err = controller.ConfirmTOTP(app.DB, session.Account.ID, name)
307 if err != nil {
308 fmt.Printf("WARN: Failed to confirm TOTP method: %s\n", err)
309 controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
310 http.Redirect(w, r, "/admin/account", http.StatusFound)
311 return
312 }
313
314 app.Log.Info(log.TYPE_ACCOUNT, "\"%s\" created TOTP method \"%s\".", session.Account.Username, totp.Name)
315
316 controller.SetSessionError(app.DB, session, "")
317 controller.SetSessionMessage(app.DB, session, fmt.Sprintf("TOTP method \"%s\" created successfully.", totp.Name))
318 http.Redirect(w, r, "/admin/account", http.StatusFound)
319 })
320}
321
322func totpDeleteHandler(app *model.AppState) http.Handler {
323 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
324 if r.Method != http.MethodPost {
325 http.NotFound(w, r)
326 return
327 }
328
329 session := r.Context().Value("session").(*model.Session)
330
331 err := r.ParseForm()
332 if err != nil {
333 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
334 return
335 }
336 name := r.FormValue("totp-name")
337 if len(name) == 0 {
338 http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
339 return
340 }
341
342 totp, err := controller.GetTOTP(app.DB, session.Account.ID, name)
343 if err != nil {
344 fmt.Printf("WARN: Failed to fetch TOTP method: %s\n", err)
345 controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
346 http.Redirect(w, r, "/admin/account", http.StatusFound)
347 return
348 }
349 if totp == nil {
350 http.NotFound(w, r)
351 return
352 }
353
354 err = controller.DeleteTOTP(app.DB, session.Account.ID, totp.Name)
355 if err != nil {
356 fmt.Printf("WARN: Failed to delete TOTP method: %s\n", err)
357 controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
358 http.Redirect(w, r, "/admin/account", http.StatusFound)
359 return
360 }
361
362 app.Log.Info(log.TYPE_ACCOUNT, "\"%s\" deleted TOTP method \"%s\".", session.Account.Username, totp.Name)
363
364 controller.SetSessionError(app.DB, session, "")
365 controller.SetSessionMessage(app.DB, session, fmt.Sprintf("TOTP method \"%s\" deleted successfully.", totp.Name))
366 http.Redirect(w, r, "/admin/account", http.StatusFound)
367 })
368}