Skip to content

Commit 17f3dd2

Browse files
authored
Add admin functionality and board configuration
- Create admin page for board configuration management - Add GetBoardData query to fetch board configuration - Generate UpdateBoardTitle and UpdateBoardEditWindow queries - Modify board_data table structure for new configuration options - Update server logic to use dynamic board title from database - Add admin link in footer for users with admin privileges - Extend User struct to include IsAdmin field - Update tests to accommodate new admin and board data functionality - Refactor MockQueries to support new board data operations - Add admin.html template for the admin configuration page - Closes #49 Change-Id: I07df7a29313ed74b04bb8873a2545c6ba3a46aa3 Signed-off-by: Ian Meyer <[email protected]>
1 parent bed4218 commit 17f3dd2

File tree

10 files changed

+348
-42
lines changed

10 files changed

+348
-42
lines changed

BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ go_library(
2424
"server.go",
2525
],
2626
embedsrcs = [
27+
"tmpl/admin.html",
2728
"tmpl/edit-profile.html",
2829
"tmpl/edit-thread.html",
2930
"tmpl/edit-thread-post.html",

models.go

Lines changed: 8 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

querier.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

queries.sql.go

Lines changed: 54 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server.go

Lines changed: 159 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -162,13 +162,15 @@ func setupMux(dsvc *DiscussService) http.Handler {
162162

163163
tailnetMux := http.NewServeMux()
164164

165+
tailnetMux.HandleFunc("POST /admin", CSRFMiddleware(dsvc.Admin))
165166
tailnetMux.HandleFunc("POST /member/edit", CSRFMiddleware(dsvc.EditMemberProfile))
166167
tailnetMux.HandleFunc("POST /thread/new", CSRFMiddleware(dsvc.CreateThread))
167168
tailnetMux.HandleFunc("POST /thread/{tid}/edit", CSRFMiddleware(dsvc.EditThread))
168169
tailnetMux.HandleFunc("POST /thread/{tid}/{pid}/edit", CSRFMiddleware(dsvc.EditThreadPost))
169170
tailnetMux.HandleFunc("POST /thread/{tid}", CSRFMiddleware(dsvc.CreateThreadPost))
170171

171172
tailnetMux.HandleFunc("GET /{$}", CSRFMiddleware(dsvc.ListThreads))
173+
tailnetMux.HandleFunc("GET /admin", CSRFMiddleware(dsvc.Admin))
172174
tailnetMux.HandleFunc("GET /member/{mid}", CSRFMiddleware(dsvc.ListMember))
173175
tailnetMux.HandleFunc("GET /member/edit", CSRFMiddleware(dsvc.EditMemberProfile))
174176
tailnetMux.HandleFunc("GET /thread/new", CSRFMiddleware(dsvc.NewThread))
@@ -306,6 +308,134 @@ type ThreadTemplateData struct {
306308
CanEdit pgtype.Bool
307309
}
308310

311+
func (s *DiscussService) Admin(w http.ResponseWriter, r *http.Request) {
312+
s.logger.DebugContext(r.Context(), "entering Admin()")
313+
defer s.logger.DebugContext(r.Context(), "exiting Admin()")
314+
315+
switch r.Method {
316+
case http.MethodGet:
317+
s.AdminGET(w, r)
318+
case http.MethodPost:
319+
s.AdminPOST(w, r)
320+
default:
321+
s.renderError(w, http.StatusMethodNotAllowed)
322+
}
323+
}
324+
325+
func (s *DiscussService) AdminGET(w http.ResponseWriter, r *http.Request) {
326+
s.logger.DebugContext(r.Context(), "entering AdminGET()")
327+
defer s.logger.DebugContext(r.Context(), "exiting AdminGET()")
328+
329+
csrfToken := GetCSRFToken(r)
330+
331+
user, err := GetUser(r)
332+
if err != nil {
333+
s.logger.ErrorContext(r.Context(), err.Error())
334+
s.renderError(w, http.StatusInternalServerError)
335+
return
336+
}
337+
338+
if !user.IsAdmin {
339+
s.logger.ErrorContext(
340+
r.Context(),
341+
"user is not admin",
342+
slog.String("email", user.Email),
343+
slog.Int64("user_id", user.ID),
344+
slog.Bool("is_admin", user.IsAdmin),
345+
)
346+
s.renderError(w, http.StatusForbidden)
347+
return
348+
}
349+
350+
boardData, err := s.queries.GetBoardData(r.Context())
351+
if err != nil {
352+
s.logger.ErrorContext(r.Context(), err.Error())
353+
s.renderError(w, http.StatusInternalServerError)
354+
return
355+
}
356+
357+
s.renderTemplate(w, r, "admin.html", map[string]interface{}{
358+
"Title": GetBoardTitle(r),
359+
"BoardData": boardData,
360+
"Version": s.version,
361+
"GitSha": s.gitSha,
362+
"CSRFToken": csrfToken,
363+
"User": user,
364+
})
365+
}
366+
367+
func (s *DiscussService) AdminPOST(w http.ResponseWriter, r *http.Request) {
368+
s.logger.DebugContext(r.Context(), "entering AdminPOST()")
369+
defer s.logger.DebugContext(r.Context(), "exiting AdminPOST()")
370+
371+
if err := validateCSRFToken(r); err != nil {
372+
s.logger.ErrorContext(r.Context(), "CSRF validation failed", slog.String("error", err.Error()))
373+
http.Error(w, "CSRF validation failed", http.StatusForbidden)
374+
return
375+
}
376+
377+
user, err := GetUser(r)
378+
if err != nil {
379+
s.logger.ErrorContext(r.Context(), err.Error())
380+
s.renderError(w, http.StatusInternalServerError)
381+
return
382+
}
383+
384+
if !user.IsAdmin {
385+
s.logger.ErrorContext(
386+
r.Context(),
387+
"user is not admin",
388+
slog.String("email", user.Email),
389+
slog.Int64("user_id", user.ID),
390+
slog.Bool("is_admin", user.IsAdmin),
391+
)
392+
s.renderError(w, http.StatusForbidden)
393+
return
394+
}
395+
396+
if err := r.ParseForm(); err != nil {
397+
s.logger.ErrorContext(r.Context(), err.Error())
398+
s.renderError(w, http.StatusBadRequest)
399+
return
400+
}
401+
402+
if !r.Form.Has("board_title") {
403+
s.logger.ErrorContext(r.Context(), "missing board_title")
404+
s.renderError(w, http.StatusBadRequest)
405+
return
406+
}
407+
408+
boardTitle := r.Form.Get("board_title")
409+
410+
if err := s.queries.UpdateBoardTitle(r.Context(), boardTitle); err != nil {
411+
s.logger.ErrorContext(r.Context(), err.Error())
412+
s.renderError(w, http.StatusInternalServerError)
413+
return
414+
}
415+
416+
if !r.Form.Has("edit_window") {
417+
s.logger.ErrorContext(r.Context(), "missing edit_window")
418+
s.renderError(w, http.StatusBadRequest)
419+
return
420+
}
421+
422+
editWindow, err := strconv.ParseInt(r.Form.Get("edit_window"), 10, 32)
423+
if err != nil {
424+
s.logger.ErrorContext(r.Context(), err.Error())
425+
s.renderError(w, http.StatusBadRequest)
426+
return
427+
}
428+
429+
if err := s.queries.UpdateBoardEditWindow(r.Context(), pgtype.Int4{Int32: int32(editWindow), Valid: true}); err != nil {
430+
s.logger.ErrorContext(r.Context(), err.Error())
431+
s.renderError(w, http.StatusInternalServerError)
432+
return
433+
}
434+
435+
http.Redirect(w, r, "/", http.StatusSeeOther)
436+
437+
}
438+
309439
// CreateThread handles the creation of a new thread.
310440
func (s *DiscussService) CreateThread(w http.ResponseWriter, r *http.Request) {
311441
if err := validateCSRFToken(r); err != nil {
@@ -482,6 +612,7 @@ func (s *DiscussService) EditMemberProfile(w http.ResponseWriter, r *http.Reques
482612
"Version": s.version,
483613
"GitSha": s.gitSha,
484614
"CSRFToken": csrfToken,
615+
"User": user,
485616
})
486617
return
487618
}
@@ -643,11 +774,12 @@ func (s *DiscussService) editThreadGET(w http.ResponseWriter, r *http.Request) {
643774
}
644775

645776
s.renderTemplate(w, r, "edit-thread.html", map[string]interface{}{
646-
"Title": BOARD_TITLE,
777+
"Title": GetBoardTitle(r),
647778
"Thread": thread,
648779
"Version": s.version,
649780
"CSRFToken": csrfToken,
650781
"GitSha": s.gitSha,
782+
"User": user,
651783
})
652784
return
653785
}
@@ -772,6 +904,7 @@ func (s *DiscussService) editThreadPostGET(w http.ResponseWriter, r *http.Reques
772904
"Post": post,
773905
"ThreadID": threadID,
774906
"GitSha": s.gitSha,
907+
"User": user,
775908
})
776909
}
777910

