Merge remote-tracking branch 'origin/master' into beta

This commit is contained in:
Ben Visness 2021-12-14 20:54:23 -06:00
commit 6307589ee4
20 changed files with 238 additions and 59 deletions

View File

@ -231,7 +231,16 @@ func followPathThroughStructs(structPtrVal reflect.Value, path []int) (reflect.V
return val, field return val, field
} }
func Query(ctx context.Context, conn ConnOrTx, destExample interface{}, query string, args ...interface{}) (*StructQueryIterator, error) { func Query(ctx context.Context, conn ConnOrTx, destExample interface{}, query string, args ...interface{}) ([]interface{}, error) {
it, err := QueryIterator(ctx, conn, destExample, query, args...)
if err != nil {
return nil, err
} else {
return it.ToSlice(), nil
}
}
func QueryIterator(ctx context.Context, conn ConnOrTx, destExample interface{}, query string, args ...interface{}) (*StructQueryIterator, error) {
destType := reflect.TypeOf(destExample) destType := reflect.TypeOf(destExample)
columnNames, fieldPaths, err := getColumnNamesAndPaths(destType, nil, "") columnNames, fieldPaths, err := getColumnNamesAndPaths(destType, nil, "")
if err != nil { if err != nil {
@ -347,7 +356,7 @@ result but find nothing.
var NotFound = errors.New("not found") var NotFound = errors.New("not found")
func QueryOne(ctx context.Context, conn ConnOrTx, 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...) rows, err := QueryIterator(ctx, conn, destExample, query, args...)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -408,7 +408,7 @@ func (bot *botInstance) doSender(ctx context.Context) {
} }
defer tx.Rollback(ctx) defer tx.Rollback(ctx)
itMessages, err := db.Query(ctx, tx, models.DiscordOutgoingMessage{}, ` msgs, err := db.Query(ctx, tx, models.DiscordOutgoingMessage{}, `
SELECT $columns SELECT $columns
FROM discord_outgoingmessages FROM discord_outgoingmessages
ORDER BY id ASC ORDER BY id ASC
@ -418,7 +418,6 @@ func (bot *botInstance) doSender(ctx context.Context) {
return return
} }
msgs := itMessages.ToSlice()
for _, imsg := range msgs { for _, imsg := range msgs {
msg := imsg.(*models.DiscordOutgoingMessage) msg := imsg.(*models.DiscordOutgoingMessage)
if time.Now().After(msg.ExpiresAt) { if time.Now().After(msg.ExpiresAt) {

View File

@ -64,7 +64,7 @@ func fetchMissingContent(ctx context.Context, dbConn *pgxpool.Pool) {
type query struct { type query struct {
Message models.DiscordMessage `db:"msg"` Message models.DiscordMessage `db:"msg"`
} }
result, err := db.Query(ctx, dbConn, query{}, imessagesWithoutContent, err := db.Query(ctx, dbConn, query{},
` `
SELECT $columns SELECT $columns
FROM FROM
@ -82,7 +82,6 @@ func fetchMissingContent(ctx context.Context, dbConn *pgxpool.Pool) {
log.Error().Err(err).Msg("failed to check for messages without content") log.Error().Err(err).Msg("failed to check for messages without content")
return return
} }
imessagesWithoutContent := result.ToSlice()
if len(imessagesWithoutContent) > 0 { if len(imessagesWithoutContent) > 0 {
log.Info().Msgf("There are %d Discord messages without content, fetching their content now...", len(imessagesWithoutContent)) log.Info().Msgf("There are %d Discord messages without content, fetching their content now...", len(imessagesWithoutContent))

View File

@ -749,7 +749,7 @@ func UpdateSnippetTagsIfAny(ctx context.Context, dbConn db.ConnOrTx, msg *Messag
type tagsRow struct { type tagsRow struct {
Tag models.Tag `db:"tags"` Tag models.Tag `db:"tags"`
} }
itUserTags, err := db.Query(ctx, tx, tagsRow{}, iUserTags, err := db.Query(ctx, tx, tagsRow{},
` `
SELECT $columns SELECT $columns
FROM FROM
@ -764,7 +764,6 @@ func UpdateSnippetTagsIfAny(ctx context.Context, dbConn db.ConnOrTx, msg *Messag
if err != nil { if err != nil {
return oops.New(err, "failed to fetch tags for user projects") return oops.New(err, "failed to fetch tags for user projects")
} }
iUserTags := itUserTags.ToSlice()
var tagIDs []int var tagIDs []int
for _, itag := range iUserTags { for _, itag := range iUserTags {
@ -805,7 +804,7 @@ var RESnippetableUrl = regexp.MustCompile(`^https?://(youtu\.be|(www\.)?youtube\
func getSnippetAssetOrUrl(ctx context.Context, tx db.ConnOrTx, msg *models.DiscordMessage) (*uuid.UUID, *string, error) { func getSnippetAssetOrUrl(ctx context.Context, tx db.ConnOrTx, msg *models.DiscordMessage) (*uuid.UUID, *string, error) {
// Check attachments // Check attachments
itAttachments, err := db.Query(ctx, tx, models.DiscordMessageAttachment{}, attachments, err := db.Query(ctx, tx, models.DiscordMessageAttachment{},
` `
SELECT $columns SELECT $columns
FROM handmade_discordmessageattachment FROM handmade_discordmessageattachment
@ -816,14 +815,13 @@ func getSnippetAssetOrUrl(ctx context.Context, tx db.ConnOrTx, msg *models.Disco
if err != nil { if err != nil {
return nil, nil, oops.New(err, "failed to fetch message attachments") return nil, nil, oops.New(err, "failed to fetch message attachments")
} }
attachments := itAttachments.ToSlice()
for _, iattachment := range attachments { for _, iattachment := range attachments {
attachment := iattachment.(*models.DiscordMessageAttachment) attachment := iattachment.(*models.DiscordMessageAttachment)
return &attachment.AssetID, nil, nil return &attachment.AssetID, nil, nil
} }
// Check embeds // Check embeds
itEmbeds, err := db.Query(ctx, tx, models.DiscordMessageEmbed{}, embeds, err := db.Query(ctx, tx, models.DiscordMessageEmbed{},
` `
SELECT $columns SELECT $columns
FROM handmade_discordmessageembed FROM handmade_discordmessageembed
@ -834,7 +832,6 @@ func getSnippetAssetOrUrl(ctx context.Context, tx db.ConnOrTx, msg *models.Disco
if err != nil { if err != nil {
return nil, nil, oops.New(err, "failed to fetch discord embeds") return nil, nil, oops.New(err, "failed to fetch discord embeds")
} }
embeds := itEmbeds.ToSlice()
for _, iembed := range embeds { for _, iembed := range embeds {
embed := iembed.(*models.DiscordMessageEmbed) embed := iembed.(*models.DiscordMessageEmbed)
if embed.VideoID != nil { if embed.VideoID != nil {

View File

@ -145,11 +145,10 @@ func FetchProjects(
} }
// Do the query // Do the query
itProjects, err := db.Query(ctx, dbConn, projectRow{}, qb.String(), qb.Args()...) iprojects, err := db.Query(ctx, dbConn, projectRow{}, qb.String(), qb.Args()...)
if err != nil { if err != nil {
return nil, oops.New(err, "failed to fetch projects") return nil, oops.New(err, "failed to fetch projects")
} }
iprojects := itProjects.ToSlice()
// Fetch project owners to do permission checks // Fetch project owners to do permission checks
projectIds := make([]int, len(iprojects)) projectIds := make([]int, len(iprojects))
@ -340,7 +339,7 @@ func FetchMultipleProjectsOwners(
UserID int `db:"user_id"` UserID int `db:"user_id"`
ProjectID int `db:"project_id"` ProjectID int `db:"project_id"`
} }
it, err := db.Query(ctx, tx, userProject{}, iuserprojects, err := db.Query(ctx, tx, userProject{},
` `
SELECT $columns SELECT $columns
FROM handmade_user_projects FROM handmade_user_projects
@ -351,7 +350,6 @@ func FetchMultipleProjectsOwners(
if err != nil { if err != nil {
return nil, oops.New(err, "failed to fetch project IDs") return nil, oops.New(err, "failed to fetch project IDs")
} }
iuserprojects := it.ToSlice()
// Get the unique user IDs from this set and fetch the users from the db // Get the unique user IDs from this set and fetch the users from the db
var userIds []int var userIds []int
@ -368,7 +366,7 @@ func FetchMultipleProjectsOwners(
userIds = append(userIds, userProject.UserID) userIds = append(userIds, userProject.UserID)
} }
} }
it, err = db.Query(ctx, tx, models.User{}, iusers, err := db.Query(ctx, tx, models.User{},
` `
SELECT $columns SELECT $columns
FROM auth_user FROM auth_user
@ -380,7 +378,6 @@ func FetchMultipleProjectsOwners(
if err != nil { if err != nil {
return nil, oops.New(err, "failed to fetch users for projects") return nil, oops.New(err, "failed to fetch users for projects")
} }
iusers := it.ToSlice()
// Build the final result set with real user data // Build the final result set with real user data
res := make([]ProjectOwners, len(projectIds)) res := make([]ProjectOwners, len(projectIds))

View File

@ -47,7 +47,7 @@ func FetchSnippets(
type snippetIDRow struct { type snippetIDRow struct {
SnippetID int `db:"snippet_id"` SnippetID int `db:"snippet_id"`
} }
itSnippetIDs, err := db.Query(ctx, tx, snippetIDRow{}, iSnippetIDs, err := db.Query(ctx, tx, snippetIDRow{},
` `
SELECT DISTINCT snippet_id SELECT DISTINCT snippet_id
FROM FROM
@ -61,7 +61,6 @@ func FetchSnippets(
if err != nil { if err != nil {
return nil, oops.New(err, "failed to get snippet IDs for tag") return nil, oops.New(err, "failed to get snippet IDs for tag")
} }
iSnippetIDs := itSnippetIDs.ToSlice()
// special early-out: no snippets found for these tags at all // special early-out: no snippets found for these tags at all
if len(iSnippetIDs) == 0 { if len(iSnippetIDs) == 0 {
@ -125,11 +124,10 @@ func FetchSnippets(
DiscordMessage *models.DiscordMessage `db:"discord_message"` DiscordMessage *models.DiscordMessage `db:"discord_message"`
} }
it, err := db.Query(ctx, tx, resultRow{}, qb.String(), qb.Args()...) iresults, err := db.Query(ctx, tx, resultRow{}, qb.String(), qb.Args()...)
if err != nil { if err != nil {
return nil, oops.New(err, "failed to fetch threads") return nil, oops.New(err, "failed to fetch threads")
} }
iresults := it.ToSlice()
result := make([]SnippetAndStuff, len(iresults)) // allocate extra space because why not result := make([]SnippetAndStuff, len(iresults)) // allocate extra space because why not
snippetIDs := make([]int, len(iresults)) snippetIDs := make([]int, len(iresults))
@ -151,7 +149,7 @@ func FetchSnippets(
SnippetID int `db:"snippet_tags.snippet_id"` SnippetID int `db:"snippet_tags.snippet_id"`
Tag *models.Tag `db:"tags"` Tag *models.Tag `db:"tags"`
} }
itSnippetTags, err := db.Query(ctx, tx, snippetTagRow{}, iSnippetTags, err := db.Query(ctx, tx, snippetTagRow{},
` `
SELECT $columns SELECT $columns
FROM FROM
@ -165,7 +163,6 @@ func FetchSnippets(
if err != nil { if err != nil {
return nil, oops.New(err, "failed to fetch tags for snippets") return nil, oops.New(err, "failed to fetch tags for snippets")
} }
iSnippetTags := itSnippetTags.ToSlice()
// associate tags with snippets // associate tags with snippets
resultBySnippetId := make(map[int]*SnippetAndStuff) resultBySnippetId := make(map[int]*SnippetAndStuff)

View File

@ -40,11 +40,10 @@ func FetchTags(ctx context.Context, dbConn db.ConnOrTx, q TagQuery) ([]*models.T
qb.Add(`LIMIT $? OFFSET $?`, q.Limit, q.Offset) qb.Add(`LIMIT $? OFFSET $?`, q.Limit, q.Offset)
} }
it, err := db.Query(ctx, dbConn, models.Tag{}, qb.String(), qb.Args()...) itags, err := db.Query(ctx, dbConn, models.Tag{}, qb.String(), qb.Args()...)
if err != nil { if err != nil {
return nil, oops.New(err, "failed to fetch tags") return nil, oops.New(err, "failed to fetch tags")
} }
itags := it.ToSlice()
res := make([]*models.Tag, len(itags)) res := make([]*models.Tag, len(itags))
for i, itag := range itags { for i, itag := range itags {

View File

@ -143,11 +143,10 @@ func FetchThreads(
ForumLastReadTime *time.Time `db:"slri.lastread"` ForumLastReadTime *time.Time `db:"slri.lastread"`
} }
it, err := db.Query(ctx, dbConn, resultRow{}, qb.String(), qb.Args()...) iresults, err := db.Query(ctx, dbConn, resultRow{}, qb.String(), qb.Args()...)
if err != nil { if err != nil {
return nil, oops.New(err, "failed to fetch threads") return nil, oops.New(err, "failed to fetch threads")
} }
iresults := it.ToSlice()
result := make([]ThreadAndStuff, len(iresults)) result := make([]ThreadAndStuff, len(iresults))
for i, iresult := range iresults { for i, iresult := range iresults {
@ -398,11 +397,10 @@ func FetchPosts(
qb.Add(`LIMIT $? OFFSET $?`, q.Limit, q.Offset) qb.Add(`LIMIT $? OFFSET $?`, q.Limit, q.Offset)
} }
it, err := db.Query(ctx, dbConn, resultRow{}, qb.String(), qb.Args()...) iresults, err := db.Query(ctx, dbConn, resultRow{}, qb.String(), qb.Args()...)
if err != nil { if err != nil {
return nil, oops.New(err, "failed to fetch posts") return nil, oops.New(err, "failed to fetch posts")
} }
iresults := it.ToSlice()
result := make([]PostAndStuff, len(iresults)) result := make([]PostAndStuff, len(iresults))
for i, iresult := range iresults { for i, iresult := range iresults {
@ -696,20 +694,28 @@ func DeletePost(
tx pgx.Tx, tx pgx.Tx,
threadId, postId int, threadId, postId int,
) (threadDeleted bool) { ) (threadDeleted bool) {
isFirstPost, err := db.QueryBool(ctx, tx, type threadInfo struct {
FirstPostID int `db:"first_id"`
Deleted bool `db:"deleted"`
}
ti, err := db.QueryOne(ctx, tx, threadInfo{},
` `
SELECT thread.first_id = $1 SELECT $columns
FROM FROM
handmade_thread AS thread handmade_thread AS thread
WHERE WHERE
thread.id = $2 thread.id = $1
`, `,
postId,
threadId, threadId,
) )
if err != nil { if err != nil {
panic(oops.New(err, "failed to check if post was the first post in the thread")) panic(oops.New(err, "failed to fetch thread info"))
} }
info := ti.(*threadInfo)
if info.Deleted {
return true
}
isFirstPost := info.FirstPostID == postId
if isFirstPost { if isFirstPost {
// Just delete the whole thread and all its posts. // Just delete the whole thread and all its posts.
@ -848,7 +854,7 @@ func CreatePostVersion(ctx context.Context, tx pgx.Tx, postId int, unparsedConte
var values [][]interface{} var values [][]interface{}
for _, asset := range assetResult.ToSlice() { for _, asset := range assetResult {
values = append(values, []interface{}{postId, asset.(*assetId).AssetID}) values = append(values, []interface{}{postId, asset.(*assetId).AssetID})
} }
@ -884,7 +890,7 @@ func FixThreadPostIds(ctx context.Context, tx pgx.Tx, threadId int) error {
} }
var firstPost, lastPost *models.Post var firstPost, lastPost *models.Post
for _, ipost := range postsIter.ToSlice() { for _, ipost := range postsIter {
post := ipost.(*models.Post) post := ipost.(*models.Post)
if firstPost == nil || post.PostDate.Before(firstPost.PostDate) { if firstPost == nil || post.PostDate.Before(firstPost.PostDate) {

View File

@ -105,6 +105,8 @@ func TestUserSettings(t *testing.T) {
func TestAdmin(t *testing.T) { func TestAdmin(t *testing.T) {
AssertRegexMatch(t, BuildAdminAtomFeed(), RegexAdminAtomFeed, nil) AssertRegexMatch(t, BuildAdminAtomFeed(), RegexAdminAtomFeed, nil)
AssertRegexMatch(t, BuildAdminApprovalQueue(), RegexAdminApprovalQueue, nil) AssertRegexMatch(t, BuildAdminApprovalQueue(), RegexAdminApprovalQueue, nil)
AssertRegexMatch(t, BuildAdminSetUserStatus(), RegexAdminSetUserStatus, nil)
AssertRegexMatch(t, BuildAdminNukeUser(), RegexAdminNukeUser, nil)
} }
func TestSnippet(t *testing.T) { func TestSnippet(t *testing.T) {

View File

@ -216,6 +216,20 @@ func BuildAdminApprovalQueue() string {
return Url("/admin/approvals", nil) return Url("/admin/approvals", nil)
} }
var RegexAdminSetUserStatus = regexp.MustCompile(`^/admin/setuserstatus$`)
func BuildAdminSetUserStatus() string {
defer CatchPanic()
return Url("/admin/setuserstatus", nil)
}
var RegexAdminNukeUser = regexp.MustCompile(`^/admin/nukeuser$`)
func BuildAdminNukeUser() string {
defer CatchPanic()
return Url("/admin/nukeuser", nil)
}
/* /*
* Snippets * Snippets
*/ */

View File

@ -47,7 +47,7 @@ func GetFullSubforumTree(ctx context.Context, conn *pgxpool.Pool) SubforumTree {
type subforumRow struct { type subforumRow struct {
Subforum Subforum `db:"sf"` Subforum Subforum `db:"sf"`
} }
rows, err := db.Query(ctx, conn, subforumRow{}, rowsSlice, err := db.Query(ctx, conn, subforumRow{},
` `
SELECT $columns SELECT $columns
FROM FROM
@ -59,7 +59,6 @@ func GetFullSubforumTree(ctx context.Context, conn *pgxpool.Pool) SubforumTree {
panic(oops.New(err, "failed to fetch subforum tree")) panic(oops.New(err, "failed to fetch subforum tree"))
} }
rowsSlice := rows.ToSlice()
sfTreeMap := make(map[int]*SubforumTreeNode, len(rowsSlice)) sfTreeMap := make(map[int]*SubforumTreeNode, len(rowsSlice))
for _, row := range rowsSlice { for _, row := range rowsSlice {
sf := row.(*subforumRow).Subforum sf := row.(*subforumRow).Subforum

View File

@ -193,6 +193,7 @@ func UserToTemplate(u *models.User, currentTheme string) User {
Username: u.Username, Username: u.Username,
Email: email, Email: email,
IsStaff: u.IsStaff, IsStaff: u.IsStaff,
Status: int(u.Status),
Name: u.BestName(), Name: u.BestName(),
Bio: u.Bio, Bio: u.Bio,

View File

@ -1,5 +1,32 @@
{{ template "base.html" . }} {{ template "base.html" . }}
{{ define "extrahead" }}
<style>
.led {
aspect-ratio: 1;
border-radius: 50%;
border-style: solid;
border-width: 1.5px;
display: inline-block;
}
.led.yellow {
background-color: #64501f;
border-color: #4f3700;
}
.led.yellow.on {
background-color: #fdf2d8;
border-color: #f9ad04;
box-shadow: 0 0 7px #ee9e06;
}
.admin .cover {
background: repeating-linear-gradient( -45deg, #ff6c00, #ff6c00 12px, #000000 5px, #000000 25px );
}
</style>
{{ end }}
{{ define "content" }} {{ define "content" }}
<div class="flex flex-column flex-row-l"> <div class="flex flex-column flex-row-l">
<div class=" <div class="
@ -34,6 +61,68 @@
</div> </div>
</div> </div>
</div> </div>
{{ if .User }}
{{ if .User.IsStaff }}
<div class="mt3 mt0-ns mt3-l ml3-ns ml0-l flex flex-column items-start bg--card pa2 br2 admin">
<div class="flex flex-row w-100 items-center">
<b class="flex-grow-1">Admin actions</b>
<div class="led yellow" style="height: 12px; margin: 3px;"></div>
<a href="javascript:;" class="unlock">Unlock</a>
</div>
<div class="relative w-100">
<div class="bg--card cover absolute w-100 h-100 br2"></div>
<div class="mt3">
<div>User status:</div>
<form id="admin_set_status_form" method="POST" action="{{ .AdminSetStatusUrl }}">
{{ csrftoken .Session }}
<input type="hidden" name="user_id" value="{{ .ProfileUser.ID }}" />
<input type="hidden" name="username" value="{{ .ProfileUser.Username }}" />
<select name="status">
<option value="inactive" {{ if eq .ProfileUser.Status 1 }}selected{{ end }}>Brand new</option>
<option value="confirmed" {{ if eq .ProfileUser.Status 2 }}selected{{ end }}>Email confirmed</option>
<option value="approved" {{ if eq .ProfileUser.Status 3 }}selected{{ end }}>Admin approved</option>
<option value="banned" {{ if eq .ProfileUser.Status 4 }}selected{{ end }}>Banned</option>
</select>
<input type="submit" value="Set" />
<div class="c--dim f7">Only sets status. Doesn't delete anything.</div>
</form>
</div>
<div class="mt3">
<div>Danger zone:</div>
<form id="admin_nuke_form" method="POST" action="{{ .AdminNukeUrl }}">
{{ csrftoken .Session }}
<input type="hidden" name="user_id" value="{{ .ProfileUser.ID }}" />
<input type="hidden" name="username" value="{{ .ProfileUser.Username }}" />
<input type="submit" value="Nuke posts" />
</form>
</div>
</div>
<script>
let unlockEl = document.querySelector(".admin .unlock");
let adminUnlockLed = document.querySelector(".admin .led");
let adminUnlocked = false;
let panelEl = document.querySelector(".admin .cover");
unlockEl.addEventListener("click", function() {
adminUnlocked = true;
adminUnlockLed.classList.add("on");
panelEl.style.display = "none";
});
document.querySelector("#admin_set_status_form").addEventListener("submit", function(ev) {
if (!adminUnlocked) {
ev.preventDefault();
}
});
document.querySelector("#admin_nuke_form").addEventListener("submit", function(ev) {
if (!adminUnlocked) {
ev.preventDefault();
}
});
</script>
</div>
{{ end }}
{{ end }}
</div> </div>
<div class="flex-grow-1 overflow-hidden"> <div class="flex-grow-1 overflow-hidden">
{{ if or .OwnProfile .ProfileUserProjects }} {{ if or .OwnProfile .ProfileUserProjects }}

View File

@ -157,6 +157,7 @@ type User struct {
Username string Username string
Email string Email string
IsStaff bool IsStaff bool
Status int
Name string Name string
Blurb string Blurb string

View File

@ -172,7 +172,7 @@ func AdminApprovalQueueSubmit(c *RequestContext) ResponseData {
if errors.Is(err, db.NotFound) { if errors.Is(err, db.NotFound) {
return RejectRequest(c, "User not found") return RejectRequest(c, "User not found")
} else { } else {
return c.ErrorResponse(http.StatusBadRequest, oops.New(err, "failed to fetch user")) return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch user"))
} }
} }
user := u.(*models.User) user := u.(*models.User)
@ -189,7 +189,7 @@ func AdminApprovalQueueSubmit(c *RequestContext) ResponseData {
user.ID, user.ID,
) )
if err != nil { if err != nil {
return c.ErrorResponse(http.StatusBadRequest, oops.New(err, "failed to set user to approved")) return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to set user to approved"))
} }
whatHappened = fmt.Sprintf("%s approved successfully", user.Username) whatHappened = fmt.Sprintf("%s approved successfully", user.Username)
} else if action == ApprovalQueueActionSpammer { } else if action == ApprovalQueueActionSpammer {
@ -203,13 +203,16 @@ func AdminApprovalQueueSubmit(c *RequestContext) ResponseData {
user.ID, user.ID,
) )
if err != nil { if err != nil {
return c.ErrorResponse(http.StatusBadRequest, oops.New(err, "failed to set user to banned")) return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to set user to banned"))
} }
err = auth.DeleteSessionForUser(c.Context(), c.Conn, user.Username) err = auth.DeleteSessionForUser(c.Context(), c.Conn, user.Username)
if err != nil { if err != nil {
return c.ErrorResponse(http.StatusBadRequest, oops.New(err, "failed to log out user")) return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to log out user"))
} }
err = deleteAllPostsForUser(c.Context(), c.Conn, user.ID) err = deleteAllPostsForUser(c.Context(), c.Conn, user.ID)
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete spammer's posts"))
}
whatHappened = fmt.Sprintf("%s banned successfully", user.Username) whatHappened = fmt.Sprintf("%s banned successfully", user.Username)
} else { } else {
whatHappened = fmt.Sprintf("Unrecognized action: %s", action) whatHappened = fmt.Sprintf("Unrecognized action: %s", action)
@ -240,16 +243,17 @@ func fetchUnapprovedPosts(c *RequestContext) ([]*UnapprovedPost, error) {
JOIN auth_user AS author ON author.id = post.author_id JOIN auth_user AS author ON author.id = post.author_id
WHERE WHERE
NOT thread.deleted NOT thread.deleted
AND author.status = $1 AND NOT post.deleted
AND author.status = ANY($1)
ORDER BY post.postdate DESC ORDER BY post.postdate DESC
`, `,
models.UserStatusConfirmed, []models.UserStatus{models.UserStatusConfirmed},
) )
if err != nil { if err != nil {
return nil, oops.New(err, "failed to fetch unapproved posts") return nil, oops.New(err, "failed to fetch unapproved posts")
} }
var res []*UnapprovedPost var res []*UnapprovedPost
for _, iresult := range it.ToSlice() { for _, iresult := range it {
res = append(res, iresult.(*UnapprovedPost)) res = append(res, iresult.(*UnapprovedPost))
} }
return res, nil return res, nil
@ -281,7 +285,7 @@ func deleteAllPostsForUser(ctx context.Context, conn *pgxpool.Pool, userId int)
return oops.New(err, "failed to fetch posts to delete for user") return oops.New(err, "failed to fetch posts to delete for user")
} }
for _, iResult := range it.ToSlice() { for _, iResult := range it {
row := iResult.(*toDelete) row := iResult.(*toDelete)
hmndata.DeletePost(ctx, tx, row.ThreadID, row.PostID) hmndata.DeletePost(ctx, tx, row.ThreadID, row.PostID)
} }

View File

@ -157,7 +157,7 @@ func DiscordShowcaseBacklog(c *RequestContext) ResponseData {
type messageIdQuery struct { type messageIdQuery struct {
MessageID string `db:"msg.id"` MessageID string `db:"msg.id"`
} }
itMsgIds, err := db.Query(c.Context(), c.Conn, messageIdQuery{}, iMsgIDs, err := db.Query(c.Context(), c.Conn, messageIdQuery{},
` `
SELECT $columns SELECT $columns
FROM FROM
@ -169,7 +169,9 @@ func DiscordShowcaseBacklog(c *RequestContext) ResponseData {
duser.UserID, duser.UserID,
config.Config.Discord.ShowcaseChannelID, config.Config.Discord.ShowcaseChannelID,
) )
iMsgIDs := itMsgIds.ToSlice() if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, err)
}
var msgIDs []string var msgIDs []string
for _, imsgId := range iMsgIDs { for _, imsgId := range iMsgIDs {

View File

@ -573,7 +573,7 @@ func FetchPodcast(c *RequestContext, projectId int, fetchEpisodes bool, episodeG
if err != nil { if err != nil {
return result, oops.New(err, "failed to fetch podcast episodes") return result, oops.New(err, "failed to fetch podcast episodes")
} }
for _, episodeRow := range podcastEpisodeQueryResult.ToSlice() { for _, episodeRow := range podcastEpisodeQueryResult {
result.Episodes = append(result.Episodes, &episodeRow.(*podcastEpisodeQuery).Episode) result.Episodes = append(result.Episodes, &episodeRow.(*podcastEpisodeQuery).Episode)
} }
} else { } else {

View File

@ -318,15 +318,15 @@ func ProjectHomepage(c *RequestContext) ResponseData {
} }
} }
for _, screenshot := range screenshotQueryResult.ToSlice() { for _, screenshot := range screenshotQueryResult {
templateData.Screenshots = append(templateData.Screenshots, hmnurl.BuildUserFile(screenshot.(*screenshotQuery).Filename)) templateData.Screenshots = append(templateData.Screenshots, hmnurl.BuildUserFile(screenshot.(*screenshotQuery).Filename))
} }
for _, link := range projectLinkResult.ToSlice() { for _, link := range projectLinkResult {
templateData.ProjectLinks = append(templateData.ProjectLinks, templates.LinkToTemplate(&link.(*projectLinkQuery).Link)) templateData.ProjectLinks = append(templateData.ProjectLinks, templates.LinkToTemplate(&link.(*projectLinkQuery).Link))
} }
for _, post := range postQueryResult.ToSlice() { for _, post := range postQueryResult {
templateData.RecentActivity = append(templateData.RecentActivity, PostToTimelineItem( templateData.RecentActivity = append(templateData.RecentActivity, PostToTimelineItem(
c.UrlContext, c.UrlContext,
lineageBuilder, lineageBuilder,
@ -796,7 +796,7 @@ func updateProject(ctx context.Context, tx pgx.Tx, user *models.User, payload *P
} }
} }
ownerResult, err := db.Query(ctx, tx, models.User{}, ownerRows, err := db.Query(ctx, tx, models.User{},
` `
SELECT $columns SELECT $columns
FROM auth_user FROM auth_user
@ -807,7 +807,6 @@ func updateProject(ctx context.Context, tx pgx.Tx, user *models.User, payload *P
if err != nil { if err != nil {
return oops.New(err, "Failed to query users") return oops.New(err, "Failed to query users")
} }
ownerRows := ownerResult.ToSlice()
_, err = tx.Exec(ctx, _, err = tx.Exec(ctx,
` `

View File

@ -190,6 +190,8 @@ func NewWebsiteRoutes(longRequestContext context.Context, conn *pgxpool.Pool) ht
hmnOnly.GET(hmnurl.RegexAdminAtomFeed, AdminAtomFeed) hmnOnly.GET(hmnurl.RegexAdminAtomFeed, AdminAtomFeed)
hmnOnly.GET(hmnurl.RegexAdminApprovalQueue, adminMiddleware(AdminApprovalQueue)) hmnOnly.GET(hmnurl.RegexAdminApprovalQueue, adminMiddleware(AdminApprovalQueue))
hmnOnly.POST(hmnurl.RegexAdminApprovalQueue, adminMiddleware(csrfMiddleware(AdminApprovalQueueSubmit))) hmnOnly.POST(hmnurl.RegexAdminApprovalQueue, adminMiddleware(csrfMiddleware(AdminApprovalQueueSubmit)))
hmnOnly.POST(hmnurl.RegexAdminSetUserStatus, adminMiddleware(csrfMiddleware(UserProfileAdminSetStatus)))
hmnOnly.POST(hmnurl.RegexAdminNukeUser, adminMiddleware(csrfMiddleware(UserProfileAdminNuke)))
hmnOnly.GET(hmnurl.RegexFeed, Feed) hmnOnly.GET(hmnurl.RegexFeed, Feed)
hmnOnly.GET(hmnurl.RegexAtomFeed, AtomFeed) hmnOnly.GET(hmnurl.RegexAtomFeed, AtomFeed)

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"sort" "sort"
"strconv"
"strings" "strings"
"time" "time"
@ -32,6 +33,9 @@ type UserProfileTemplateData struct {
CanAddProject bool CanAddProject bool
NewProjectUrl string NewProjectUrl string
AdminSetStatusUrl string
AdminNukeUrl string
} }
func UserProfile(c *RequestContext) ResponseData { func UserProfile(c *RequestContext) ResponseData {
@ -81,7 +85,7 @@ func UserProfile(c *RequestContext) ResponseData {
type userLinkQuery struct { type userLinkQuery struct {
UserLink models.Link `db:"link"` UserLink models.Link `db:"link"`
} }
userLinkQueryResult, err := db.Query(c.Context(), c.Conn, userLinkQuery{}, userLinksSlice, err := db.Query(c.Context(), c.Conn, userLinkQuery{},
` `
SELECT $columns SELECT $columns
FROM FROM
@ -95,7 +99,6 @@ func UserProfile(c *RequestContext) ResponseData {
if err != nil { if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch links for user: %s", username)) return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch links for user: %s", username))
} }
userLinksSlice := userLinkQueryResult.ToSlice()
profileUserLinks := make([]templates.Link, 0, len(userLinksSlice)) profileUserLinks := make([]templates.Link, 0, len(userLinksSlice))
for _, l := range userLinksSlice { for _, l := range userLinksSlice {
profileUserLinks = append(profileUserLinks, templates.LinkToTemplate(&l.(*userLinkQuery).UserLink)) profileUserLinks = append(profileUserLinks, templates.LinkToTemplate(&l.(*userLinkQuery).UserLink))
@ -191,6 +194,9 @@ func UserProfile(c *RequestContext) ResponseData {
CanAddProject: numPersonalProjects < maxPersonalProjects, CanAddProject: numPersonalProjects < maxPersonalProjects,
NewProjectUrl: hmnurl.BuildProjectNew(), NewProjectUrl: hmnurl.BuildProjectNew(),
AdminSetStatusUrl: hmnurl.BuildAdminSetUserStatus(),
AdminNukeUrl: hmnurl.BuildAdminNukeUser(),
}, c.Perf) }, c.Perf)
return res return res
} }
@ -216,7 +222,7 @@ func UserSettings(c *RequestContext) ResponseData {
DiscordShowcaseBacklogUrl string DiscordShowcaseBacklogUrl string
} }
ilinks, err := db.Query(c.Context(), c.Conn, models.Link{}, links, err := db.Query(c.Context(), c.Conn, models.Link{},
` `
SELECT $columns SELECT $columns
FROM handmade_links FROM handmade_links
@ -228,7 +234,6 @@ func UserSettings(c *RequestContext) ResponseData {
if err != nil { if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch user links")) return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch user links"))
} }
links := ilinks.ToSlice()
linksText := "" linksText := ""
for _, ilink := range links { for _, ilink := range links {
@ -440,6 +445,64 @@ func UserSettingsSave(c *RequestContext) ResponseData {
return res return res
} }
func UserProfileAdminSetStatus(c *RequestContext) ResponseData {
c.Req.ParseForm()
userIdStr := c.Req.Form.Get("user_id")
userId, err := strconv.Atoi(userIdStr)
if err != nil {
return RejectRequest(c, "No user id provided")
}
status := c.Req.Form.Get("status")
var desiredStatus models.UserStatus
switch status {
case "inactive":
desiredStatus = models.UserStatusInactive
case "confirmed":
desiredStatus = models.UserStatusConfirmed
case "approved":
desiredStatus = models.UserStatusApproved
case "banned":
desiredStatus = models.UserStatusBanned
default:
return RejectRequest(c, "No legal user status provided")
}
_, err = c.Conn.Exec(c.Context(),
`
UPDATE auth_user
SET status = $1
WHERE id = $2
`,
desiredStatus,
userId,
)
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to update user status"))
}
res := c.Redirect(hmnurl.BuildUserProfile(c.Req.Form.Get("username")), http.StatusSeeOther)
res.AddFutureNotice("success", "Successfully set status")
return res
}
func UserProfileAdminNuke(c *RequestContext) ResponseData {
c.Req.ParseForm()
userIdStr := c.Req.Form.Get("user_id")
userId, err := strconv.Atoi(userIdStr)
if err != nil {
return RejectRequest(c, "No user id provided")
}
err = deleteAllPostsForUser(c.Context(), c.Conn, userId)
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete user posts"))
}
res := c.Redirect(hmnurl.BuildUserProfile(c.Req.Form.Get("username")), http.StatusSeeOther)
res.AddFutureNotice("success", "Successfully nuked user")
return res
}
func updatePassword(c *RequestContext, tx pgx.Tx, old, new, confirm string) *ResponseData { func updatePassword(c *RequestContext, tx pgx.Tx, old, new, confirm string) *ResponseData {
if new != confirm { if new != confirm {
res := RejectRequest(c, "Your password and password confirmation did not match.") res := RejectRequest(c, "Your password and password confirmation did not match.")