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
}
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)
columnNames, fieldPaths, err := getColumnNamesAndPaths(destType, nil, "")
if err != nil {
@ -347,7 +356,7 @@ result but find nothing.
var NotFound = errors.New("not found")
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 {
return nil, err
}

View File

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

View File

@ -64,7 +64,7 @@ func fetchMissingContent(ctx context.Context, dbConn *pgxpool.Pool) {
type query struct {
Message models.DiscordMessage `db:"msg"`
}
result, err := db.Query(ctx, dbConn, query{},
imessagesWithoutContent, err := db.Query(ctx, dbConn, query{},
`
SELECT $columns
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")
return
}
imessagesWithoutContent := result.ToSlice()
if len(imessagesWithoutContent) > 0 {
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 {
Tag models.Tag `db:"tags"`
}
itUserTags, err := db.Query(ctx, tx, tagsRow{},
iUserTags, err := db.Query(ctx, tx, tagsRow{},
`
SELECT $columns
FROM
@ -764,7 +764,6 @@ func UpdateSnippetTagsIfAny(ctx context.Context, dbConn db.ConnOrTx, msg *Messag
if err != nil {
return oops.New(err, "failed to fetch tags for user projects")
}
iUserTags := itUserTags.ToSlice()
var tagIDs []int
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) {
// Check attachments
itAttachments, err := db.Query(ctx, tx, models.DiscordMessageAttachment{},
attachments, err := db.Query(ctx, tx, models.DiscordMessageAttachment{},
`
SELECT $columns
FROM handmade_discordmessageattachment
@ -816,14 +815,13 @@ func getSnippetAssetOrUrl(ctx context.Context, tx db.ConnOrTx, msg *models.Disco
if err != nil {
return nil, nil, oops.New(err, "failed to fetch message attachments")
}
attachments := itAttachments.ToSlice()
for _, iattachment := range attachments {
attachment := iattachment.(*models.DiscordMessageAttachment)
return &attachment.AssetID, nil, nil
}
// Check embeds
itEmbeds, err := db.Query(ctx, tx, models.DiscordMessageEmbed{},
embeds, err := db.Query(ctx, tx, models.DiscordMessageEmbed{},
`
SELECT $columns
FROM handmade_discordmessageembed
@ -834,7 +832,6 @@ func getSnippetAssetOrUrl(ctx context.Context, tx db.ConnOrTx, msg *models.Disco
if err != nil {
return nil, nil, oops.New(err, "failed to fetch discord embeds")
}
embeds := itEmbeds.ToSlice()
for _, iembed := range embeds {
embed := iembed.(*models.DiscordMessageEmbed)
if embed.VideoID != nil {

View File

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

View File

@ -47,7 +47,7 @@ func FetchSnippets(
type snippetIDRow struct {
SnippetID int `db:"snippet_id"`
}
itSnippetIDs, err := db.Query(ctx, tx, snippetIDRow{},
iSnippetIDs, err := db.Query(ctx, tx, snippetIDRow{},
`
SELECT DISTINCT snippet_id
FROM
@ -61,7 +61,6 @@ func FetchSnippets(
if err != nil {
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
if len(iSnippetIDs) == 0 {
@ -125,11 +124,10 @@ func FetchSnippets(
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 {
return nil, oops.New(err, "failed to fetch threads")
}
iresults := it.ToSlice()
result := make([]SnippetAndStuff, len(iresults)) // allocate extra space because why not
snippetIDs := make([]int, len(iresults))
@ -151,7 +149,7 @@ func FetchSnippets(
SnippetID int `db:"snippet_tags.snippet_id"`
Tag *models.Tag `db:"tags"`
}
itSnippetTags, err := db.Query(ctx, tx, snippetTagRow{},
iSnippetTags, err := db.Query(ctx, tx, snippetTagRow{},
`
SELECT $columns
FROM
@ -165,7 +163,6 @@ func FetchSnippets(
if err != nil {
return nil, oops.New(err, "failed to fetch tags for snippets")
}
iSnippetTags := itSnippetTags.ToSlice()
// associate tags with snippets
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)
}
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 {
return nil, oops.New(err, "failed to fetch tags")
}
itags := it.ToSlice()
res := make([]*models.Tag, len(itags))
for i, itag := range itags {

View File

@ -143,11 +143,10 @@ func FetchThreads(
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 {
return nil, oops.New(err, "failed to fetch threads")
}
iresults := it.ToSlice()
result := make([]ThreadAndStuff, len(iresults))
for i, iresult := range iresults {
@ -398,11 +397,10 @@ func FetchPosts(
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 {
return nil, oops.New(err, "failed to fetch posts")
}
iresults := it.ToSlice()
result := make([]PostAndStuff, len(iresults))
for i, iresult := range iresults {
@ -696,20 +694,28 @@ func DeletePost(
tx pgx.Tx,
threadId, postId int,
) (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
handmade_thread AS thread
WHERE
thread.id = $2
thread.id = $1
`,
postId,
threadId,
)
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 {
// 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{}
for _, asset := range assetResult.ToSlice() {
for _, asset := range assetResult {
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
for _, ipost := range postsIter.ToSlice() {
for _, ipost := range postsIter {
post := ipost.(*models.Post)
if firstPost == nil || post.PostDate.Before(firstPost.PostDate) {

View File

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

View File

@ -216,6 +216,20 @@ func BuildAdminApprovalQueue() string {
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
*/

View File

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

View File

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

View File

@ -1,5 +1,32 @@
{{ 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" }}
<div class="flex flex-column flex-row-l">
<div class="
@ -34,6 +61,68 @@
</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 class="flex-grow-1 overflow-hidden">
{{ if or .OwnProfile .ProfileUserProjects }}

View File

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

View File

@ -172,7 +172,7 @@ func AdminApprovalQueueSubmit(c *RequestContext) ResponseData {
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"))
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch user"))
}
}
user := u.(*models.User)
@ -189,7 +189,7 @@ func AdminApprovalQueueSubmit(c *RequestContext) ResponseData {
user.ID,
)
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)
} else if action == ApprovalQueueActionSpammer {
@ -203,13 +203,16 @@ func AdminApprovalQueueSubmit(c *RequestContext) ResponseData {
user.ID,
)
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)
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)
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)
} else {
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
WHERE
NOT thread.deleted
AND author.status = $1
AND NOT post.deleted
AND author.status = ANY($1)
ORDER BY post.postdate DESC
`,
models.UserStatusConfirmed,
[]models.UserStatus{models.UserStatusConfirmed},
)
if err != nil {
return nil, oops.New(err, "failed to fetch unapproved posts")
}
var res []*UnapprovedPost
for _, iresult := range it.ToSlice() {
for _, iresult := range it {
res = append(res, iresult.(*UnapprovedPost))
}
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")
}
for _, iResult := range it.ToSlice() {
for _, iResult := range it {
row := iResult.(*toDelete)
hmndata.DeletePost(ctx, tx, row.ThreadID, row.PostID)
}

View File

@ -157,7 +157,7 @@ func DiscordShowcaseBacklog(c *RequestContext) ResponseData {
type messageIdQuery struct {
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
FROM
@ -169,7 +169,9 @@ func DiscordShowcaseBacklog(c *RequestContext) ResponseData {
duser.UserID,
config.Config.Discord.ShowcaseChannelID,
)
iMsgIDs := itMsgIds.ToSlice()
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, err)
}
var msgIDs []string
for _, imsgId := range iMsgIDs {

View File

@ -573,7 +573,7 @@ func FetchPodcast(c *RequestContext, projectId int, fetchEpisodes bool, episodeG
if err != nil {
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)
}
} 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))
}
for _, link := range projectLinkResult.ToSlice() {
for _, link := range projectLinkResult {
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(
c.UrlContext,
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
FROM auth_user
@ -807,7 +807,6 @@ func updateProject(ctx context.Context, tx pgx.Tx, user *models.User, payload *P
if err != nil {
return oops.New(err, "Failed to query users")
}
ownerRows := ownerResult.ToSlice()
_, 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.RegexAdminApprovalQueue, adminMiddleware(AdminApprovalQueue))
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.RegexAtomFeed, AtomFeed)

View File

@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"sort"
"strconv"
"strings"
"time"
@ -32,6 +33,9 @@ type UserProfileTemplateData struct {
CanAddProject bool
NewProjectUrl string
AdminSetStatusUrl string
AdminNukeUrl string
}
func UserProfile(c *RequestContext) ResponseData {
@ -81,7 +85,7 @@ func UserProfile(c *RequestContext) ResponseData {
type userLinkQuery struct {
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
FROM
@ -95,7 +99,6 @@ func UserProfile(c *RequestContext) ResponseData {
if err != nil {
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))
for _, l := range userLinksSlice {
profileUserLinks = append(profileUserLinks, templates.LinkToTemplate(&l.(*userLinkQuery).UserLink))
@ -191,6 +194,9 @@ func UserProfile(c *RequestContext) ResponseData {
CanAddProject: numPersonalProjects < maxPersonalProjects,
NewProjectUrl: hmnurl.BuildProjectNew(),
AdminSetStatusUrl: hmnurl.BuildAdminSetUserStatus(),
AdminNukeUrl: hmnurl.BuildAdminNukeUser(),
}, c.Perf)
return res
}
@ -216,7 +222,7 @@ func UserSettings(c *RequestContext) ResponseData {
DiscordShowcaseBacklogUrl string
}
ilinks, err := db.Query(c.Context(), c.Conn, models.Link{},
links, err := db.Query(c.Context(), c.Conn, models.Link{},
`
SELECT $columns
FROM handmade_links
@ -228,7 +234,6 @@ func UserSettings(c *RequestContext) ResponseData {
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch user links"))
}
links := ilinks.ToSlice()
linksText := ""
for _, ilink := range links {
@ -440,6 +445,64 @@ func UserSettingsSave(c *RequestContext) ResponseData {
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 {
if new != confirm {
res := RejectRequest(c, "Your password and password confirmation did not match.")