@@ -842,10 +975,11 @@ func (s *DiscussService) ListThreads(w http.ResponseWriter, r *http.Request) {
842975
}
843976

844977
s.renderTemplate(w, r, "index.html", map[string]interface{}{
845-
"Title": BOARD_TITLE,
978+
"Title": GetBoardTitle(r),
846979
"Threads": threadsParsed,
847980
"Version": s.version,
848981
"GitSha": s.gitSha,
982+
"User": user,
849983
})
850984
}
851985

@@ -917,14 +1051,15 @@ func (s *DiscussService) ListThreadPosts(w http.ResponseWriter, r *http.Request)
9171051
csrfToken := GetCSRFToken(r)
9181052

9191053
s.renderTemplate(w, r, "thread.html", map[string]interface{}{
920-
"Title": BOARD_TITLE,
1054+
"Title": GetBoardTitle(r),
9211055
"ThreadPosts": threadPosts,
9221056
// nosemgrep
9231057
"Subject": template.HTML(subject),
9241058
"ID": threadID,
9251059
"GitSha": s.gitSha,
9261060
"Version": s.version,
9271061
"CSRFToken": template.HTML(csrfToken),
1062+
"User": user,
9281063
})
9291064
}
9301065

@@ -987,7 +1122,7 @@ func (s *DiscussService) ListMember(w http.ResponseWriter, r *http.Request) {
9871122
}
9881123

