Seed users (and rework a lot of user access to use new helpers)

This commit is contained in:
Ben Visness 2022-05-07 13:58:00 -05:00
parent b46f5d8637
commit 3a93aa93e9
22 changed files with 445 additions and 202 deletions

1
go.mod
View File

@ -3,6 +3,7 @@ module git.handmade.network/hmn/hmn
go 1.18
require (
github.com/HandmadeNetwork/golorem v0.0.0-20220507185207-414965a3a817
github.com/Masterminds/sprig v2.22.0+incompatible
github.com/alecthomas/chroma v0.9.2
github.com/aws/aws-sdk-go-v2 v1.8.1

2
go.sum
View File

@ -17,6 +17,8 @@ github.com/HandmadeNetwork/bbcode v0.0.0-20210623031351-ec0e2e2e39d9 h1:5WhEr56C
github.com/HandmadeNetwork/bbcode v0.0.0-20210623031351-ec0e2e2e39d9/go.mod h1:vMiNHD8absjmnO60Do5KCaJBwdbaiI/AzhMmSipMme4=
github.com/HandmadeNetwork/goldmark v1.4.1-0.20210707024600-f7e596e26b5e h1:z0GlF2OMmy852mrcMVpjZIzEHYCbUweS8RaWRCPfL1g=
github.com/HandmadeNetwork/goldmark v1.4.1-0.20210707024600-f7e596e26b5e/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/HandmadeNetwork/golorem v0.0.0-20220507185207-414965a3a817 h1:cBqVP/sLiK7DPay7Aac1PRUwu3fCVyL5Wc+xLXzqwkE=
github.com/HandmadeNetwork/golorem v0.0.0-20220507185207-414965a3a817/go.mod h1:doKbGBIdiM1nkEfvAeP5hvUmERah9H6StTVfCverqdE=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=

View File

@ -37,8 +37,8 @@ func addCreateProjectCommand(projectCommand *cobra.Command) {
descParsed := parsing.ParseMarkdown(description, parsing.ForumRealMarkdown)
ctx := context.Background()
conn := db.NewConnPool(1, 1)
defer conn.Close()
conn := db.NewConn()
defer conn.Close(ctx)
tx, err := conn.Begin(ctx)
if err != nil {
@ -160,8 +160,8 @@ func addProjectTagCommand(projectCommand *cobra.Command) {
tag, _ := cmd.Flags().GetString("tag")
ctx := context.Background()
conn := db.NewConnPool(1, 1)
defer conn.Close()
conn := db.NewConn()
defer conn.Close(ctx)
resultTag, err := hmndata.SetProjectTag(ctx, conn, nil, projectID, tag)
if err != nil {

View File

@ -42,8 +42,8 @@ func init() {
password := args[1]
ctx := context.Background()
conn := db.NewConnPool(1, 1)
defer conn.Close()
conn := db.NewConn()
defer conn.Close(ctx)
row := conn.QueryRow(ctx, "SELECT id, username FROM hmn_user WHERE lower(username) = lower($1)", username)
var id int
@ -83,8 +83,8 @@ func init() {
username := args[0]
ctx := context.Background()
conn := db.NewConnPool(1, 1)
defer conn.Close()
conn := db.NewConn()
defer conn.Close(ctx)
res, err := conn.Exec(ctx, "UPDATE hmn_user SET status = $1 WHERE LOWER(username) = LOWER($2);", models.UserStatusConfirmed, username)
if err != nil {
@ -138,8 +138,8 @@ func init() {
}
ctx := context.Background()
conn := db.NewConnPool(1, 1)
defer conn.Close()
conn := db.NewConn()
defer conn.Close(ctx)
res, err := conn.Exec(ctx, "UPDATE hmn_user SET status = $1 WHERE LOWER(username) = LOWER($2);", status, username)
if err != nil {
@ -201,8 +201,8 @@ func init() {
projectSlug, _ := cmd.Flags().GetString("project_slug")
ctx := context.Background()
conn := db.NewConnPool(1, 1)
defer conn.Close()
conn := db.NewConn()
defer conn.Close(ctx)
tx, err := conn.Begin(ctx)
if err != nil {
@ -280,8 +280,8 @@ func init() {
subforumSlug, _ := cmd.Flags().GetString("subforum_slug")
ctx := context.Background()
conn := db.NewConnPool(1, 1)
defer conn.Close()
conn := db.NewConn()
defer conn.Close(ctx)
tx, err := conn.Begin(ctx)
if err != nil {

View File

@ -190,6 +190,11 @@ func UpdatePassword(ctx context.Context, conn db.ConnOrTx, username string, hp H
return nil
}
func SetPassword(ctx context.Context, conn db.ConnOrTx, username string, password string) error {
hp := HashPassword(password)
return UpdatePassword(ctx, conn, username, hp)
}
func DeleteInactiveUsers(ctx context.Context, conn *pgxpool.Pool) (int64, error) {
tag, err := conn.Exec(ctx,
`

View File

@ -11,6 +11,7 @@ import (
"git.handmade.network/hmn/hmn/src/config"
"git.handmade.network/hmn/hmn/src/logging"
"git.handmade.network/hmn/hmn/src/oops"
"git.handmade.network/hmn/hmn/src/utils"
"github.com/google/uuid"
"github.com/jackc/pgconn"
"github.com/jackc/pgtype"
@ -45,7 +46,18 @@ var connInfo = pgtype.NewConnInfo()
// Creates a new connection to the HMN database.
// This connection is not safe for concurrent use.
func NewConn() *pgx.Conn {
conn, err := pgx.Connect(context.Background(), config.Config.Postgres.DSN())
return NewConnWithConfig(config.PostgresConfig{})
}
func NewConnWithConfig(cfg config.PostgresConfig) *pgx.Conn {
cfg = overrideDefaultConfig(cfg)
pgcfg, err := pgx.ParseConfig(cfg.DSN())
pgcfg.Logger = zerologadapter.NewLogger(log.Logger)
pgcfg.LogLevel = cfg.LogLevel
conn, err := pgx.ConnectConfig(context.Background(), pgcfg)
if err != nil {
panic(oops.New(err, "failed to connect to database"))
}
@ -55,15 +67,21 @@ func NewConn() *pgx.Conn {
// Creates a connection pool for the HMN database.
// The resulting pool is safe for concurrent use.
func NewConnPool(minConns, maxConns int32) *pgxpool.Pool {
cfg, err := pgxpool.ParseConfig(config.Config.Postgres.DSN())
func NewConnPool() *pgxpool.Pool {
return NewConnPoolWithConfig(config.PostgresConfig{})
}
cfg.MinConns = minConns
cfg.MaxConns = maxConns
cfg.ConnConfig.Logger = zerologadapter.NewLogger(log.Logger)
cfg.ConnConfig.LogLevel = config.Config.Postgres.LogLevel
func NewConnPoolWithConfig(cfg config.PostgresConfig) *pgxpool.Pool {
cfg = overrideDefaultConfig(cfg)
conn, err := pgxpool.ConnectConfig(context.Background(), cfg)
pgcfg, err := pgxpool.ParseConfig(cfg.DSN())
pgcfg.MinConns = cfg.MinConn
pgcfg.MaxConns = cfg.MaxConn
pgcfg.ConnConfig.Logger = zerologadapter.NewLogger(log.Logger)
pgcfg.ConnConfig.LogLevel = cfg.LogLevel
conn, err := pgxpool.ConnectConfig(context.Background(), pgcfg)
if err != nil {
panic(oops.New(err, "failed to create database connection pool"))
}
@ -71,6 +89,19 @@ func NewConnPool(minConns, maxConns int32) *pgxpool.Pool {
return conn
}
func overrideDefaultConfig(cfg config.PostgresConfig) config.PostgresConfig {
return config.PostgresConfig{
User: utils.OrDefault(cfg.User, config.Config.Postgres.User),
Password: utils.OrDefault(cfg.Password, config.Config.Postgres.Password),
Hostname: utils.OrDefault(cfg.Hostname, config.Config.Postgres.Hostname),
Port: utils.OrDefault(cfg.Port, config.Config.Postgres.Port),
DbName: utils.OrDefault(cfg.DbName, config.Config.Postgres.DbName),
LogLevel: utils.OrDefault(cfg.LogLevel, config.Config.Postgres.LogLevel),
MinConn: utils.OrDefault(cfg.MinConn, config.Config.Postgres.MinConn),
MaxConn: utils.OrDefault(cfg.MaxConn, config.Config.Postgres.MaxConn),
}
}
/*
Performs a SQL query and returns a slice of all the result rows. The query is just plain SQL, but make sure to read the package documentation for details. You must explicitly provide the type argument - this is how it knows what Go type to map the results to, and it cannot be inferred.

View File

@ -27,7 +27,7 @@ func init() {
Long: "Scrape the entire history of Discord channels, saving message content (but not creating snippets)",
Run: func(cmd *cobra.Command, args []string) {
ctx := context.Background()
conn := db.NewConnPool(1, 1)
conn := db.NewConnPool()
defer conn.Close()
for _, channelID := range args {
@ -47,8 +47,8 @@ func init() {
os.Exit(1)
}
ctx := context.Background()
conn := db.NewConnPool(1, 1)
defer conn.Close()
conn := db.NewConn()
defer conn.Close(ctx)
chanID := args[0]

View File

@ -96,11 +96,12 @@ func (bot *botInstance) handleProfileCommand(ctx context.Context, i *Interaction
FROM
discord_user AS duser
JOIN hmn_user ON duser.hmn_user_id = hmn_user.id
LEFT JOIN asset AS hmn_user_avatar ON hmn_user_avatar.id = hmn_user.avatar_asset_id
WHERE
duser.userid = $1
AND hmn_user.status = $2
`,
userID,
models.UserStatusApproved,
)
if err != nil {
if errors.Is(err, db.NotFound) {

View File

@ -150,7 +150,7 @@ func FetchProjects(
for i, p := range projectRows {
projectIds[i] = p.Project.ID
}
projectOwners, err := FetchMultipleProjectsOwners(ctx, tx, projectIds)
projectOwners, err := FetchMultipleProjectsOwners(ctx, tx, currentUser, projectIds)
if err != nil {
return nil, err
}
@ -316,6 +316,7 @@ type ProjectOwners struct {
func FetchMultipleProjectsOwners(
ctx context.Context,
dbConn db.ConnOrTx,
currentUser *models.User,
projectIds []int,
) ([]ProjectOwners, error) {
perf := perf.ExtractPerf(ctx)
@ -358,17 +359,9 @@ func FetchMultipleProjectsOwners(
userIds = append(userIds, userProject.UserID)
}
}
users, err := db.Query[models.User](ctx, tx,
`
SELECT $columns{hmn_user}
FROM
hmn_user
LEFT JOIN asset AS hmn_user_avatar ON hmn_user_avatar.id = hmn_user.avatar_asset_id
WHERE
hmn_user.id = ANY($1)
`,
userIds,
)
users, err := FetchUsers(ctx, tx, currentUser, UsersQuery{
UserIDs: userIds,
})
if err != nil {
return nil, oops.New(err, "failed to fetch users for projects")
}
@ -415,13 +408,14 @@ func FetchMultipleProjectsOwners(
func FetchProjectOwners(
ctx context.Context,
dbConn db.ConnOrTx,
currentUser *models.User,
projectId int,
) ([]*models.User, error) {
perf := perf.ExtractPerf(ctx)
perf.StartBlock("SQL", "Fetch owners for project")
defer perf.EndBlock()
projectOwners, err := FetchMultipleProjectsOwners(ctx, dbConn, []int{projectId})
projectOwners, err := FetchMultipleProjectsOwners(ctx, dbConn, currentUser, []int{projectId})
if err != nil {
return nil, err
}

154
src/hmndata/user_helper.go Normal file
View File

@ -0,0 +1,154 @@
package hmndata
import (
"context"
"strings"
"git.handmade.network/hmn/hmn/src/db"
"git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/oops"
"git.handmade.network/hmn/hmn/src/perf"
)
type UsersQuery struct {
// Ignored when using FetchUser
UserIDs []int // if empty, all users
Usernames []string // if empty, all users
// Flags to modify behavior
AnyStatus bool // Bypasses shadowban system
}
/*
Fetches users and related models from the database according to all the given
query params. For the most correct results, provide as much information as you have
on hand.
*/
func FetchUsers(
ctx context.Context,
dbConn db.ConnOrTx,
currentUser *models.User,
q UsersQuery,
) ([]*models.User, error) {
perf := perf.ExtractPerf(ctx)
perf.StartBlock("SQL", "Fetch users")
defer perf.EndBlock()
var currentUserID *int
if currentUser != nil {
currentUserID = &currentUser.ID
}
for i := range q.Usernames {
q.Usernames[i] = strings.ToLower(q.Usernames[i])
}
type userRow struct {
User models.User `db:"hmn_user"`
AvatarAsset *models.Asset `db:"avatar"`
}
var qb db.QueryBuilder
qb.Add(`
SELECT $columns
FROM
hmn_user
LEFT JOIN asset AS avatar ON avatar.id = hmn_user.avatar_asset_id
WHERE
TRUE
`)
if len(q.UserIDs) > 0 {
qb.Add(`AND hmn_user.id = ANY($?)`, q.UserIDs)
}
if len(q.Usernames) > 0 {
qb.Add(`AND LOWER(hmn_user.username) = ANY($?)`, q.Usernames)
}
if !q.AnyStatus {
if currentUser == nil {
qb.Add(`AND hmn_user.status = $?`, models.UserStatusApproved)
} else if !currentUser.IsStaff {
qb.Add(
`
AND (
hmn_user.status = $? -- user is Approved
OR hmn_user.id = $? -- getting self
)
`,
models.UserStatusApproved,
currentUserID,
)
}
}
userRows, err := db.Query[userRow](ctx, dbConn, qb.String(), qb.Args()...)
if err != nil {
return nil, oops.New(err, "failed to fetch users")
}
result := make([]*models.User, len(userRows))
for i, row := range userRows {
user := row.User
user.AvatarAsset = row.AvatarAsset
result[i] = &user
}
return result, nil
}
/*
Fetches a single user and related data. A wrapper around FetchUsers.
As with FetchUsers, provide as much information as you know to get the
most correct results.
Returns db.NotFound if no result is found.
*/
func FetchUser(
ctx context.Context,
dbConn db.ConnOrTx,
currentUser *models.User,
userID int,
q UsersQuery,
) (*models.User, error) {
q.UserIDs = []int{userID}
res, err := FetchUsers(ctx, dbConn, currentUser, q)
if err != nil {
return nil, oops.New(err, "failed to fetch user")
}
if len(res) == 0 {
return nil, db.NotFound
}
return res[0], nil
}
/*
Fetches a single user and related data. A wrapper around FetchUsers.
As with FetchUsers, provide as much information as you know to get the
most correct results.
Returns db.NotFound if no result is found.
*/
func FetchUserByUsername(
ctx context.Context,
dbConn db.ConnOrTx,
currentUser *models.User,
username string,
q UsersQuery,
) (*models.User, error) {
q.Usernames = []string{username}
res, err := FetchUsers(ctx, dbConn, currentUser, q)
if err != nil {
return nil, oops.New(err, "failed to fetch user")
}
if len(res) == 0 {
return nil, db.NotFound
}
return res[0], nil
}
// NOTE(ben): Someday we can add CountUsers...I don't have a need for it right now.

View File

@ -5,7 +5,6 @@ import (
_ "embed"
"fmt"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
@ -15,7 +14,6 @@ import (
"git.handmade.network/hmn/hmn/src/db"
"git.handmade.network/hmn/hmn/src/migration/migrations"
"git.handmade.network/hmn/hmn/src/migration/types"
"git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/website"
"github.com/jackc/pgconn"
"github.com/jackc/pgx/v4"
@ -162,7 +160,9 @@ func LatestVersion() types.MigrationVersion {
func Migrate(targetVersion types.MigrationVersion) {
ctx := context.Background() // In the future, this could actually do something cool.
conn := db.NewConn()
conn := db.NewConnWithConfig(config.PostgresConfig{
LogLevel: pgx.LogLevelWarn,
})
defer conn.Close(ctx)
// create migration table
@ -368,113 +368,3 @@ func ResetDB() {
}
}
}
// Applies a cloned db to the local db.
// Applies the seed after the migration specified in `afterMigration`.
// NOTE(asaf): The db role specified in the config must have the CREATEDB attribute! `ALTER ROLE hmn WITH CREATEDB;`
func SeedFromFile(seedFile string) {
file, err := os.Open(seedFile)
if err != nil {
panic(fmt.Errorf("couldn't open seed file %s: %w", seedFile, err))
}
file.Close()
fmt.Println("Executing seed...")
cmd := exec.Command("pg_restore",
"--single-transaction",
"--dbname", config.Config.Postgres.DSN(),
seedFile,
)
fmt.Println("Running command:", cmd)
if output, err := cmd.CombinedOutput(); err != nil {
fmt.Print(string(output))
panic(fmt.Errorf("failed to execute seed: %w", err))
}
fmt.Println("Done! You may want to migrate forward from here.")
ListMigrations()
}
// NOTE(asaf): This will be useful for open-sourcing the website, but is not yet necessary.
// Creates only what's necessary for a fresh deployment with no data
// TODO(opensource)
func BareMinimumSeed() {
Migrate(LatestVersion())
ctx := context.Background()
conn := db.NewConnPool(1, 1)
defer conn.Close()
tx, err := conn.Begin(ctx)
if err != nil {
panic(err)
}
defer tx.Rollback(ctx)
// Create the HMN project
_, err = tx.Exec(ctx,
`
INSERT INTO project (id, slug, name, blurb, description, personal, lifecycle, color_1, color_2, forum_enabled, blog_enabled, date_created)
VALUES (1, 'hmn', 'Handmade Network', '', '', FALSE, $1, 'ab4c47', 'a5467d', TRUE, TRUE, '2017-01-01T00:00:00Z')
`,
models.ProjectLifecycleActive,
)
if err != nil {
panic(err)
}
// Create the base forum
_, err = tx.Exec(ctx, `
INSERT INTO subforum (id, slug, name, parent_id, project_id)
VALUES (2, '', 'Handmade Network', null, 1)
`)
if err != nil {
panic(err)
}
// Associate the forum with the HMN project
_, err = tx.Exec(ctx, `
UPDATE project SET forum_id = 2 WHERE slug = 'hmn'
`)
if err != nil {
panic(err)
}
err = tx.Commit(ctx)
if err != nil {
panic(err)
}
}
// NOTE(asaf): This will be useful for open-sourcing the website, but is not yet necessary.
// Creates enough data for development
// TODO(opensource)
func SampleSeed() {
BareMinimumSeed()
ctx := context.Background()
conn := db.NewConnPool(1, 1)
defer conn.Close()
tx, err := conn.Begin(ctx)
if err != nil {
panic(err)
}
defer tx.Rollback(ctx)
// admin := CreateAdminUser("admin", "12345678")
// user := CreateUser("regular_user", "12345678")
// hmnProject := CreateProject("hmn", "Handmade Network")
// Create category
// Create thread
// Create accepted user project
// Create pending user project
// Create showcase items
// Create codelanguages
// Create library and library resources
err = tx.Commit(ctx)
if err != nil {
panic(err)
}
}

187
src/migration/seed.go Normal file
View File

@ -0,0 +1,187 @@
package migration
import (
"context"
"fmt"
"math/rand"
"os"
"os/exec"
"git.handmade.network/hmn/hmn/src/auth"
"git.handmade.network/hmn/hmn/src/config"
"git.handmade.network/hmn/hmn/src/db"
"git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/utils"
lorem "github.com/HandmadeNetwork/golorem"
"github.com/jackc/pgx/v4"
)
// Applies a cloned db to the local db.
// Applies the seed after the migration specified in `afterMigration`.
// NOTE(asaf): The db role specified in the config must have the CREATEDB attribute! `ALTER ROLE hmn WITH CREATEDB;`
func SeedFromFile(seedFile string) {
file, err := os.Open(seedFile)
if err != nil {
panic(fmt.Errorf("couldn't open seed file %s: %w", seedFile, err))
}
file.Close()
fmt.Println("Executing seed...")
cmd := exec.Command("pg_restore",
"--single-transaction",
"--dbname", config.Config.Postgres.DSN(),
seedFile,
)
fmt.Println("Running command:", cmd)
if output, err := cmd.CombinedOutput(); err != nil {
fmt.Print(string(output))
panic(fmt.Errorf("failed to execute seed: %w", err))
}
fmt.Println("Done! You may want to migrate forward from here.")
ListMigrations()
}
// Creates only what's necessary to get the site running. Not really very useful for
// local dev on its own; sample data makes things a lot better.
func BareMinimumSeed() {
Migrate(LatestVersion())
ctx := context.Background()
conn := db.NewConnWithConfig(config.PostgresConfig{
LogLevel: pgx.LogLevelWarn,
})
defer conn.Close(ctx)
tx, err := conn.Begin(ctx)
if err != nil {
panic(err)
}
defer tx.Rollback(ctx)
fmt.Println("Creating HMN project...")
_, err = tx.Exec(ctx,
`
INSERT INTO project (id, slug, name, blurb, description, personal, lifecycle, color_1, color_2, forum_enabled, blog_enabled, date_created)
VALUES (1, 'hmn', 'Handmade Network', '', '', FALSE, $1, 'ab4c47', 'a5467d', TRUE, TRUE, '2017-01-01T00:00:00Z')
`,
models.ProjectLifecycleActive,
)
if err != nil {
panic(err)
}
fmt.Println("Creating main forum...")
_, err = tx.Exec(ctx, `
INSERT INTO subforum (id, slug, name, parent_id, project_id)
VALUES (2, '', 'Handmade Network', NULL, 1)
`)
if err != nil {
panic(err)
}
_, err = tx.Exec(ctx, `
UPDATE project SET forum_id = 2 WHERE slug = 'hmn'
`)
if err != nil {
panic(err)
}
err = tx.Commit(ctx)
if err != nil {
panic(err)
}
}
// Seeds the database with sample data for local dev.
func SampleSeed() {
BareMinimumSeed()
ctx := context.Background()
conn := db.NewConnWithConfig(config.PostgresConfig{
LogLevel: pgx.LogLevelWarn,
})
defer conn.Close(ctx)
tx, err := conn.Begin(ctx)
if err != nil {
panic(err)
}
defer tx.Rollback(ctx)
fmt.Println("Creating admin user (\"admin\"/\"password\")...")
seedUser(ctx, conn, models.User{Username: "admin", Email: "admin@handmade.network", IsStaff: true})
fmt.Println("Creating normal users (all with password \"password\")...")
alice := seedUser(ctx, conn, models.User{Username: "alice", Name: "Alice"})
bob := seedUser(ctx, conn, models.User{Username: "bob", Name: "Bob"})
charlie := seedUser(ctx, conn, models.User{Username: "charlie", Name: "Charlie"})
fmt.Println("Creating a spammer...")
seedUser(ctx, conn, models.User{Username: "spam", Name: "Hot singletons in your local area", Status: models.UserStatusConfirmed})
_ = []*models.User{alice, bob, charlie}
// admin := CreateAdminUser("admin", "12345678")
// user := CreateUser("regular_user", "12345678")
// hmnProject := CreateProject("hmn", "Handmade Network")
// Create category
// Create thread
// Create accepted user project
// Create pending user project
// Create showcase items
// Create codelanguages
// Create library and library resources
err = tx.Commit(ctx)
if err != nil {
panic(err)
}
}
func seedUser(ctx context.Context, conn db.ConnOrTx, input models.User) *models.User {
user, err := db.QueryOne[models.User](ctx, conn,
`
INSERT INTO hmn_user (
username, password, email,
is_staff,
status,
name, bio, blurb, signature,
darktheme,
showemail, edit_library,
date_joined, registration_ip, avatar_asset_id
)
VALUES (
$1, $2, $3,
$4,
$5,
$6, $7, $8, $9,
TRUE,
$10, FALSE,
'2017-01-01T00:00:00Z', '192.168.2.1', null
)
RETURNING $columns
`,
input.Username, "", utils.OrDefault(input.Email, fmt.Sprintf("%s@example.com", input.Username)),
input.IsStaff,
utils.OrDefault(input.Status, models.UserStatusApproved),
utils.OrDefault(input.Name, randomName()), utils.OrDefault(input.Bio, lorem.Paragraph(0, 2)), utils.OrDefault(input.Blurb, lorem.Sentence(0, 14)), utils.OrDefault(input.Signature, lorem.Sentence(0, 16)),
input.ShowEmail,
)
if err != nil {
panic(err)
}
err = auth.SetPassword(ctx, conn, input.Username, "password")
if err != nil {
panic(err)
}
return user
}
func randomName() string {
return "John Doe" // chosen by fair dice roll. guaranteed to be random.
}
func randomBool() bool {
return rand.Intn(2) == 1
}

View File

@ -36,7 +36,6 @@ type User struct {
Blurb string `db:"blurb"`
Signature string `db:"signature"`
AvatarAssetID *uuid.UUID `db:"avatar_asset_id"`
AvatarAsset *Asset `db:"avatar"`
DarkTheme bool `db:"darktheme"`
Timezone string `db:"timezone"`
@ -48,6 +47,9 @@ type User struct {
DiscordDeleteSnippetOnMessageDelete bool `db:"discord_delete_snippet_on_message_delete"`
MarkedAllReadAt time.Time `db:"marked_all_read_at"`
// Non-db fields, to be filled in by fetch helpers
AvatarAsset *Asset
}
func (u *User) BestName() string {

View File

@ -10,6 +10,16 @@ import (
"git.handmade.network/hmn/hmn/src/oops"
)
// Returns the provided value, or a default value if the input was zero.
func OrDefault[T comparable](v T, def T) T {
var zero T
if v == zero {
return def
} else {
return v
}
}
func IntMin(a, b int) int {
if a < b {
return a

View File

@ -256,15 +256,7 @@ func AdminApprovalQueueSubmit(c *RequestContext) ResponseData {
return RejectRequest(c, "User id can't be parsed")
}
user, err := db.QueryOne[models.User](c.Context(), c.Conn,
`
SELECT $columns{hmn_user}
FROM hmn_user
LEFT JOIN asset AS hmn_user_avatar ON hmn_user_avatar.id = hmn_user.avatar_asset_id
WHERE hmn_user.id = $1
`,
userId,
)
user, err := hmndata.FetchUser(c.Context(), c.Conn, c.CurrentUser, userId, hmndata.UsersQuery{})
if err != nil {
if errors.Is(err, db.NotFound) {
return RejectRequest(c, "User not found")

View File

@ -21,10 +21,9 @@ func APICheckUsername(c *RequestContext) ResponseData {
c.Perf.StartBlock("SQL", "Fetch user")
user, err := db.QueryOne[models.User](c.Context(), c.Conn,
`
SELECT $columns{hmn_user}
SELECT $columns
FROM
hmn_user
LEFT JOIN asset AS hmn_user_avatar ON hmn_user_avatar.id = hmn_user.avatar_asset_id
WHERE
LOWER(hmn_user.username) = LOWER($1)
AND status = ANY ($2)

View File

@ -77,10 +77,8 @@ func Login(c *RequestContext) ResponseData {
user, err := db.QueryOne[models.User](c.Context(), c.Conn,
`
SELECT $columns{hmn_user}
FROM
hmn_user
LEFT JOIN asset AS hmn_user_avatar ON hmn_user_avatar.id = hmn_user.avatar_asset_id
SELECT $columns
FROM hmn_user
WHERE LOWER(username) = LOWER($1)
`,
username,
@ -457,10 +455,8 @@ func RequestPasswordResetSubmit(c *RequestContext) ResponseData {
}
user, err := db.QueryOne[models.User](c.Context(), c.Conn,
`
SELECT $columns{hmn_user}
FROM
hmn_user
LEFT JOIN asset AS hmn_user_avatar ON hmn_user_avatar.id = hmn_user.avatar_asset_id
SELECT $columns
FROM hmn_user
WHERE
LOWER(username) = LOWER($1)
AND LOWER(email) = LOWER($2)

View File

@ -77,7 +77,7 @@ func BlogIndex(c *RequestContext) ResponseData {
canCreate := false
if c.CurrentProject.HasBlog() && c.CurrentUser != nil {
isProjectOwner := false
owners, err := hmndata.FetchProjectOwners(c.Context(), c.Conn, c.CurrentProject.ID)
owners, err := hmndata.FetchProjectOwners(c.Context(), c.Conn, c.CurrentUser, c.CurrentProject.ID)
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch project owners"))
}

View File

@ -181,7 +181,7 @@ func ProjectHomepage(c *RequestContext) ResponseData {
// There are no further permission checks to do, because permissions are
// checked whatever way we fetch the project.
owners, err := hmndata.FetchProjectOwners(c.Context(), c.Conn, c.CurrentProject.ID)
owners, err := hmndata.FetchProjectOwners(c.Context(), c.Conn, c.CurrentUser, c.CurrentProject.ID)
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, err)
}
@ -818,10 +818,8 @@ func updateProject(ctx context.Context, tx pgx.Tx, user *models.User, payload *P
owners, err := db.Query[models.User](ctx, tx,
`
SELECT $columns{hmn_user}
FROM
hmn_user
LEFT JOIN asset AS hmn_user_avatar ON hmn_user_avatar.id = hmn_user.avatar_asset_id
SELECT $columns
FROM hmn_user
WHERE LOWER(username) = ANY ($1)
`,
payload.OwnerUsernames,

View File

@ -548,16 +548,9 @@ func getCurrentUserAndSession(c *RequestContext, sessionId string) (*models.User
}
}
user, err := db.QueryOne[models.User](c.Context(), c.Conn,
`
SELECT $columns{hmn_user}
FROM
hmn_user
LEFT JOIN asset AS hmn_user_avatar ON hmn_user_avatar.id = hmn_user.avatar_asset_id
WHERE username = $1
`,
session.Username,
)
user, err := hmndata.FetchUserByUsername(c.Context(), c.Conn, nil, session.Username, hmndata.UsersQuery{
AnyStatus: true,
})
if err != nil {
if errors.Is(err, db.NotFound) {
logging.Debug().Str("username", session.Username).Msg("returning no current user for this request because the user for the session couldn't be found")

View File

@ -52,19 +52,7 @@ func UserProfile(c *RequestContext) ResponseData {
if c.CurrentUser != nil && strings.ToLower(c.CurrentUser.Username) == username {
profileUser = c.CurrentUser
} else {
c.Perf.StartBlock("SQL", "Fetch user")
user, err := db.QueryOne[models.User](c.Context(), c.Conn,
`
SELECT $columns{hmn_user}
FROM
hmn_user
LEFT JOIN asset AS hmn_user_avatar ON hmn_user_avatar.id = hmn_user.avatar_asset_id
WHERE
LOWER(hmn_user.username) = $1
`,
username,
)
c.Perf.EndBlock()
user, err := hmndata.FetchUserByUsername(c.Context(), c.Conn, c.CurrentUser, username, hmndata.UsersQuery{})
if err != nil {
if errors.Is(err, db.NotFound) {
return FourOhFour(c)

View File

@ -33,7 +33,7 @@ var WebsiteCommand = &cobra.Command{
backgroundJobContext, cancelBackgroundJobs := context.WithCancel(context.Background())
longRequestContext, cancelLongRequests := context.WithCancel(context.Background())
conn := db.NewConnPool(config.Config.Postgres.MinConn, config.Config.Postgres.MaxConn)
conn := db.NewConnPool()
perfCollector := perf.RunPerfCollector(backgroundJobContext)
server := http.Server{