Merge remote-tracking branch 'origin/master' into beta
This commit is contained in:
commit
6307589ee4
13
src/db/db.go
13
src/db/db.go
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -157,6 +157,7 @@ type User struct {
|
|||
Username string
|
||||
Email string
|
||||
IsStaff bool
|
||||
Status int
|
||||
|
||||
Name string
|
||||
Blurb string
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
`
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.")
|
||||
|
|
Loading…
Reference in New Issue