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
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 }}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
`
|
`
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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.")
|
||||||
|
|
Loading…
Reference in New Issue