@@ -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.
310440func (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