Seed users (and rework a lot of user access to use new helpers)
This commit is contained in:
parent
b46f5d8637
commit
3a93aa93e9
1
go.mod
1
go.mod
|
@ -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
2
go.sum
|
@ -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=
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
`
|
||||
|
|
47
src/db/db.go
47
src/db/db.go
|
@ -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.
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 = ¤tUser.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.
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"))
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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{
|
||||
|
|
Loading…
Reference in New Issue