9891124
s.renderTemplate(w, r, "member.html", map[string]interface{}{
990-
"Title": BOARD_TITLE,
1125+
"Title": GetBoardTitle(r),
9911126
"Member": member,
9921127
"CurrentUserEmail": user.Email,
9931128
"Threads": memberThreadsParsed,
@@ -1025,7 +1160,7 @@ func (s *DiscussService) NewThread(w http.ResponseWriter, r *http.Request) {
10251160

10261161
s.renderTemplate(w, r, "newthread.html", map[string]interface{}{
10271162
"User": user,
1028-
"Title": BOARD_TITLE,
1163+
"Title": GetBoardTitle(r),
10291164
"CSRFToken": csrfToken,
10301165
"Version": s.version,
10311166
"GitSha": s.gitSha,
@@ -1044,6 +1179,16 @@ func UserMiddleware(s *DiscussService, next http.Handler) http.HandlerFunc {
10441179

10451180
ctx := context.WithValue(r.Context(), "email", email)
10461181

1182+
boardData, err := s.queries.GetBoardData(r.Context())
1183+
if err != nil {
1184+
s.logger.ErrorContext(r.Context(), err.Error())
1185+
s.renderError(w, http.StatusInternalServerError)
1186+
return
1187+
}
1188+
1189+
// Set the board title in the context
1190+
ctx = context.WithValue(ctx, "board_title", boardData.Title)
1191+
10471192
user, err := s.queries.CreateOrReturnID(ctx, email)
10481193
if err != nil {
10491194
s.logger.ErrorContext(ctx, err.Error())
@@ -1087,3 +1232,12 @@ func GetUser(r *http.Request) (User, error) {
10871232

10881233
return u, nil
10891234
}
1235+
1236+
func GetBoardTitle(r *http.Request) string {
1237+
ctx := r.Context()
1238+
if title, ok := ctx.Value("board_title").(string); ok {
1239+
return title
1240+
}
1241+
1242+
return BOARD_TITLE
1243+
}

0 commit comments

Comments
 (0)