diff --git a/BUILD.bazel b/BUILD.bazel index cde209e..30d5e52 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -24,6 +24,7 @@ go_library( "server.go", ], embedsrcs = [ + "tmpl/admin.html", "tmpl/edit-profile.html", "tmpl/edit-thread.html", "tmpl/edit-thread-post.html", diff --git a/models.go b/models.go index a07ea8b..d590574 100644 --- a/models.go +++ b/models.go @@ -9,9 +9,14 @@ import ( ) type BoardDatum struct { - ID int32 - Name string - Value string + ID int32 + Title string + AllowEditing pgtype.Bool + AllowDeleting pgtype.Bool + EditWindow pgtype.Int4 + TotalMembers pgtype.Int4 + TotalThreads pgtype.Int4 + TotalThreadPosts pgtype.Int4 } type Member struct { diff --git a/querier.go b/querier.go index 8998ccf..5df24af 100644 --- a/querier.go +++ b/querier.go @@ -6,12 +6,15 @@ package main import ( "context" + + "github.com/jackc/pgx/v5/pgtype" ) type Querier interface { CreateOrReturnID(ctx context.Context, pEmail string) (CreateOrReturnIDRow, error) CreateThread(ctx context.Context, arg CreateThreadParams) error CreateThreadPost(ctx context.Context, arg CreateThreadPostParams) error + GetBoardData(ctx context.Context) (GetBoardDataRow, error) GetMember(ctx context.Context, id int64) (GetMemberRow, error) GetMemberId(ctx context.Context, email string) (int64, error) GetThreadForEdit(ctx context.Context, arg GetThreadForEditParams) (GetThreadForEditRow, error) @@ -22,6 +25,8 @@ type Querier interface { ListMemberThreads(ctx context.Context, memberID int64) ([]ListMemberThreadsRow, error) ListThreadPosts(ctx context.Context, arg ListThreadPostsParams) ([]ListThreadPostsRow, error) ListThreads(ctx context.Context, arg ListThreadsParams) ([]ListThreadsRow, error) + UpdateBoardEditWindow(ctx context.Context, editWindow pgtype.Int4) error + UpdateBoardTitle(ctx context.Context, title string) error UpdateMemberProfileByID(ctx context.Context, arg UpdateMemberProfileByIDParams) error UpdateThread(ctx context.Context, arg UpdateThreadParams) error UpdateThreadPost(ctx context.Context, arg UpdateThreadPostParams) error diff --git a/queries.sql.go b/queries.sql.go index 0a0d92a..9b943e3 100644 --- a/queries.sql.go +++ b/queries.sql.go @@ -61,6 +61,40 @@ func (q *Queries) CreateThreadPost(ctx context.Context, arg CreateThreadPostPara return err } +const getBoardData = `-- name: GetBoardData :one +SELECT + id, + title, + total_members, + total_threads, + total_thread_posts, + edit_window +FROM board_data +` + +type GetBoardDataRow struct { + ID int32 + Title string + TotalMembers pgtype.Int4 + TotalThreads pgtype.Int4 + TotalThreadPosts pgtype.Int4 + EditWindow pgtype.Int4 +} + +func (q *Queries) GetBoardData(ctx context.Context) (GetBoardDataRow, error) { + row := q.db.QueryRow(ctx, getBoardData) + var i GetBoardDataRow + err := row.Scan( + &i.ID, + &i.Title, + &i.TotalMembers, + &i.TotalThreads, + &i.TotalThreadPosts, + &i.EditWindow, + ) + return i, err +} + const getMember = `-- name: GetMember :one SELECT m.email, @@ -482,6 +516,26 @@ func (q *Queries) ListThreads(ctx context.Context, arg ListThreadsParams) ([]Lis return items, nil } +const updateBoardEditWindow = `-- name: UpdateBoardEditWindow :exec +UPDATE board_data +SET edit_window=$1 +` + +func (q *Queries) UpdateBoardEditWindow(ctx context.Context, editWindow pgtype.Int4) error { + _, err := q.db.Exec(ctx, updateBoardEditWindow, editWindow) + return err +} + +const updateBoardTitle = `-- name: UpdateBoardTitle :exec +UPDATE board_data +SET title=$1 +` + +func (q *Queries) UpdateBoardTitle(ctx context.Context, title string) error { + _, err := q.db.Exec(ctx, updateBoardTitle, title) + return err +} + const updateMemberProfileByID = `-- name: UpdateMemberProfileByID :exec UPDATE member_profile SET photo_url = $2, diff --git a/server.go b/server.go index 3fe5971..73b4db0 100644 --- a/server.go +++ b/server.go @@ -162,6 +162,7 @@ func setupMux(dsvc *DiscussService) http.Handler { tailnetMux := http.NewServeMux() + tailnetMux.HandleFunc("POST /admin", CSRFMiddleware(dsvc.Admin)) tailnetMux.HandleFunc("POST /member/edit", CSRFMiddleware(dsvc.EditMemberProfile)) tailnetMux.HandleFunc("POST /thread/new", CSRFMiddleware(dsvc.CreateThread)) tailnetMux.HandleFunc("POST /thread/{tid}/edit", CSRFMiddleware(dsvc.EditThread)) @@ -169,6 +170,7 @@ func setupMux(dsvc *DiscussService) http.Handler { tailnetMux.HandleFunc("POST /thread/{tid}", CSRFMiddleware(dsvc.CreateThreadPost)) tailnetMux.HandleFunc("GET /{$}", CSRFMiddleware(dsvc.ListThreads)) + tailnetMux.HandleFunc("GET /admin", CSRFMiddleware(dsvc.Admin)) tailnetMux.HandleFunc("GET /member/{mid}", CSRFMiddleware(dsvc.ListMember)) tailnetMux.HandleFunc("GET /member/edit", CSRFMiddleware(dsvc.EditMemberProfile)) tailnetMux.HandleFunc("GET /thread/new", CSRFMiddleware(dsvc.NewThread)) @@ -306,6 +308,134 @@ type ThreadTemplateData struct { CanEdit pgtype.Bool } +func (s *DiscussService) Admin(w http.ResponseWriter, r *http.Request) { + s.logger.DebugContext(r.Context(), "entering Admin()") + defer s.logger.DebugContext(r.Context(), "exiting Admin()") + + switch r.Method { + case http.MethodGet: + s.AdminGET(w, r) + case http.MethodPost: + s.AdminPOST(w, r) + default: + s.renderError(w, http.StatusMethodNotAllowed) + } +} + +func (s *DiscussService) AdminGET(w http.ResponseWriter, r *http.Request) { + s.logger.DebugContext(r.Context(), "entering AdminGET()") + defer s.logger.DebugContext(r.Context(), "exiting AdminGET()") + + csrfToken := GetCSRFToken(r) + + user, err := GetUser(r) + if err != nil { + s.logger.ErrorContext(r.Context(), err.Error()) + s.renderError(w, http.StatusInternalServerError) + return + } + + if !user.IsAdmin { + s.logger.ErrorContext( + r.Context(), + "user is not admin", + slog.String("email", user.Email), + slog.Int64("user_id", user.ID), + slog.Bool("is_admin", user.IsAdmin), + ) + s.renderError(w, http.StatusForbidden) + return + } + + boardData, err := s.queries.GetBoardData(r.Context()) + if err != nil { + s.logger.ErrorContext(r.Context(), err.Error()) + s.renderError(w, http.StatusInternalServerError) + return + } + + s.renderTemplate(w, r, "admin.html", map[string]interface{}{ + "Title": GetBoardTitle(r), + "BoardData": boardData, + "Version": s.version, + "GitSha": s.gitSha, + "CSRFToken": csrfToken, + "User": user, + }) +} + +func (s *DiscussService) AdminPOST(w http.ResponseWriter, r *http.Request) { + s.logger.DebugContext(r.Context(), "entering AdminPOST()") + defer s.logger.DebugContext(r.Context(), "exiting AdminPOST()") + + if err := validateCSRFToken(r); err != nil { + s.logger.ErrorContext(r.Context(), "CSRF validation failed", slog.String("error", err.Error())) + http.Error(w, "CSRF validation failed", http.StatusForbidden) + return + } + + user, err := GetUser(r) + if err != nil { + s.logger.ErrorContext(r.Context(), err.Error()) + s.renderError(w, http.StatusInternalServerError) + return + } + + if !user.IsAdmin { + s.logger.ErrorContext( + r.Context(), + "user is not admin", + slog.String("email", user.Email), + slog.Int64("user_id", user.ID), + slog.Bool("is_admin", user.IsAdmin), + ) + s.renderError(w, http.StatusForbidden) + return + } + + if err := r.ParseForm(); err != nil { + s.logger.ErrorContext(r.Context(), err.Error()) + s.renderError(w, http.StatusBadRequest) + return + } + + if !r.Form.Has("board_title") { + s.logger.ErrorContext(r.Context(), "missing board_title") + s.renderError(w, http.StatusBadRequest) + return + } + + boardTitle := r.Form.Get("board_title") + + if err := s.queries.UpdateBoardTitle(r.Context(), boardTitle); err != nil { + s.logger.ErrorContext(r.Context(), err.Error()) + s.renderError(w, http.StatusInternalServerError) + return + } + + if !r.Form.Has("edit_window") { + s.logger.ErrorContext(r.Context(), "missing edit_window") + s.renderError(w, http.StatusBadRequest) + return + } + + editWindow, err := strconv.ParseInt(r.Form.Get("edit_window"), 10, 32) + if err != nil { + s.logger.ErrorContext(r.Context(), err.Error()) + s.renderError(w, http.StatusBadRequest) + return + } + + if err := s.queries.UpdateBoardEditWindow(r.Context(), pgtype.Int4{Int32: int32(editWindow), Valid: true}); err != nil { + s.logger.ErrorContext(r.Context(), err.Error()) + s.renderError(w, http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/", http.StatusSeeOther) + +} + // CreateThread handles the creation of a new thread. func (s *DiscussService) CreateThread(w http.ResponseWriter, r *http.Request) { if err := validateCSRFToken(r); err != nil { @@ -482,6 +612,7 @@ func (s *DiscussService) EditMemberProfile(w http.ResponseWriter, r *http.Reques "Version": s.version, "GitSha": s.gitSha, "CSRFToken": csrfToken, + "User": user, }) return } @@ -643,11 +774,12 @@ func (s *DiscussService) editThreadGET(w http.ResponseWriter, r *http.Request) { } s.renderTemplate(w, r, "edit-thread.html", map[string]interface{}{ - "Title": BOARD_TITLE, + "Title": GetBoardTitle(r), "Thread": thread, "Version": s.version, "CSRFToken": csrfToken, "GitSha": s.gitSha, + "User": user, }) return } @@ -772,6 +904,7 @@ func (s *DiscussService) editThreadPostGET(w http.ResponseWriter, r *http.Reques "Post": post, "ThreadID": threadID, "GitSha": s.gitSha, + "User": user, }) } @@ -842,10 +975,11 @@ func (s *DiscussService) ListThreads(w http.ResponseWriter, r *http.Request) { } s.renderTemplate(w, r, "index.html", map[string]interface{}{ - "Title": BOARD_TITLE, + "Title": GetBoardTitle(r), "Threads": threadsParsed, "Version": s.version, "GitSha": s.gitSha, + "User": user, }) } @@ -917,7 +1051,7 @@ func (s *DiscussService) ListThreadPosts(w http.ResponseWriter, r *http.Request) csrfToken := GetCSRFToken(r) s.renderTemplate(w, r, "thread.html", map[string]interface{}{ - "Title": BOARD_TITLE, + "Title": GetBoardTitle(r), "ThreadPosts": threadPosts, // nosemgrep "Subject": template.HTML(subject), @@ -925,6 +1059,7 @@ func (s *DiscussService) ListThreadPosts(w http.ResponseWriter, r *http.Request) "GitSha": s.gitSha, "Version": s.version, "CSRFToken": template.HTML(csrfToken), + "User": user, }) } @@ -987,7 +1122,7 @@ func (s *DiscussService) ListMember(w http.ResponseWriter, r *http.Request) { } s.renderTemplate(w, r, "member.html", map[string]interface{}{ - "Title": BOARD_TITLE, + "Title": GetBoardTitle(r), "Member": member, "CurrentUserEmail": user.Email, "Threads": memberThreadsParsed, @@ -1025,7 +1160,7 @@ func (s *DiscussService) NewThread(w http.ResponseWriter, r *http.Request) { s.renderTemplate(w, r, "newthread.html", map[string]interface{}{ "User": user, - "Title": BOARD_TITLE, + "Title": GetBoardTitle(r), "CSRFToken": csrfToken, "Version": s.version, "GitSha": s.gitSha, @@ -1044,6 +1179,16 @@ func UserMiddleware(s *DiscussService, next http.Handler) http.HandlerFunc { ctx := context.WithValue(r.Context(), "email", email) + boardData, err := s.queries.GetBoardData(r.Context()) + if err != nil { + s.logger.ErrorContext(r.Context(), err.Error()) + s.renderError(w, http.StatusInternalServerError) + return + } + + // Set the board title in the context + ctx = context.WithValue(ctx, "board_title", boardData.Title) + user, err := s.queries.CreateOrReturnID(ctx, email) if err != nil { s.logger.ErrorContext(ctx, err.Error()) @@ -1087,3 +1232,12 @@ func GetUser(r *http.Request) (User, error) { return u, nil } + +func GetBoardTitle(r *http.Request) string { + ctx := r.Context() + if title, ok := ctx.Value("board_title").(string); ok { + return title + } + + return BOARD_TITLE +} diff --git a/server_test.go b/server_test.go index bf380c7..f12b388 100644 --- a/server_test.go +++ b/server_test.go @@ -806,14 +806,14 @@ func TestListMember(t *testing.T) { url: "/member/1", mid: "1", setupMocks: func(mq *MockQueries) { - mq.getMemberFunc = func(ctx context.Context, id int64) (GetMemberRow, error) { + mq.GetMemberFunc = func(ctx context.Context, id int64) (GetMemberRow, error) { return GetMemberRow{ Email: "test@example.com", Location: pgtype.Text{String: "Test Location", Valid: true}, ID: id, }, nil } - mq.listMemberThreadsFunc = func(ctx context.Context, memberID int64) ([]ListMemberThreadsRow, error) { + mq.ListMemberThreadsFunc = func(ctx context.Context, memberID int64) ([]ListMemberThreadsRow, error) { return []ListMemberThreadsRow{ { ThreadID: 1, @@ -855,7 +855,7 @@ func TestListMember(t *testing.T) { url: "/member/1", mid: "1", setupMocks: func(mq *MockQueries) { - mq.getMemberFunc = func(ctx context.Context, id int64) (GetMemberRow, error) { + mq.GetMemberFunc = func(ctx context.Context, id int64) (GetMemberRow, error) { return GetMemberRow{}, errors.New("database error") } }, @@ -868,14 +868,14 @@ func TestListMember(t *testing.T) { url: "/member/1", mid: "1", setupMocks: func(mq *MockQueries) { - mq.getMemberFunc = func(ctx context.Context, id int64) (GetMemberRow, error) { + mq.GetMemberFunc = func(ctx context.Context, id int64) (GetMemberRow, error) { return GetMemberRow{ Email: "test@example.com", Location: pgtype.Text{String: "Test Location", Valid: true}, ID: id, }, nil } - mq.listMemberThreadsFunc = func(ctx context.Context, memberID int64) ([]ListMemberThreadsRow, error) { + mq.ListMemberThreadsFunc = func(ctx context.Context, memberID int64) ([]ListMemberThreadsRow, error) { return nil, errors.New("database error") } }, @@ -887,8 +887,8 @@ func TestListMember(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Reset mock queries for each test - mockQueries.getMemberFunc = nil - mockQueries.listMemberThreadsFunc = nil + mockQueries.GetMemberFunc = nil + mockQueries.ListMemberThreadsFunc = nil mockQueries.CreateOrReturnIDFunc = nil // Setup mocks @@ -1264,19 +1264,21 @@ func (m *MockTx) Rollback(ctx context.Context) error { } type MockQueries struct { - inTransaction bool - getMemberFunc func(ctx context.Context, id int64) (GetMemberRow, error) - listMemberThreadsFunc func(ctx context.Context, memberID int64) ([]ListMemberThreadsRow, error) - CreateOrReturnIDFunc func(ctx context.Context, email string) (CreateOrReturnIDRow, error) - ListThreadPostsFunc func(ctx context.Context, arg ListThreadPostsParams) ([]ListThreadPostsRow, error) - GetThreadSequenceIdFunc func(ctx context.Context) (int64, error) - CreateThreadFunc func(ctx context.Context, arg CreateThreadParams) error - UpdateThreadFunc func(ctx context.Context, arg UpdateThreadParams) error - UpdateThreadPostFunc func(ctx context.Context, arg UpdateThreadPostParams) error - GetThreadSubjectByIdFunc func(ctx context.Context, id int64) (string, error) - // arg contains the parameters required to fetch the thread for editing. - GetThreadForEditFunc func(ctx context.Context, arg GetThreadForEditParams) (GetThreadForEditRow, error) - GetThreadPostForEditFunc func(ctx context.Context, arg GetThreadPostForEditParams) (GetThreadPostForEditRow, error) + inTransaction bool + CreateOrReturnIDFunc func(ctx context.Context, email string) (CreateOrReturnIDRow, error) + CreateThreadFunc func(ctx context.Context, arg CreateThreadParams) error + GetBoardDataFunc func(ctx context.Context) (GetBoardDataRow, error) + GetMemberFunc func(ctx context.Context, id int64) (GetMemberRow, error) + GetThreadForEditFunc func(ctx context.Context, arg GetThreadForEditParams) (GetThreadForEditRow, error) + GetThreadPostForEditFunc func(ctx context.Context, arg GetThreadPostForEditParams) (GetThreadPostForEditRow, error) + GetThreadSequenceIdFunc func(ctx context.Context) (int64, error) + GetThreadSubjectByIdFunc func(ctx context.Context, id int64) (string, error) + ListMemberThreadsFunc func(ctx context.Context, memberID int64) ([]ListMemberThreadsRow, error) + ListThreadPostsFunc func(ctx context.Context, arg ListThreadPostsParams) ([]ListThreadPostsRow, error) + UpdateBoardEditWindowFunc func(ctx context.Context, arg pgtype.Int4) error + UpdateBoardTitleFunc func(ctx context.Context, arg string) error + UpdateThreadFunc func(ctx context.Context, arg UpdateThreadParams) error + UpdateThreadPostFunc func(ctx context.Context, arg UpdateThreadPostParams) error } func (m *MockQueries) CreateOrReturnID(ctx context.Context, pEmail string) (CreateOrReturnIDRow, error) { @@ -1300,9 +1302,24 @@ func (m *MockQueries) CreateThreadPost(ctx context.Context, arg CreateThreadPost return nil } +func (m *MockQueries) GetBoardData(ctx context.Context) (GetBoardDataRow, error) { + if m.GetBoardDataFunc != nil { + return m.GetBoardDataFunc(ctx) + } + + // Mock implementation + // Default to 900 second edit window + return GetBoardDataRow{ + EditWindow: pgtype.Int4{Int32: 900, Valid: true}, + Title: "Mock Board Title", + ID: 1, + }, nil + +} + func (m *MockQueries) GetMember(ctx context.Context, id int64) (GetMemberRow, error) { - if m.getMemberFunc != nil { - return m.getMemberFunc(ctx, id) + if m.GetMemberFunc != nil { + return m.GetMemberFunc(ctx, id) } // Mock implementation @@ -1367,8 +1384,8 @@ func (m *MockQueries) GetThreadPostForEdit(ctx context.Context, arg GetThreadPos } func (m *MockQueries) ListMemberThreads(ctx context.Context, memberID int64) ([]ListMemberThreadsRow, error) { - if m.listMemberThreadsFunc != nil { - return m.listMemberThreadsFunc(ctx, memberID) + if m.ListMemberThreadsFunc != nil { + return m.ListMemberThreadsFunc(ctx, memberID) } // Mock implementation @@ -1409,6 +1426,24 @@ func (m *MockQueries) ListThreads(ctx context.Context, arg ListThreadsParams) ([ }, nil } +func (m *MockQueries) UpdateBoardEditWindow(ctx context.Context, arg pgtype.Int4) error { + if m.UpdateBoardEditWindowFunc != nil { + return m.UpdateBoardEditWindowFunc(ctx, arg) + } + + // Mock implementation + return nil +} + +func (m *MockQueries) UpdateBoardTitle(ctx context.Context, arg string) error { + if m.UpdateBoardTitleFunc != nil { + return m.UpdateBoardTitleFunc(ctx, arg) + } + + // Mock implementation + return nil +} + func (m *MockQueries) UpdateMemberProfileByID(ctx context.Context, arg UpdateMemberProfileByIDParams) error { // Mock implementation return nil diff --git a/sqlc/queries.sql b/sqlc/queries.sql index cf69e9d..db572d4 100644 --- a/sqlc/queries.sql +++ b/sqlc/queries.sql @@ -142,6 +142,16 @@ ON WHERE tp.thread_id=$1 ORDER BY tp.date_posted ASC; +-- name: GetBoardData :one +SELECT + id, + title, + total_members, + total_threads, + total_thread_posts, + edit_window +FROM board_data; + -- name: GetThreadSubjectById :one SELECT subject FROM thread WHERE id=$1; @@ -164,6 +174,14 @@ FROM thread_post tp LEFT JOIN member m ON tp.member_id=m.id WHERE tp.id=$1 AND m.id=$2; +-- name: UpdateBoardTitle :exec +UPDATE board_data +SET title=$1; + +-- name: UpdateBoardEditWindow :exec +UPDATE board_data +SET edit_window=$1; + -- name: UpdateMemberProfileByID :exec UPDATE member_profile SET photo_url = $2, diff --git a/sqlc/schema.sql b/sqlc/schema.sql index 087ad46..158ba61 100644 --- a/sqlc/schema.sql +++ b/sqlc/schema.sql @@ -1,11 +1,17 @@ CREATE TABLE board_data ( - id serial PRIMARY KEY, -- id - name text NOT NULL CHECK(name <> ''), -- name of variable - value text NOT NULL CHECK(value <> ''), -- preference value - UNIQUE(name) + id serial PRIMARY KEY, -- id + title varchar NOT NULL CHECK(title <> ''), -- title of board + allow_editing boolean DEFAULT false, -- allow editing of posts + allow_deleting boolean DEFAULT false, -- allow deleting of posts + edit_window int DEFAULT 0, -- time in seconds to allow editing of posts + total_members int DEFAULT 0, -- total members + total_threads int DEFAULT 0, -- total threads + total_thread_posts int DEFAULT 0 -- total posts in threads ); +INSERT INTO board_data (title, edit_window) VALUES ('My Board', 900); + CREATE TABLE member ( cookie char(32), @@ -76,10 +82,10 @@ CREATE TABLE thread_member CREATE OR REPLACE FUNCTION member_sync() RETURNS trigger AS $$ BEGIN IF TG_OP = 'DELETE' THEN - UPDATE board_data SET value=(value::integer)-1 WHERE name='total_members'; + UPDATE board_data SET total_members=(total_members::integer)-1; RETURN OLD; ELSEIF TG_OP = 'INSERT' THEN - UPDATE board_data SET value=(value::integer)+1 WHERE name='total_members'; + UPDATE board_data SET total_members=(total_members::integer)+1; RETURN NEW; END IF; RETURN NULL; @@ -90,11 +96,11 @@ CREATE OR REPLACE FUNCTION thread_sync() RETURNS trigger AS $$ BEGIN IF TG_OP = 'DELETE' THEN UPDATE member SET total_threads=total_threads-1 WHERE id=OLD.member_id; - UPDATE board_data SET value=(value::integer)-1 WHERE name='total_threads'; + UPDATE board_data SET total_threads=(total_threads::integer)-1; RETURN OLD; ELSEIF TG_OP = 'INSERT' THEN UPDATE member SET total_threads=total_threads+1 WHERE id=NEW.member_id; - UPDATE board_data SET value=(value::integer)+1 WHERE name='total_threads'; + UPDATE board_data SET total_threads=(total_threads::integer)+1; RETURN NEW; END IF; RETURN NULL; @@ -105,7 +111,7 @@ CREATE OR REPLACE FUNCTION thread_post_sync() RETURNS trigger AS $$ BEGIN IF TG_OP = 'DELETE' THEN UPDATE member SET total_thread_posts=total_thread_posts-1, last_post=now() WHERE id=OLD.member_id; - UPDATE board_data SET value=(value::integer)-1 WHERE name='total_thread_posts'; + UPDATE board_data SET total_thread_posts=(total_thread_posts::integer)-1; IF (SELECT count(*) FROM thread_post WHERE thread_id=OLD.thread_id) > 1 THEN UPDATE thread @@ -128,7 +134,7 @@ BEGIN ELSEIF TG_OP = 'INSERT' THEN UPDATE member SET last_post=now() WHERE id=NEW.member_id; UPDATE member SET total_thread_posts=total_thread_posts+1 WHERE id=NEW.member_id; - UPDATE board_data SET value=(value::integer)+1 WHERE name='total_thread_posts'; + UPDATE board_data SET total_thread_posts=(total_thread_posts::integer)+1; UPDATE thread SET diff --git a/tmpl/admin.html b/tmpl/admin.html new file mode 100644 index 0000000..c5c1624 --- /dev/null +++ b/tmpl/admin.html @@ -0,0 +1,25 @@ +{{ template "header" . }} + +