Add forum post deletion
This commit is contained in:
parent
a2eacd6d00
commit
7f3c818a8f
27
src/db/db.go
27
src/db/db.go
|
@ -54,6 +54,11 @@ func typeIsQueryable(t reflect.Type) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// This interface should match both a direct pgx connection or a pgx transaction.
|
||||
type ConnOrTx interface {
|
||||
Query(ctx context.Context, sql string, args ...interface{}) (pgx.Rows, error)
|
||||
}
|
||||
|
||||
var connInfo = pgtype.NewConnInfo()
|
||||
|
||||
func NewConn() *pgx.Conn {
|
||||
|
@ -211,7 +216,7 @@ func followPathThroughStructs(structPtrVal reflect.Value, path []int) (reflect.V
|
|||
return val, field
|
||||
}
|
||||
|
||||
func Query(ctx context.Context, conn *pgxpool.Pool, destExample interface{}, query string, args ...interface{}) (*StructQueryIterator, error) {
|
||||
func Query(ctx context.Context, conn ConnOrTx, destExample interface{}, query string, args ...interface{}) (*StructQueryIterator, error) {
|
||||
destType := reflect.TypeOf(destExample)
|
||||
columnNames, fieldPaths, err := getColumnNamesAndPaths(destType, nil, "")
|
||||
if err != nil {
|
||||
|
@ -279,7 +284,7 @@ func getColumnNamesAndPaths(destType reflect.Type, pathSoFar []int, prefix strin
|
|||
|
||||
var ErrNoMatchingRows = errors.New("no matching rows")
|
||||
|
||||
func QueryOne(ctx context.Context, conn *pgxpool.Pool, destExample interface{}, query string, args ...interface{}) (interface{}, error) {
|
||||
func QueryOne(ctx context.Context, conn ConnOrTx, destExample interface{}, query string, args ...interface{}) (interface{}, error) {
|
||||
rows, err := Query(ctx, conn, destExample, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -294,7 +299,7 @@ func QueryOne(ctx context.Context, conn *pgxpool.Pool, destExample interface{},
|
|||
return result, nil
|
||||
}
|
||||
|
||||
func QueryScalar(ctx context.Context, conn *pgxpool.Pool, query string, args ...interface{}) (interface{}, error) {
|
||||
func QueryScalar(ctx context.Context, conn ConnOrTx, query string, args ...interface{}) (interface{}, error) {
|
||||
rows, err := conn.Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -317,7 +322,7 @@ func QueryScalar(ctx context.Context, conn *pgxpool.Pool, query string, args ...
|
|||
return nil, ErrNoMatchingRows
|
||||
}
|
||||
|
||||
func QueryInt(ctx context.Context, conn *pgxpool.Pool, query string, args ...interface{}) (int, error) {
|
||||
func QueryInt(ctx context.Context, conn ConnOrTx, query string, args ...interface{}) (int, error) {
|
||||
result, err := QueryScalar(ctx, conn, query, args...)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
|
@ -334,3 +339,17 @@ func QueryInt(ctx context.Context, conn *pgxpool.Pool, query string, args ...int
|
|||
return 0, oops.New(nil, "QueryInt got a non-int result: %v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func QueryBool(ctx context.Context, conn ConnOrTx, query string, args ...interface{}) (bool, error) {
|
||||
result, err := QueryScalar(ctx, conn, query, args...)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
switch r := result.(type) {
|
||||
case bool:
|
||||
return r, nil
|
||||
default:
|
||||
return false, oops.New(nil, "QueryBool got a non-bool result: %v", result)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,51 +75,7 @@
|
|||
|
||||
{{ with .PostReplyingTo }}
|
||||
<h4 class="mt3">The post you're replying to:</h4>
|
||||
<div class="bg--dim pa3 br3">
|
||||
<div class="w-100 flex items-center">
|
||||
{{ if .Author }}
|
||||
<div class="w-20 mw3 w3">
|
||||
<!-- Mobile avatar -->
|
||||
<div class="aspect-ratio--1x1 contain bg-center" style="background-image:url('{{ .Author.AvatarUrl }}');"></div>
|
||||
</div>
|
||||
<div class="pl3 flex flex-column">
|
||||
<div>
|
||||
<a class="username" href="{{ .Author.ProfileUrl }}" target="_blank">{{ .Author.Username }}</a> {{/* TODO: Text scale stuff? Seems unnecessary. */}}
|
||||
<!-- Mobile badges -->
|
||||
<div class="di ph1">
|
||||
{{ if .Author.IsStaff }}
|
||||
<div class="badge staff"></div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ if and .Author.Name (ne .Author.Name .Author.Username) }}
|
||||
<div class="c--dim f7"> {{ .Author.Name }} </div>
|
||||
{{ end }}
|
||||
<div class="c--dim f7">
|
||||
{{ timehtml (relativedate .PostDate) .PostDate }}
|
||||
{{ if .Editor }}
|
||||
<span class="pl3">
|
||||
Edited by
|
||||
<a class="name" href="{{ .Editor.ProfileUrl }}" target="_blank">{{ coalesce .Editor.Name .Editor.Username }}</a>
|
||||
on {{ timehtml (absolutedate .EditDate) .EditDate }}
|
||||
{{ with .EditReason }}
|
||||
Reason: {{ . }}
|
||||
{{ end }}
|
||||
</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ else }}
|
||||
<div class="username">Deleted member</div>
|
||||
<div class="avatar" style="background-image:url('{{ .Author.AvatarUrl }}');"></div>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="w-100 pt3">
|
||||
<div class="contents overflow-x-auto">
|
||||
{{ .Content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ template "forum_post_standalone.html" . }}
|
||||
{{ end }}
|
||||
|
||||
{{/*
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
{{ template "base.html" . }}
|
||||
|
||||
{{ define "content" }}
|
||||
<div class="mw7 margin-center">
|
||||
<h3 class="mb3">Are you sure you want to delete this post?</h3>
|
||||
{{ template "forum_post_standalone.html" .Post }}
|
||||
<form action="{{ .SubmitUrl }}" method="POST" class="pv3 flex justify-end">
|
||||
<input type="submit" value="Delete Post">
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
|
@ -0,0 +1,40 @@
|
|||
<div class="bg--dim pa3 br3 tl">
|
||||
<div class="w-100 flex items-center">
|
||||
<div class="w-20 mw3 w3">
|
||||
<!-- Mobile avatar -->
|
||||
<div class="aspect-ratio--1x1 contain bg-center" style="background-image:url('{{ .Author.AvatarUrl }}');"></div>
|
||||
</div>
|
||||
<div class="pl3 flex flex-column">
|
||||
<div>
|
||||
<a class="username" href="{{ .Author.ProfileUrl }}" target="_blank">{{ .Author.Username }}</a> {{/* TODO: Text scale stuff? Seems unnecessary. */}}
|
||||
<!-- Mobile badges -->
|
||||
<div class="di ph1">
|
||||
{{ if .Author.IsStaff }}
|
||||
<div class="badge staff"></div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ if and .Author.Name (ne .Author.Name .Author.Username) }}
|
||||
<div class="c--dim f7"> {{ .Author.Name }} </div>
|
||||
{{ end }}
|
||||
<div class="c--dim f7">
|
||||
{{ timehtml (relativedate .PostDate) .PostDate }}
|
||||
{{ if .Editor }}
|
||||
<span class="pl3">
|
||||
Edited by
|
||||
<a class="name" href="{{ .Editor.ProfileUrl }}" target="_blank">{{ coalesce .Editor.Name .Editor.Username }}</a>
|
||||
on {{ timehtml (absolutedate .EditDate) .EditDate }}
|
||||
{{ with .EditReason }}
|
||||
Reason: {{ . }}
|
||||
{{ end }}
|
||||
</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-100 pt3">
|
||||
<div class="contents overflow-x-auto">
|
||||
{{ .Content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -19,6 +19,7 @@ import (
|
|||
"git.handmade.network/hmn/hmn/src/templates"
|
||||
"git.handmade.network/hmn/hmn/src/utils"
|
||||
"github.com/jackc/pgx/v4"
|
||||
"github.com/jackc/pgx/v4/pgxpool"
|
||||
)
|
||||
|
||||
type forumCategoryData struct {
|
||||
|
@ -821,8 +822,10 @@ func ForumPostEdit(c *RequestContext) ResponseData {
|
|||
result := postQueryResult.(*postQuery)
|
||||
|
||||
// Ensure that the user is permitted to edit the post
|
||||
isPostAuthor := result.Author != nil && result.Author.ID == c.CurrentUser.ID
|
||||
if !(isPostAuthor || c.CurrentUser.IsStaff) {
|
||||
canEdit, err := canEditPost(c.Context(), c.Conn, requestedPostId, *c.CurrentUser)
|
||||
if err != nil {
|
||||
return ErrorResponse(http.StatusInternalServerError, err)
|
||||
} else if !canEdit {
|
||||
return FourOhFour(c)
|
||||
}
|
||||
|
||||
|
@ -875,33 +878,10 @@ func ForumPostEditSubmit(c *RequestContext) ResponseData {
|
|||
}
|
||||
|
||||
// Ensure that the user is permitted to edit the post
|
||||
type postResult struct {
|
||||
AuthorID *int `db:"author.id"`
|
||||
}
|
||||
iresult, err := db.QueryOne(c.Context(), c.Conn, postResult{},
|
||||
`
|
||||
SELECT $columns
|
||||
FROM
|
||||
handmade_post AS post
|
||||
LEFT JOIN auth_user AS author ON post.author_id = author.id
|
||||
WHERE
|
||||
post.category_id = $1
|
||||
AND post.thread_id = $2
|
||||
AND post.id = $3
|
||||
AND NOT post.deleted
|
||||
ORDER BY postdate
|
||||
`,
|
||||
currentCatId,
|
||||
threadId,
|
||||
postId,
|
||||
)
|
||||
if err != nil && !errors.Is(err, db.ErrNoMatchingRows) {
|
||||
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to get author of post to delete"))
|
||||
}
|
||||
result := iresult.(*postResult)
|
||||
|
||||
isPostAuthor := result.AuthorID != nil && *result.AuthorID == c.CurrentUser.ID
|
||||
if !(isPostAuthor || c.CurrentUser.IsStaff) {
|
||||
canEdit, err := canEditPost(c.Context(), c.Conn, postId, *c.CurrentUser)
|
||||
if err != nil {
|
||||
return ErrorResponse(http.StatusInternalServerError, err)
|
||||
} else if !canEdit {
|
||||
return FourOhFour(c)
|
||||
}
|
||||
|
||||
|
@ -920,6 +900,210 @@ func ForumPostEditSubmit(c *RequestContext) ResponseData {
|
|||
return c.Redirect(postUrl, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func ForumPostDelete(c *RequestContext) ResponseData {
|
||||
// TODO(compression): This logic for fetching posts by thread id / post id is gonna
|
||||
// show up in a lot of places. It's used multiple times for forums, and also for blogs.
|
||||
// Consider compressing this later.
|
||||
c.Perf.StartBlock("SQL", "Fetch category tree")
|
||||
categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn)
|
||||
lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree)
|
||||
c.Perf.EndBlock()
|
||||
|
||||
currentCatId, valid := validateSubforums(lineageBuilder, c.CurrentProject, c.PathParams["cats"])
|
||||
if !valid {
|
||||
return FourOhFour(c)
|
||||
}
|
||||
|
||||
requestedThreadId, err := strconv.Atoi(c.PathParams["threadid"])
|
||||
if err != nil {
|
||||
return FourOhFour(c)
|
||||
}
|
||||
|
||||
requestedPostId, err := strconv.Atoi(c.PathParams["postid"])
|
||||
if err != nil {
|
||||
return FourOhFour(c)
|
||||
}
|
||||
|
||||
// Ensure that the user is allowed to delete this post
|
||||
canEdit, err := canEditPost(c.Context(), c.Conn, requestedPostId, *c.CurrentUser)
|
||||
if err != nil {
|
||||
return ErrorResponse(http.StatusInternalServerError, err)
|
||||
} else if !canEdit {
|
||||
return FourOhFour(c)
|
||||
}
|
||||
|
||||
c.Perf.StartBlock("SQL", "Fetch post to delete")
|
||||
type postQuery struct {
|
||||
Thread models.Thread `db:"thread"`
|
||||
Post models.Post `db:"post"`
|
||||
CurrentVersion models.PostVersion `db:"ver"`
|
||||
Author *models.User `db:"author"`
|
||||
Editor *models.User `db:"editor"`
|
||||
}
|
||||
postQueryResult, err := db.QueryOne(c.Context(), c.Conn, postQuery{},
|
||||
`
|
||||
SELECT $columns
|
||||
FROM
|
||||
handmade_thread AS thread
|
||||
JOIN handmade_post AS post ON post.thread_id = thread.id
|
||||
JOIN handmade_postversion AS ver ON post.current_id = ver.id
|
||||
LEFT JOIN auth_user AS author ON post.author_id = author.id
|
||||
LEFT JOIN auth_user AS editor ON ver.editor_id = editor.id
|
||||
WHERE
|
||||
post.category_id = $1
|
||||
AND post.thread_id = $2
|
||||
AND post.id = $3
|
||||
AND NOT post.deleted
|
||||
ORDER BY postdate
|
||||
`,
|
||||
currentCatId,
|
||||
requestedThreadId,
|
||||
requestedPostId,
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrNoMatchingRows) {
|
||||
return FourOhFour(c)
|
||||
} else {
|
||||
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch post to delete"))
|
||||
}
|
||||
}
|
||||
result := postQueryResult.(*postQuery)
|
||||
|
||||
baseData := getBaseData(c)
|
||||
baseData.Title = fmt.Sprintf("Deleting post in \"%s\" | %s", result.Thread.Title, *categoryTree[currentCatId].Name)
|
||||
baseData.MathjaxEnabled = true
|
||||
// TODO(ben): Set breadcrumbs
|
||||
|
||||
templatePost := templates.PostToTemplate(&result.Post, result.Author, c.Theme)
|
||||
templatePost.AddContentVersion(result.CurrentVersion, result.Editor)
|
||||
|
||||
type forumPostDeleteData struct {
|
||||
templates.BaseData
|
||||
Post templates.Post
|
||||
SubmitUrl string
|
||||
}
|
||||
|
||||
var res ResponseData
|
||||
res.MustWriteTemplate("forum_post_delete.html", forumPostDeleteData{
|
||||
BaseData: baseData,
|
||||
SubmitUrl: hmnurl.BuildForumPostDelete(c.CurrentProject.Slug, lineageBuilder.GetSubforumLineageSlugs(currentCatId), requestedThreadId, requestedPostId),
|
||||
Post: templatePost,
|
||||
}, c.Perf)
|
||||
return res
|
||||
}
|
||||
|
||||
func ForumPostDeleteSubmit(c *RequestContext) ResponseData {
|
||||
// TODO(compression): This logic for fetching posts by thread id / post id is gonna
|
||||
// show up in a lot of places. It's used multiple times for forums, and also for blogs.
|
||||
// Consider compressing this later.
|
||||
c.Perf.StartBlock("SQL", "Fetch category tree")
|
||||
categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn)
|
||||
lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree)
|
||||
c.Perf.EndBlock()
|
||||
|
||||
currentCatId, valid := validateSubforums(lineageBuilder, c.CurrentProject, c.PathParams["cats"])
|
||||
if !valid {
|
||||
return FourOhFour(c)
|
||||
}
|
||||
|
||||
threadId, err := strconv.Atoi(c.PathParams["threadid"])
|
||||
if err != nil {
|
||||
return FourOhFour(c)
|
||||
}
|
||||
|
||||
postId, err := strconv.Atoi(c.PathParams["postid"])
|
||||
if err != nil {
|
||||
return FourOhFour(c)
|
||||
}
|
||||
|
||||
// Ensure that the user is allowed to delete this post
|
||||
canEdit, err := canEditPost(c.Context(), c.Conn, postId, *c.CurrentUser)
|
||||
if err != nil {
|
||||
return ErrorResponse(http.StatusInternalServerError, err)
|
||||
} else if !canEdit {
|
||||
return FourOhFour(c)
|
||||
}
|
||||
|
||||
tx, err := c.Conn.Begin(c.Context())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer tx.Rollback(c.Context())
|
||||
|
||||
isFirstPost, err := db.QueryBool(c.Context(), tx,
|
||||
`
|
||||
SELECT thread.first_id = $1
|
||||
FROM
|
||||
handmade_thread AS thread
|
||||
WHERE
|
||||
thread.id = $2
|
||||
`,
|
||||
postId,
|
||||
threadId,
|
||||
)
|
||||
if err != nil {
|
||||
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to check if post was the first post in the thread"))
|
||||
}
|
||||
|
||||
if isFirstPost {
|
||||
// Just delete the whole thread and all its posts.
|
||||
_, err = tx.Exec(c.Context(),
|
||||
`
|
||||
UPDATE handmade_thread
|
||||
SET deleted = TRUE
|
||||
WHERE id = $1
|
||||
`,
|
||||
threadId,
|
||||
)
|
||||
_, err = tx.Exec(c.Context(),
|
||||
`
|
||||
UPDATE handmade_post
|
||||
SET deleted = TRUE
|
||||
WHERE thread_id = $1
|
||||
`,
|
||||
threadId,
|
||||
)
|
||||
|
||||
err = tx.Commit(c.Context())
|
||||
if err != nil {
|
||||
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete thread and posts when deleting the first post"))
|
||||
}
|
||||
|
||||
forumUrl := hmnurl.BuildForumCategory(c.CurrentProject.Slug, lineageBuilder.GetSubforumLineageSlugs(currentCatId), 1)
|
||||
return c.Redirect(forumUrl, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
_, err = tx.Exec(c.Context(),
|
||||
`
|
||||
UPDATE handmade_post
|
||||
SET deleted = TRUE
|
||||
WHERE
|
||||
id = $1
|
||||
`,
|
||||
postId,
|
||||
)
|
||||
if err != nil {
|
||||
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to mark forum post as deleted"))
|
||||
}
|
||||
|
||||
err = fixThreadPostIds(c.Context(), tx, threadId)
|
||||
if err != nil {
|
||||
if errors.Is(err, errThreadEmpty) {
|
||||
panic("it shouldn't be possible to delete the last remaining post in a thread, without it also being the first post in the thread and thus resulting in the whole thread getting deleted earlier")
|
||||
} else {
|
||||
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fix up thread post ids"))
|
||||
}
|
||||
}
|
||||
|
||||
err = tx.Commit(c.Context())
|
||||
if err != nil {
|
||||
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete post"))
|
||||
}
|
||||
|
||||
threadUrl := hmnurl.BuildForumThread(c.CurrentProject.Slug, lineageBuilder.GetSubforumLineageSlugs(currentCatId), threadId, "", 1) // TODO: Go to the last page of the thread? Or the post before the post we just deleted?
|
||||
return c.Redirect(threadUrl, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func createNewForumPostAndVersion(ctx context.Context, tx pgx.Tx, catId, threadId, userId, projectId int, unparsedContent string, ipString string, replyId *int) (postId, versionId int) {
|
||||
// Create post
|
||||
err := tx.QueryRow(ctx,
|
||||
|
@ -1028,3 +1212,92 @@ func validateSubforums(lineageBuilder *models.CategoryLineageBuilder, project *m
|
|||
}
|
||||
return subforumCatId, valid
|
||||
}
|
||||
|
||||
func canEditPost(ctx context.Context, conn *pgxpool.Pool, postId int, currentUser models.User) (bool, error) {
|
||||
if currentUser.IsStaff {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
type postResult struct {
|
||||
AuthorID *int `db:"author.id"`
|
||||
}
|
||||
iresult, err := db.QueryOne(ctx, conn, postResult{},
|
||||
`
|
||||
SELECT $columns
|
||||
FROM
|
||||
handmade_post AS post
|
||||
LEFT JOIN auth_user AS author ON post.author_id = author.id
|
||||
WHERE
|
||||
post.id = $1
|
||||
AND NOT post.deleted
|
||||
ORDER BY postdate
|
||||
`,
|
||||
postId,
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrNoMatchingRows) {
|
||||
return false, nil
|
||||
} else {
|
||||
return false, oops.New(err, "failed to get author of post when checking permissions")
|
||||
}
|
||||
}
|
||||
result := iresult.(*postResult)
|
||||
|
||||
return result.AuthorID != nil && *result.AuthorID == currentUser.ID, nil
|
||||
}
|
||||
|
||||
var errThreadEmpty = errors.New("thread contained no non-deleted posts")
|
||||
|
||||
/*
|
||||
Ensures that the first_id and last_id on the thread are still good.
|
||||
|
||||
Returns errThreadEmpty if the thread contains no visible posts any more.
|
||||
You should probably mark the thread as deleted in this case.
|
||||
*/
|
||||
func fixThreadPostIds(ctx context.Context, tx pgx.Tx, threadId int) error {
|
||||
postsIter, err := db.Query(ctx, tx, models.Post{},
|
||||
`
|
||||
SELECT $columns
|
||||
FROM handmade_post
|
||||
WHERE
|
||||
thread_id = $1
|
||||
AND NOT deleted
|
||||
`,
|
||||
threadId,
|
||||
)
|
||||
if err != nil {
|
||||
return oops.New(err, "failed to fetch posts when fixing up thread")
|
||||
}
|
||||
|
||||
var firstPost, lastPost *models.Post
|
||||
for _, ipost := range postsIter.ToSlice() {
|
||||
post := ipost.(*models.Post)
|
||||
|
||||
if firstPost == nil || post.PostDate.Before(firstPost.PostDate) {
|
||||
firstPost = post
|
||||
}
|
||||
if lastPost == nil || post.PostDate.After(lastPost.PostDate) {
|
||||
lastPost = post
|
||||
}
|
||||
}
|
||||
|
||||
if firstPost == nil || lastPost == nil {
|
||||
return errThreadEmpty
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx,
|
||||
`
|
||||
UPDATE handmade_thread
|
||||
SET first_id = $1, last_id = $2
|
||||
WHERE id = $3
|
||||
`,
|
||||
firstPost.ID,
|
||||
lastPost.ID,
|
||||
threadId,
|
||||
)
|
||||
if err != nil {
|
||||
return oops.New(err, "failed to update thread first/last ids")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -162,7 +162,8 @@ func NewWebsiteRoutes(conn *pgxpool.Pool, perfCollector *perf.PerfCollector) htt
|
|||
mainRoutes.POST(hmnurl.RegexForumPostReply, authMiddleware(ForumPostReplySubmit))
|
||||
mainRoutes.GET(hmnurl.RegexForumPostEdit, authMiddleware(ForumPostEdit))
|
||||
mainRoutes.POST(hmnurl.RegexForumPostEdit, authMiddleware(ForumPostEditSubmit))
|
||||
// mainRoutes.GET(hmnurl.RegexForumPostDelete, authMiddleware(ForumPostDelete))
|
||||
mainRoutes.GET(hmnurl.RegexForumPostDelete, authMiddleware(ForumPostDelete))
|
||||
mainRoutes.POST(hmnurl.RegexForumPostDelete, authMiddleware(ForumPostDeleteSubmit))
|
||||
|
||||
mainRoutes.GET(hmnurl.RegexProjectCSS, ProjectCSS)
|
||||
|
||||
|
|
Loading…
Reference in New Issue