User approval admin page and RSS
This commit is contained in:
parent
cad1c397c1
commit
f8985e6ee3
|
@ -88,6 +88,21 @@ func DeleteSession(ctx context.Context, conn *pgxpool.Pool, id string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DeleteSessionForUser(ctx context.Context, conn *pgxpool.Pool, username string) error {
|
||||||
|
_, err := conn.Exec(ctx,
|
||||||
|
`
|
||||||
|
DELETE FROM sessions
|
||||||
|
WHERE LOWER(username) = LOWER($1)
|
||||||
|
`,
|
||||||
|
username,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return oops.New(err, "failed to delete session")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewSessionCookie(session *models.Session) *http.Cookie {
|
func NewSessionCookie(session *models.Session) *http.Cookie {
|
||||||
return &http.Cookie{
|
return &http.Cookie{
|
||||||
Name: SessionCookieName,
|
Name: SessionCookieName,
|
||||||
|
|
|
@ -25,6 +25,10 @@ var Config = HMNConfig{
|
||||||
CookieDomain: ".handmade.local",
|
CookieDomain: ".handmade.local",
|
||||||
CookieSecure: false,
|
CookieSecure: false,
|
||||||
},
|
},
|
||||||
|
Admin: AdminConfig {
|
||||||
|
AtomUsername: "admin",
|
||||||
|
AtomPassword: "password",
|
||||||
|
},
|
||||||
Email: EmailConfig{
|
Email: EmailConfig{
|
||||||
ServerAddress: "smtp.example.com",
|
ServerAddress: "smtp.example.com",
|
||||||
ServerPort: 587,
|
ServerPort: 587,
|
||||||
|
|
|
@ -23,6 +23,7 @@ type HMNConfig struct {
|
||||||
LogLevel zerolog.Level
|
LogLevel zerolog.Level
|
||||||
Postgres PostgresConfig
|
Postgres PostgresConfig
|
||||||
Auth AuthConfig
|
Auth AuthConfig
|
||||||
|
Admin AdminConfig
|
||||||
Email EmailConfig
|
Email EmailConfig
|
||||||
DigitalOcean DigitalOceanConfig
|
DigitalOcean DigitalOceanConfig
|
||||||
Discord DiscordConfig
|
Discord DiscordConfig
|
||||||
|
@ -82,6 +83,11 @@ type EpisodeGuide struct {
|
||||||
Projects map[string]string // NOTE(asaf): Maps from slugs to default episode guide topic
|
Projects map[string]string // NOTE(asaf): Maps from slugs to default episode guide topic
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AdminConfig struct {
|
||||||
|
AtomUsername string
|
||||||
|
AtomPassword string
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
if Config.EpisodeGuide.Projects == nil {
|
if Config.EpisodeGuide.Projects == nil {
|
||||||
Config.EpisodeGuide.Projects = make(map[string]string)
|
Config.EpisodeGuide.Projects = make(map[string]string)
|
||||||
|
|
|
@ -102,6 +102,11 @@ func TestUserSettings(t *testing.T) {
|
||||||
AssertRegexMatch(t, BuildUserSettings("test"), RegexUserSettings, nil)
|
AssertRegexMatch(t, BuildUserSettings("test"), RegexUserSettings, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAdmin(t *testing.T) {
|
||||||
|
AssertRegexMatch(t, BuildAdminAtomFeed(), RegexAdminAtomFeed, nil)
|
||||||
|
AssertRegexMatch(t, BuildAdminApprovalQueue(), RegexAdminApprovalQueue, nil)
|
||||||
|
}
|
||||||
|
|
||||||
func TestSnippet(t *testing.T) {
|
func TestSnippet(t *testing.T) {
|
||||||
AssertRegexMatch(t, BuildSnippet(15), RegexSnippet, map[string]string{"snippetid": "15"})
|
AssertRegexMatch(t, BuildSnippet(15), RegexSnippet, map[string]string{"snippetid": "15"})
|
||||||
}
|
}
|
||||||
|
|
|
@ -200,6 +200,24 @@ func BuildUserSettings(section string) string {
|
||||||
return ProjectUrlWithFragment("/settings", nil, "", section)
|
return ProjectUrlWithFragment("/settings", nil, "", section)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Admin
|
||||||
|
*/
|
||||||
|
|
||||||
|
var RegexAdminAtomFeed = regexp.MustCompile(`^/admin/atom$`)
|
||||||
|
|
||||||
|
func BuildAdminAtomFeed() string {
|
||||||
|
defer CatchPanic()
|
||||||
|
return Url("/admin/atom", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexAdminApprovalQueue = regexp.MustCompile(`^/admin/approvals$`)
|
||||||
|
|
||||||
|
func BuildAdminApprovalQueue() string {
|
||||||
|
defer CatchPanic()
|
||||||
|
return Url("/admin/approvals", nil)
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Snippets
|
* Snippets
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
{{ template "base.html" . }}
|
||||||
|
|
||||||
|
{{ define "content" }}
|
||||||
|
<div class="content-block">
|
||||||
|
{{ range .Posts }}
|
||||||
|
<div class="post background-even pa3">
|
||||||
|
<div class="fl w-100 w-25-l pt3 pa3-l flex tc-l">
|
||||||
|
<div class="fl w-20 mw3 dn-l w3">
|
||||||
|
<!-- Mobile avatar -->
|
||||||
|
<div class="aspect-ratio--1x1 contain bg-center" style="background-image:url('{{ .Author.AvatarUrl }}');"></div>
|
||||||
|
</div>
|
||||||
|
<div class="w-100-l pl3 pl0-l flex flex-column items-center-l">
|
||||||
|
<div>
|
||||||
|
<a class="username" href="{{ .Author.ProfileUrl }}" target="_blank">{{ .Author.Username }}</a>
|
||||||
|
</div>
|
||||||
|
{{ if and .Author.Name (ne .Author.Name .Author.Username) }}
|
||||||
|
<div class="c--dim f7"> {{ .Author.Name }} </div>
|
||||||
|
{{ end }}
|
||||||
|
<!-- Large avatar -->
|
||||||
|
<div class="dn db-l w-60 pv2">
|
||||||
|
<div class="aspect-ratio--1x1 contain bg-center" style="background-image:url('{{ .Author.AvatarUrl }}');"></div>
|
||||||
|
</div>
|
||||||
|
<div class="i c--dim f7">
|
||||||
|
{{ if .Author.Blurb }}
|
||||||
|
{{ .Author.Blurb }} {{/* TODO: Linebreaks? */}}
|
||||||
|
{{ else if .Author.Bio }}
|
||||||
|
{{ .Author.Bio }}
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fl w-100 w-75-l pv3 pa3-l">
|
||||||
|
<div class="w-100 flex-l flex-row-reverse-l">
|
||||||
|
<div class="inline-flex flex-row-reverse pl3-l pb3 items-center">
|
||||||
|
<div class="postid">
|
||||||
|
<a name="{{ .ID }}" href="{{ .Url }}">#{{ .ID }}</a>
|
||||||
|
</div>
|
||||||
|
<div class="flex pr3">
|
||||||
|
<form method="POST" class="mr4" target="{{ $.SubmitUrl }}">
|
||||||
|
{{ csrftoken $.Session }}
|
||||||
|
<input type="hidden" name="action" value="{{ $.ApprovalAction }}" />
|
||||||
|
<input type="hidden" name="user_id" value="{{ .Author.ID }}" />
|
||||||
|
<input type="submit" value="Approve User" />
|
||||||
|
</form>
|
||||||
|
<form method="POST" target="{{ $.SubmitUrl }}">
|
||||||
|
{{ csrftoken $.Session }}
|
||||||
|
<input type="hidden" name="action" value="{{ $.SpammerAction }}" />
|
||||||
|
<input type="hidden" name="user_id" value="{{ .Author.ID }}" />
|
||||||
|
<input type="submit" value="Mark as spammer" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-100 pb3">
|
||||||
|
<div class="b" role="heading" aria-level="2">{{ .Title }}</div>
|
||||||
|
{{ timehtml (relativedate .PostDate) .PostDate }}
|
||||||
|
{{ if and $.User.IsStaff .IP }}
|
||||||
|
<span>[{{ .IP }}]</span>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="post-content overflow-x-auto">
|
||||||
|
{{ .Content }}
|
||||||
|
</div>
|
||||||
|
{{/* {% if post.author.signature|length %}
|
||||||
|
<div class="signature"><hr />
|
||||||
|
{{ post.author.signature|bbdecode|safe }}
|
||||||
|
</div>
|
||||||
|
{% endif %} */}}
|
||||||
|
</div>
|
||||||
|
<div class="cb"></div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
{{ noescape "<?xml version=\"1.0\" encoding=\"utf-8\"?>" }}
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
|
<title type="text">{{ .Title }}</title>
|
||||||
|
<subtitle type="html">{{ .Subtitle }}</subtitle>
|
||||||
|
<link href="{{ .HomepageUrl }}"/>
|
||||||
|
<link rel="self" type="application/atom+xml" href="{{ .AtomFeedUrl }}"/>
|
||||||
|
<link rel="alternate" type="text/html" hreflang="en" href="{{ .FeedUrl }}"/>
|
||||||
|
<rights type="html">{{ .CopyrightStatement }}</rights>
|
||||||
|
<generator uri="{{ .HomepageUrl }}" version="{{ .SiteVersion }}">Handmade Network site engine v{{ .SiteVersion }}</generator>
|
||||||
|
<updated>{{ rfc3339 .Updated }}</updated>
|
||||||
|
<id>{{ .FeedID }}</id>
|
||||||
|
{{ if .Posts }}
|
||||||
|
{{ range .Posts }}
|
||||||
|
<entry>
|
||||||
|
<title>{{ if .PostTypePrefix }}{{ .PostTypePrefix }}: {{ end }}{{ .Title }}</title>
|
||||||
|
<link rel="alternate" type="text/html" href="{{ .Url }}" />
|
||||||
|
<id>{{ .UUID }}</id>
|
||||||
|
<published>{{ rfc3339 .Date }}</published>
|
||||||
|
<updated>{{ rfc3339 .LastEditDate }}</updated>
|
||||||
|
<author>
|
||||||
|
<name>{{ .User.Name }}</name>
|
||||||
|
<uri>{{ .User.ProfileUrl }}</uri>
|
||||||
|
</author>
|
||||||
|
<summary type="html">{{ .Preview }}</summary>
|
||||||
|
</entry>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
</feed>
|
|
@ -0,0 +1,286 @@
|
||||||
|
package website
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.handmade.network/hmn/hmn/src/auth"
|
||||||
|
"git.handmade.network/hmn/hmn/src/config"
|
||||||
|
"git.handmade.network/hmn/hmn/src/db"
|
||||||
|
"git.handmade.network/hmn/hmn/src/hmnurl"
|
||||||
|
"git.handmade.network/hmn/hmn/src/models"
|
||||||
|
"git.handmade.network/hmn/hmn/src/oops"
|
||||||
|
"git.handmade.network/hmn/hmn/src/templates"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jackc/pgx/v4/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AdminAtomFeedData struct {
|
||||||
|
Title string
|
||||||
|
Subtitle string
|
||||||
|
|
||||||
|
HomepageUrl string
|
||||||
|
AtomFeedUrl string
|
||||||
|
FeedUrl string
|
||||||
|
|
||||||
|
CopyrightStatement string
|
||||||
|
SiteVersion string
|
||||||
|
Updated time.Time
|
||||||
|
FeedID string
|
||||||
|
|
||||||
|
Posts []templates.PostListItem
|
||||||
|
}
|
||||||
|
|
||||||
|
func AdminAtomFeed(c *RequestContext) ResponseData {
|
||||||
|
creds := fmt.Sprintf("%s:%s", config.Config.Admin.AtomUsername, config.Config.Admin.AtomPassword)
|
||||||
|
expectedAuth := fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(creds)))
|
||||||
|
auth, hasAuth := c.Req.Header["Authorization"]
|
||||||
|
if !hasAuth || auth[0] != expectedAuth {
|
||||||
|
return FourOhFour(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
feedData := AdminAtomFeedData{
|
||||||
|
HomepageUrl: hmnurl.BuildHomepage(),
|
||||||
|
CopyrightStatement: fmt.Sprintf("Copyright (C) 2014-%d Handmade.Network and its contributors", time.Now().Year()),
|
||||||
|
SiteVersion: "2.0",
|
||||||
|
Title: "Admin feed",
|
||||||
|
Subtitle: "Unapproved user posts",
|
||||||
|
FeedID: uuid.NewSHA1(uuid.NameSpaceURL, []byte(hmnurl.BuildAdminAtomFeed())).URN(),
|
||||||
|
AtomFeedUrl: hmnurl.BuildAdminAtomFeed(),
|
||||||
|
FeedUrl: hmnurl.BuildAdminApprovalQueue(),
|
||||||
|
}
|
||||||
|
|
||||||
|
unapprovedPosts, err := fetchUnapprovedPosts(c)
|
||||||
|
if err != nil {
|
||||||
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch unapproved posts"))
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Perf.StartBlock("SQL", "Fetch subforum tree")
|
||||||
|
subforumTree := models.GetFullSubforumTree(c.Context(), c.Conn)
|
||||||
|
lineageBuilder := models.MakeSubforumLineageBuilder(subforumTree)
|
||||||
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
|
for _, post := range unapprovedPosts {
|
||||||
|
postItem := MakePostListItem(
|
||||||
|
lineageBuilder,
|
||||||
|
&post.Project,
|
||||||
|
&post.Thread,
|
||||||
|
&post.Post,
|
||||||
|
&post.Author,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
c.Theme,
|
||||||
|
)
|
||||||
|
|
||||||
|
postItem.PostTypePrefix = fmt.Sprintf("ADMIN::UNAPPROVED: %s", postItem.PostTypePrefix)
|
||||||
|
postItem.UUID = uuid.NewSHA1(uuid.NameSpaceURL, []byte(postItem.Url)).URN()
|
||||||
|
postItem.LastEditDate = post.CurrentVersion.Date
|
||||||
|
feedData.Posts = append(feedData.Posts, postItem)
|
||||||
|
}
|
||||||
|
if len(feedData.Posts) > 0 {
|
||||||
|
feedData.Updated = feedData.Posts[0].Date
|
||||||
|
} else {
|
||||||
|
feedData.Updated = time.Now()
|
||||||
|
}
|
||||||
|
var res ResponseData
|
||||||
|
res.MustWriteTemplate("admin_atom.xml", feedData, c.Perf)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
ApprovalQueueActionApprove string = "approve"
|
||||||
|
ApprovalQueueActionSpammer string = "spammer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type postWithTitle struct {
|
||||||
|
templates.Post
|
||||||
|
Title string
|
||||||
|
}
|
||||||
|
|
||||||
|
type adminApprovalQueueData struct {
|
||||||
|
templates.BaseData
|
||||||
|
|
||||||
|
Posts []postWithTitle
|
||||||
|
SubmitUrl string
|
||||||
|
ApprovalAction string
|
||||||
|
SpammerAction string
|
||||||
|
}
|
||||||
|
|
||||||
|
func AdminApprovalQueue(c *RequestContext) ResponseData {
|
||||||
|
c.Perf.StartBlock("SQL", "Fetch subforum tree")
|
||||||
|
subforumTree := models.GetFullSubforumTree(c.Context(), c.Conn)
|
||||||
|
lineageBuilder := models.MakeSubforumLineageBuilder(subforumTree)
|
||||||
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
|
posts, err := fetchUnapprovedPosts(c)
|
||||||
|
if err != nil {
|
||||||
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch unapproved posts"))
|
||||||
|
}
|
||||||
|
|
||||||
|
data := adminApprovalQueueData{
|
||||||
|
BaseData: getBaseDataAutocrumb(c, "Admin approval queue"),
|
||||||
|
SubmitUrl: hmnurl.BuildAdminApprovalQueue(),
|
||||||
|
ApprovalAction: ApprovalQueueActionApprove,
|
||||||
|
SpammerAction: ApprovalQueueActionSpammer,
|
||||||
|
}
|
||||||
|
for _, p := range posts {
|
||||||
|
post := templates.PostToTemplate(&p.Post, &p.Author, c.Theme)
|
||||||
|
post.AddContentVersion(p.CurrentVersion, &p.Author) // NOTE(asaf): Don't care about editors here
|
||||||
|
post.Url = UrlForGenericPost(&p.Thread, &p.Post, lineageBuilder, p.Project.Slug)
|
||||||
|
data.Posts = append(data.Posts, postWithTitle{
|
||||||
|
Post: post,
|
||||||
|
Title: p.Thread.Title,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var res ResponseData
|
||||||
|
res.MustWriteTemplate("admin_approval_queue.html", data, c.Perf)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func AdminApprovalQueueSubmit(c *RequestContext) ResponseData {
|
||||||
|
err := c.Req.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
return c.ErrorResponse(http.StatusBadRequest, oops.New(err, "failed to parse admin approval form"))
|
||||||
|
}
|
||||||
|
action := c.Req.Form.Get("action")
|
||||||
|
userIdStr := c.Req.Form.Get("user_id")
|
||||||
|
userId, err := strconv.Atoi(userIdStr)
|
||||||
|
if err != nil {
|
||||||
|
return RejectRequest(c, "User id can't be parsed")
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := db.QueryOne(c.Context(), c.Conn, models.User{},
|
||||||
|
`
|
||||||
|
SELECT $columns FROM auth_user WHERE id = $1
|
||||||
|
`,
|
||||||
|
userId,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, db.NotFound) {
|
||||||
|
return RejectRequest(c, "User not found")
|
||||||
|
} else {
|
||||||
|
return c.ErrorResponse(http.StatusBadRequest, oops.New(err, "failed to fetch user"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
user := u.(*models.User)
|
||||||
|
|
||||||
|
whatHappened := ""
|
||||||
|
if action == ApprovalQueueActionApprove {
|
||||||
|
_, err := c.Conn.Exec(c.Context(),
|
||||||
|
`
|
||||||
|
UPDATE auth_user
|
||||||
|
SET status = $1
|
||||||
|
WHERE id = $2
|
||||||
|
`,
|
||||||
|
models.UserStatusApproved,
|
||||||
|
user.ID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return c.ErrorResponse(http.StatusBadRequest, oops.New(err, "failed to set user to approved"))
|
||||||
|
}
|
||||||
|
whatHappened = fmt.Sprintf("%s approved successfully", user.Username)
|
||||||
|
} else if action == ApprovalQueueActionSpammer {
|
||||||
|
_, err := c.Conn.Exec(c.Context(),
|
||||||
|
`
|
||||||
|
UPDATE auth_user
|
||||||
|
SET status = $1
|
||||||
|
WHERE id = $2
|
||||||
|
`,
|
||||||
|
models.UserStatusBanned,
|
||||||
|
user.ID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return c.ErrorResponse(http.StatusBadRequest, oops.New(err, "failed to set user to banned"))
|
||||||
|
}
|
||||||
|
err = auth.DeleteSessionForUser(c.Context(), c.Conn, user.Username)
|
||||||
|
if err != nil {
|
||||||
|
return c.ErrorResponse(http.StatusBadRequest, oops.New(err, "failed to log out user"))
|
||||||
|
}
|
||||||
|
err = deleteAllPostsForUser(c.Context(), c.Conn, user.ID)
|
||||||
|
whatHappened = fmt.Sprintf("%s banned successfully", user.Username)
|
||||||
|
} else {
|
||||||
|
whatHappened = fmt.Sprintf("Unrecognized action: %s", action)
|
||||||
|
}
|
||||||
|
|
||||||
|
res := c.Redirect(hmnurl.BuildAdminApprovalQueue(), http.StatusSeeOther)
|
||||||
|
res.AddFutureNotice("success", whatHappened)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnapprovedPost struct {
|
||||||
|
Project models.Project `db:"project"`
|
||||||
|
Thread models.Thread `db:"thread"`
|
||||||
|
Post models.Post `db:"post"`
|
||||||
|
CurrentVersion models.PostVersion `db:"ver"`
|
||||||
|
Author models.User `db:"author"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchUnapprovedPosts(c *RequestContext) ([]*UnapprovedPost, error) {
|
||||||
|
it, err := db.Query(c.Context(), c.Conn, UnapprovedPost{},
|
||||||
|
`
|
||||||
|
SELECT $columns
|
||||||
|
FROM
|
||||||
|
handmade_post AS post
|
||||||
|
JOIN handmade_project AS project ON post.project_id = project.id
|
||||||
|
JOIN handmade_thread AS thread ON post.thread_id = thread.id
|
||||||
|
JOIN handmade_postversion AS ver ON ver.id = post.current_id
|
||||||
|
JOIN auth_user AS author ON author.id = post.author_id
|
||||||
|
WHERE
|
||||||
|
NOT thread.deleted
|
||||||
|
AND author.status = $1
|
||||||
|
ORDER BY post.postdate DESC
|
||||||
|
`,
|
||||||
|
models.UserStatusConfirmed,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, oops.New(err, "failed to fetch unapproved posts")
|
||||||
|
}
|
||||||
|
var res []*UnapprovedPost
|
||||||
|
for _, iresult := range it.ToSlice() {
|
||||||
|
res = append(res, iresult.(*UnapprovedPost))
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteAllPostsForUser(ctx context.Context, conn *pgxpool.Pool, userId int) error {
|
||||||
|
tx, err := conn.Begin(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return oops.New(err, "failed to start transaction")
|
||||||
|
}
|
||||||
|
defer tx.Rollback(ctx)
|
||||||
|
type toDelete struct {
|
||||||
|
ThreadID int `db:"thread.id"`
|
||||||
|
PostID int `db:"post.id"`
|
||||||
|
}
|
||||||
|
it, err := db.Query(ctx, tx, toDelete{},
|
||||||
|
`
|
||||||
|
SELECT $columns
|
||||||
|
FROM
|
||||||
|
handmade_post as post
|
||||||
|
JOIN handmade_thread AS thread ON post.thread_id = thread.id
|
||||||
|
JOIN auth_user AS author ON author.id = post.author_id
|
||||||
|
WHERE author.id = $1
|
||||||
|
`,
|
||||||
|
userId,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return oops.New(err, "failed to fetch posts to delete for user")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, iResult := range it.ToSlice() {
|
||||||
|
row := iResult.(*toDelete)
|
||||||
|
DeletePost(ctx, tx, row.ThreadID, row.PostID)
|
||||||
|
}
|
||||||
|
err = tx.Commit(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return oops.New(err, "failed to commit transaction")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -372,8 +372,7 @@ func EmailConfirmationSubmit(c *RequestContext) ResponseData {
|
||||||
SET status = $1
|
SET status = $1
|
||||||
WHERE id = $2
|
WHERE id = $2
|
||||||
`,
|
`,
|
||||||
// models.UserStatusConfirmed,
|
models.UserStatusConfirmed,
|
||||||
models.UserStatusApproved, // TODO: Change this back to Confirmed after the jam.
|
|
||||||
validationResult.User.ID,
|
validationResult.User.ID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -103,6 +103,16 @@ func NewWebsiteRoutes(longRequestContext context.Context, conn *pgxpool.Pool, pe
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
adminMiddleware := func(h Handler) Handler {
|
||||||
|
return func(c *RequestContext) (res ResponseData) {
|
||||||
|
if c.CurrentUser == nil || !c.CurrentUser.IsStaff {
|
||||||
|
return FourOhFour(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return h(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
csrfMiddleware := func(h Handler) Handler {
|
csrfMiddleware := func(h Handler) Handler {
|
||||||
// CSRF mitigation actions per the OWASP cheat sheet:
|
// CSRF mitigation actions per the OWASP cheat sheet:
|
||||||
// https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
|
// https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
|
||||||
|
@ -183,6 +193,10 @@ func NewWebsiteRoutes(longRequestContext context.Context, conn *pgxpool.Pool, pe
|
||||||
mainRoutes.GET(hmnurl.RegexDoPasswordReset, DoPasswordReset)
|
mainRoutes.GET(hmnurl.RegexDoPasswordReset, DoPasswordReset)
|
||||||
mainRoutes.POST(hmnurl.RegexDoPasswordReset, DoPasswordResetSubmit)
|
mainRoutes.POST(hmnurl.RegexDoPasswordReset, DoPasswordResetSubmit)
|
||||||
|
|
||||||
|
mainRoutes.GET(hmnurl.RegexAdminAtomFeed, AdminAtomFeed)
|
||||||
|
mainRoutes.GET(hmnurl.RegexAdminApprovalQueue, adminMiddleware(AdminApprovalQueue))
|
||||||
|
mainRoutes.POST(hmnurl.RegexAdminApprovalQueue, adminMiddleware(csrfMiddleware(AdminApprovalQueueSubmit)))
|
||||||
|
|
||||||
mainRoutes.GET(hmnurl.RegexFeed, Feed)
|
mainRoutes.GET(hmnurl.RegexFeed, Feed)
|
||||||
mainRoutes.GET(hmnurl.RegexAtomFeed, AtomFeed)
|
mainRoutes.GET(hmnurl.RegexAtomFeed, AtomFeed)
|
||||||
mainRoutes.GET(hmnurl.RegexShowcase, Showcase)
|
mainRoutes.GET(hmnurl.RegexShowcase, Showcase)
|
||||||
|
@ -316,7 +330,7 @@ func getBaseData(c *RequestContext, title string, breadcrumbs []templates.Breadc
|
||||||
|
|
||||||
IsProjectPage: !c.CurrentProject.IsHMN(),
|
IsProjectPage: !c.CurrentProject.IsHMN(),
|
||||||
Header: templates.Header{
|
Header: templates.Header{
|
||||||
AdminUrl: hmnurl.BuildHomepage(), // TODO(asaf)
|
AdminUrl: hmnurl.BuildAdminApprovalQueue(), // TODO(asaf): Replace with general-purpose admin page
|
||||||
UserSettingsUrl: hmnurl.BuildUserSettings(""),
|
UserSettingsUrl: hmnurl.BuildUserSettings(""),
|
||||||
LoginActionUrl: hmnurl.BuildLoginAction(c.FullUrl()),
|
LoginActionUrl: hmnurl.BuildLoginAction(c.FullUrl()),
|
||||||
LogoutActionUrl: hmnurl.BuildLogoutAction(c.FullUrl()),
|
LogoutActionUrl: hmnurl.BuildLogoutAction(c.FullUrl()),
|
||||||
|
|
Loading…
Reference in New Issue