2021-09-24 00:12:46 +00:00
|
|
|
package website
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"encoding/base64"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
2022-08-06 23:03:34 +00:00
|
|
|
"html/template"
|
2021-09-24 00:12:46 +00:00
|
|
|
"net/http"
|
2022-03-06 12:48:47 +00:00
|
|
|
"sort"
|
2021-09-24 00:12:46 +00:00
|
|
|
"strconv"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"git.handmade.network/hmn/hmn/src/auth"
|
|
|
|
"git.handmade.network/hmn/hmn/src/config"
|
|
|
|
"git.handmade.network/hmn/hmn/src/db"
|
2021-12-09 02:04:15 +00:00
|
|
|
"git.handmade.network/hmn/hmn/src/hmndata"
|
2021-09-24 00:12:46 +00:00
|
|
|
"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"
|
2023-01-02 21:52:41 +00:00
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
2021-09-24 00:12:46 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
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"]
|
2021-09-24 00:37:08 +00:00
|
|
|
if !hasAuth {
|
|
|
|
res := ResponseData{
|
|
|
|
StatusCode: http.StatusUnauthorized,
|
|
|
|
}
|
|
|
|
res.Header().Set("WWW-Authenticate", "Basic realm=\"Admin\"")
|
|
|
|
return res
|
|
|
|
} else if auth[0] != expectedAuth {
|
2021-09-24 00:12:46 +00:00
|
|
|
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",
|
2021-09-24 00:39:59 +00:00
|
|
|
Title: "Handmade Network Admin feed",
|
2021-09-24 00:12:46 +00:00
|
|
|
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")
|
2022-06-24 21:38:11 +00:00
|
|
|
subforumTree := models.GetFullSubforumTree(c, c.Conn)
|
2021-09-24 00:12:46 +00:00
|
|
|
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
|
|
|
|
|
2022-03-06 12:48:47 +00:00
|
|
|
UnapprovedUsers []*unapprovedUserData
|
|
|
|
SubmitUrl string
|
|
|
|
ApprovalAction string
|
|
|
|
SpammerAction string
|
|
|
|
}
|
|
|
|
|
|
|
|
type projectWithLinks struct {
|
|
|
|
Project templates.Project
|
|
|
|
Links []templates.Link
|
|
|
|
}
|
|
|
|
|
|
|
|
type unapprovedUserData struct {
|
|
|
|
User templates.User
|
|
|
|
Date time.Time
|
|
|
|
UserLinks []templates.Link
|
|
|
|
ProjectsWithLinks []projectWithLinks
|
2022-08-06 23:03:34 +00:00
|
|
|
Timeline []templates.TimelineItem
|
2021-09-24 00:12:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func AdminApprovalQueue(c *RequestContext) ResponseData {
|
|
|
|
c.Perf.StartBlock("SQL", "Fetch subforum tree")
|
2022-06-24 21:38:11 +00:00
|
|
|
subforumTree := models.GetFullSubforumTree(c, c.Conn)
|
2021-09-24 00:12:46 +00:00
|
|
|
lineageBuilder := models.MakeSubforumLineageBuilder(subforumTree)
|
|
|
|
c.Perf.EndBlock()
|
|
|
|
|
2022-08-06 23:03:34 +00:00
|
|
|
potentialUsers, err := db.QueryScalar[int](c, c.Conn,
|
|
|
|
`
|
|
|
|
SELECT id
|
|
|
|
FROM hmn_user
|
|
|
|
WHERE hmn_user.status = $1
|
|
|
|
`,
|
|
|
|
models.UserStatusConfirmed,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch unapproved users"))
|
|
|
|
}
|
|
|
|
|
|
|
|
snippets, err := hmndata.FetchSnippets(c, c.Conn, c.CurrentUser, hmndata.SnippetQuery{
|
|
|
|
OwnerIDs: potentialUsers,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch unapproved snippets"))
|
|
|
|
}
|
|
|
|
|
2021-09-24 00:12:46 +00:00
|
|
|
posts, err := fetchUnapprovedPosts(c)
|
|
|
|
if err != nil {
|
|
|
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch unapproved posts"))
|
|
|
|
}
|
|
|
|
|
2022-03-06 12:48:47 +00:00
|
|
|
projects, err := fetchUnapprovedProjects(c)
|
|
|
|
if err != nil {
|
|
|
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch unapproved projects"))
|
2021-09-24 00:12:46 +00:00
|
|
|
}
|
2022-03-06 12:48:47 +00:00
|
|
|
|
|
|
|
unapprovedUsers := make([]*unapprovedUserData, 0)
|
|
|
|
userIDToDataIdx := make(map[int]int)
|
|
|
|
|
2022-08-06 23:03:34 +00:00
|
|
|
for _, s := range snippets {
|
|
|
|
var userData *unapprovedUserData
|
|
|
|
if idx, ok := userIDToDataIdx[s.Owner.ID]; ok {
|
|
|
|
userData = unapprovedUsers[idx]
|
|
|
|
} else {
|
|
|
|
userData = &unapprovedUserData{
|
|
|
|
User: templates.UserToTemplate(s.Owner, c.Theme),
|
|
|
|
UserLinks: make([]templates.Link, 0, 10),
|
|
|
|
}
|
|
|
|
unapprovedUsers = append(unapprovedUsers, userData)
|
|
|
|
userIDToDataIdx[s.Owner.ID] = len(unapprovedUsers) - 1
|
|
|
|
}
|
|
|
|
|
|
|
|
if s.Snippet.When.After(userData.Date) {
|
|
|
|
userData.Date = s.Snippet.When
|
|
|
|
}
|
|
|
|
timelineItem := SnippetToTimelineItem(&s.Snippet, s.Asset, s.DiscordMessage, s.Projects, s.Owner, c.Theme, false)
|
|
|
|
timelineItem.OwnerAvatarUrl = ""
|
|
|
|
timelineItem.SmallInfo = true
|
|
|
|
userData.Timeline = append(userData.Timeline, timelineItem)
|
|
|
|
}
|
|
|
|
|
2021-09-24 00:12:46 +00:00
|
|
|
for _, p := range posts {
|
2022-03-06 12:48:47 +00:00
|
|
|
var userData *unapprovedUserData
|
|
|
|
if idx, ok := userIDToDataIdx[p.Author.ID]; ok {
|
|
|
|
userData = unapprovedUsers[idx]
|
|
|
|
} else {
|
|
|
|
userData = &unapprovedUserData{
|
|
|
|
User: templates.UserToTemplate(&p.Author, c.Theme),
|
|
|
|
UserLinks: make([]templates.Link, 0, 10),
|
|
|
|
}
|
|
|
|
unapprovedUsers = append(unapprovedUsers, userData)
|
|
|
|
userIDToDataIdx[p.Author.ID] = len(unapprovedUsers) - 1
|
|
|
|
}
|
|
|
|
|
|
|
|
if p.Post.PostDate.After(userData.Date) {
|
|
|
|
userData.Date = p.Post.PostDate
|
|
|
|
}
|
2022-08-06 23:03:34 +00:00
|
|
|
timelineItem := PostToTimelineItem(hmndata.UrlContextForProject(&p.Project), lineageBuilder, &p.Post, &p.Thread, &p.Author, c.Theme)
|
|
|
|
timelineItem.OwnerAvatarUrl = ""
|
|
|
|
timelineItem.SmallInfo = true
|
|
|
|
timelineItem.Description = template.HTML(p.CurrentVersion.TextParsed)
|
|
|
|
userData.Timeline = append(userData.Timeline, timelineItem)
|
2021-09-24 00:12:46 +00:00
|
|
|
}
|
|
|
|
|
2022-03-06 12:48:47 +00:00
|
|
|
for _, p := range projects {
|
|
|
|
var userData *unapprovedUserData
|
|
|
|
if idx, ok := userIDToDataIdx[p.User.ID]; ok {
|
|
|
|
userData = unapprovedUsers[idx]
|
|
|
|
} else {
|
|
|
|
userData = &unapprovedUserData{
|
|
|
|
User: templates.UserToTemplate(p.User, c.Theme),
|
|
|
|
UserLinks: make([]templates.Link, 0, 10),
|
|
|
|
}
|
|
|
|
unapprovedUsers = append(unapprovedUsers, userData)
|
|
|
|
userIDToDataIdx[p.User.ID] = len(unapprovedUsers) - 1
|
|
|
|
}
|
|
|
|
|
|
|
|
projectLinks := make([]templates.Link, 0, len(p.ProjectLinks))
|
|
|
|
for _, l := range p.ProjectLinks {
|
|
|
|
projectLinks = append(projectLinks, templates.LinkToTemplate(l))
|
|
|
|
}
|
|
|
|
if p.ProjectAndStuff.Project.DateCreated.After(userData.Date) {
|
|
|
|
userData.Date = p.ProjectAndStuff.Project.DateCreated
|
|
|
|
}
|
|
|
|
userData.ProjectsWithLinks = append(userData.ProjectsWithLinks, projectWithLinks{
|
|
|
|
Project: templates.ProjectAndStuffToTemplate(p.ProjectAndStuff, hmndata.UrlContextForProject(&p.ProjectAndStuff.Project).BuildHomepage(), c.Theme),
|
|
|
|
Links: projectLinks,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
userIds := make([]int, 0, len(unapprovedUsers))
|
|
|
|
for _, u := range unapprovedUsers {
|
|
|
|
userIds = append(userIds, u.User.ID)
|
|
|
|
}
|
|
|
|
|
2022-06-24 21:38:11 +00:00
|
|
|
userLinks, err := db.Query[models.Link](c, c.Conn,
|
2022-03-06 12:48:47 +00:00
|
|
|
`
|
|
|
|
SELECT $columns
|
|
|
|
FROM
|
2022-05-07 13:11:05 +00:00
|
|
|
link
|
2022-03-06 12:48:47 +00:00
|
|
|
WHERE
|
|
|
|
user_id = ANY($1)
|
|
|
|
ORDER BY ordering ASC
|
|
|
|
`,
|
|
|
|
userIds,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch user links"))
|
|
|
|
}
|
|
|
|
|
2022-04-16 17:49:29 +00:00
|
|
|
for _, link := range userLinks {
|
2022-03-06 12:48:47 +00:00
|
|
|
userData := unapprovedUsers[userIDToDataIdx[*link.UserID]]
|
|
|
|
userData.UserLinks = append(userData.UserLinks, templates.LinkToTemplate(link))
|
|
|
|
}
|
|
|
|
|
|
|
|
sort.Slice(unapprovedUsers, func(a, b int) bool {
|
|
|
|
return unapprovedUsers[a].Date.After(unapprovedUsers[b].Date)
|
|
|
|
})
|
|
|
|
|
|
|
|
data := adminApprovalQueueData{
|
|
|
|
BaseData: getBaseDataAutocrumb(c, "Admin approval queue"),
|
|
|
|
UnapprovedUsers: unapprovedUsers,
|
|
|
|
SubmitUrl: hmnurl.BuildAdminApprovalQueue(),
|
|
|
|
ApprovalAction: ApprovalQueueActionApprove,
|
|
|
|
SpammerAction: ApprovalQueueActionSpammer,
|
|
|
|
}
|
|
|
|
|
2021-09-24 00:12:46 +00:00
|
|
|
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 {
|
2022-06-24 21:38:11 +00:00
|
|
|
return c.RejectRequest("User id can't be parsed")
|
2021-09-24 00:12:46 +00:00
|
|
|
}
|
|
|
|
|
2022-06-24 21:38:11 +00:00
|
|
|
user, err := hmndata.FetchUser(c, c.Conn, c.CurrentUser, userId, hmndata.UsersQuery{})
|
2021-09-24 00:12:46 +00:00
|
|
|
if err != nil {
|
|
|
|
if errors.Is(err, db.NotFound) {
|
2022-06-24 21:38:11 +00:00
|
|
|
return c.RejectRequest("User not found")
|
2021-09-24 00:12:46 +00:00
|
|
|
} else {
|
2021-12-13 16:58:26 +00:00
|
|
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch user"))
|
2021-09-24 00:12:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
whatHappened := ""
|
|
|
|
if action == ApprovalQueueActionApprove {
|
2022-06-24 21:38:11 +00:00
|
|
|
_, err := c.Conn.Exec(c,
|
2021-09-24 00:12:46 +00:00
|
|
|
`
|
2022-05-07 13:11:05 +00:00
|
|
|
UPDATE hmn_user
|
2021-09-24 00:12:46 +00:00
|
|
|
SET status = $1
|
|
|
|
WHERE id = $2
|
|
|
|
`,
|
|
|
|
models.UserStatusApproved,
|
|
|
|
user.ID,
|
|
|
|
)
|
|
|
|
if err != nil {
|
2021-12-13 16:58:26 +00:00
|
|
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to set user to approved"))
|
2021-09-24 00:12:46 +00:00
|
|
|
}
|
|
|
|
whatHappened = fmt.Sprintf("%s approved successfully", user.Username)
|
|
|
|
} else if action == ApprovalQueueActionSpammer {
|
2022-06-24 21:38:11 +00:00
|
|
|
_, err := c.Conn.Exec(c,
|
2021-09-24 00:12:46 +00:00
|
|
|
`
|
2022-05-07 13:11:05 +00:00
|
|
|
UPDATE hmn_user
|
2021-09-24 00:12:46 +00:00
|
|
|
SET status = $1
|
|
|
|
WHERE id = $2
|
|
|
|
`,
|
|
|
|
models.UserStatusBanned,
|
|
|
|
user.ID,
|
|
|
|
)
|
|
|
|
if err != nil {
|
2021-12-13 16:58:26 +00:00
|
|
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to set user to banned"))
|
2021-09-24 00:12:46 +00:00
|
|
|
}
|
2022-06-24 21:38:11 +00:00
|
|
|
err = auth.DeleteSessionForUser(c, c.Conn, user.Username)
|
2021-09-24 00:12:46 +00:00
|
|
|
if err != nil {
|
2021-12-13 16:58:26 +00:00
|
|
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to log out user"))
|
2021-09-24 00:12:46 +00:00
|
|
|
}
|
2022-06-24 21:38:11 +00:00
|
|
|
err = deleteAllPostsForUser(c, c.Conn, user.ID)
|
2021-12-13 16:58:26 +00:00
|
|
|
if err != nil {
|
|
|
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete spammer's posts"))
|
|
|
|
}
|
2022-06-24 21:38:11 +00:00
|
|
|
err = deleteAllProjectsForUser(c, c.Conn, user.ID)
|
2022-03-06 12:48:47 +00:00
|
|
|
if err != nil {
|
|
|
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete spammer's projects"))
|
|
|
|
}
|
2022-08-09 09:57:12 +00:00
|
|
|
err = deleteAllSnippetsForUser(c, c.Conn, user.ID)
|
|
|
|
if err != nil {
|
|
|
|
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete spammer's snippets"))
|
|
|
|
}
|
2021-09-24 00:12:46 +00:00
|
|
|
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) {
|
2022-06-24 21:38:11 +00:00
|
|
|
posts, err := db.Query[UnapprovedPost](c, c.Conn,
|
2021-09-24 00:12:46 +00:00
|
|
|
`
|
|
|
|
SELECT $columns
|
|
|
|
FROM
|
2022-05-07 13:11:05 +00:00
|
|
|
post
|
|
|
|
JOIN project ON post.project_id = project.id
|
|
|
|
JOIN thread ON post.thread_id = thread.id
|
|
|
|
JOIN post_version AS ver ON ver.id = post.current_id
|
|
|
|
JOIN hmn_user AS author ON author.id = post.author_id
|
|
|
|
LEFT JOIN asset AS author_avatar ON author_avatar.id = author.avatar_asset_id
|
2021-09-24 00:12:46 +00:00
|
|
|
WHERE
|
|
|
|
NOT thread.deleted
|
2021-12-13 16:58:26 +00:00
|
|
|
AND NOT post.deleted
|
|
|
|
AND author.status = ANY($1)
|
2021-09-24 00:12:46 +00:00
|
|
|
ORDER BY post.postdate DESC
|
|
|
|
`,
|
2021-12-13 16:58:26 +00:00
|
|
|
[]models.UserStatus{models.UserStatusConfirmed},
|
2021-09-24 00:12:46 +00:00
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, oops.New(err, "failed to fetch unapproved posts")
|
|
|
|
}
|
2022-04-16 17:49:29 +00:00
|
|
|
return posts, nil
|
2021-09-24 00:12:46 +00:00
|
|
|
}
|
|
|
|
|
2022-03-06 12:48:47 +00:00
|
|
|
type UnapprovedProject struct {
|
|
|
|
User *models.User
|
|
|
|
ProjectAndStuff *hmndata.ProjectAndStuff
|
|
|
|
ProjectLinks []*models.Link
|
|
|
|
}
|
|
|
|
|
|
|
|
func fetchUnapprovedProjects(c *RequestContext) ([]UnapprovedProject, error) {
|
2022-06-24 21:38:11 +00:00
|
|
|
ownerIDs, err := db.QueryScalar[int](c, c.Conn,
|
2022-03-06 12:48:47 +00:00
|
|
|
`
|
2022-04-16 17:49:29 +00:00
|
|
|
SELECT id
|
2022-03-06 12:48:47 +00:00
|
|
|
FROM
|
2022-05-07 13:11:05 +00:00
|
|
|
hmn_user AS u
|
2022-03-06 12:48:47 +00:00
|
|
|
WHERE
|
|
|
|
u.status = ANY($1)
|
|
|
|
`,
|
|
|
|
[]models.UserStatus{models.UserStatusConfirmed},
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, oops.New(err, "failed to fetch unapproved users")
|
|
|
|
}
|
|
|
|
|
2022-06-24 21:38:11 +00:00
|
|
|
projects, err := hmndata.FetchProjects(c, c.Conn, c.CurrentUser, hmndata.ProjectsQuery{
|
2022-03-06 12:48:47 +00:00
|
|
|
OwnerIDs: ownerIDs,
|
|
|
|
IncludeHidden: true,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
projectIDs := make([]int, 0, len(projects))
|
|
|
|
for _, p := range projects {
|
|
|
|
projectIDs = append(projectIDs, p.Project.ID)
|
|
|
|
}
|
|
|
|
|
2022-06-24 21:38:11 +00:00
|
|
|
projectLinks, err := db.Query[models.Link](c, c.Conn,
|
2022-03-06 12:48:47 +00:00
|
|
|
`
|
|
|
|
SELECT $columns
|
|
|
|
FROM
|
2022-05-07 13:11:05 +00:00
|
|
|
link
|
2022-03-06 12:48:47 +00:00
|
|
|
WHERE
|
|
|
|
link.project_id = ANY($1)
|
|
|
|
ORDER BY link.ordering ASC
|
|
|
|
`,
|
|
|
|
projectIDs,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, oops.New(err, "failed to fetch links for projects")
|
|
|
|
}
|
|
|
|
|
|
|
|
var result []UnapprovedProject
|
|
|
|
|
|
|
|
for idx, proj := range projects {
|
|
|
|
links := make([]*models.Link, 0, 10) // NOTE(asaf): 10 should be enough for most projects.
|
2022-04-16 17:49:29 +00:00
|
|
|
for _, link := range projectLinks {
|
2022-03-06 12:48:47 +00:00
|
|
|
if *link.ProjectID == proj.Project.ID {
|
|
|
|
links = append(links, link)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, u := range proj.Owners {
|
|
|
|
if u.Status == models.UserStatusConfirmed {
|
|
|
|
result = append(result, UnapprovedProject{
|
|
|
|
User: u,
|
|
|
|
ProjectAndStuff: &projects[idx],
|
|
|
|
ProjectLinks: links,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
2021-09-24 00:12:46 +00:00
|
|
|
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"`
|
|
|
|
}
|
2022-04-16 17:49:29 +00:00
|
|
|
rows, err := db.Query[toDelete](ctx, tx,
|
2021-09-24 00:12:46 +00:00
|
|
|
`
|
|
|
|
SELECT $columns
|
|
|
|
FROM
|
2022-05-07 13:11:05 +00:00
|
|
|
post as post
|
|
|
|
JOIN thread ON post.thread_id = thread.id
|
|
|
|
JOIN hmn_user AS author ON author.id = post.author_id
|
2021-09-24 00:12:46 +00:00
|
|
|
WHERE author.id = $1
|
|
|
|
`,
|
|
|
|
userId,
|
|
|
|
)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return oops.New(err, "failed to fetch posts to delete for user")
|
|
|
|
}
|
|
|
|
|
2022-04-16 17:49:29 +00:00
|
|
|
for _, row := range rows {
|
2021-12-09 02:04:15 +00:00
|
|
|
hmndata.DeletePost(ctx, tx, row.ThreadID, row.PostID)
|
2021-09-24 00:12:46 +00:00
|
|
|
}
|
|
|
|
err = tx.Commit(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return oops.New(err, "failed to commit transaction")
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2022-03-06 12:48:47 +00:00
|
|
|
|
|
|
|
func deleteAllProjectsForUser(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)
|
|
|
|
|
2022-04-16 17:49:29 +00:00
|
|
|
projectIDsToDelete, err := db.QueryScalar[int](ctx, tx,
|
2022-03-06 12:48:47 +00:00
|
|
|
`
|
2022-04-16 17:49:29 +00:00
|
|
|
SELECT project.id
|
2022-03-06 12:48:47 +00:00
|
|
|
FROM
|
2022-05-07 13:11:05 +00:00
|
|
|
project
|
|
|
|
JOIN user_project AS up ON up.project_id = project.id
|
2022-03-06 12:48:47 +00:00
|
|
|
WHERE
|
|
|
|
up.user_id = $1
|
|
|
|
`,
|
|
|
|
userId,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return oops.New(err, "failed to fetch user's projects")
|
|
|
|
}
|
|
|
|
|
2022-04-16 17:49:29 +00:00
|
|
|
if len(projectIDsToDelete) > 0 {
|
2022-03-06 12:48:47 +00:00
|
|
|
_, err = tx.Exec(ctx,
|
|
|
|
`
|
2022-05-07 13:11:05 +00:00
|
|
|
DELETE FROM project WHERE id = ANY($1)
|
2022-03-06 12:48:47 +00:00
|
|
|
`,
|
2022-04-16 17:49:29 +00:00
|
|
|
projectIDsToDelete,
|
2022-03-06 12:48:47 +00:00
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return oops.New(err, "failed to delete user's projects")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
err = tx.Commit(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return oops.New(err, "failed to commit transaction")
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2022-08-09 09:57:12 +00:00
|
|
|
|
|
|
|
func deleteAllSnippetsForUser(ctx context.Context, conn *pgxpool.Pool, userId int) error {
|
|
|
|
_, err := conn.Exec(ctx,
|
|
|
|
`
|
|
|
|
DELETE FROM snippet
|
|
|
|
WHERE owner_id = $1
|
|
|
|
`,
|
|
|
|
userId,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return oops.New(err, "failed to delete snippets for user")
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|