From 7ea66dc02e45e6fca71b7ac1c89257e2601bb03b Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Wed, 18 Feb 2026 07:02:36 +0000 Subject: [PATCH] Add user account roles --- auth/auth.go | 1 + db/dbinfo/user_.bob.go | 12 ++++- db/enums/enums.bob.go | 72 +++++++++++++++++++++++++++++ db/factory/bobfactory_main.bob.go | 1 + db/factory/bobfactory_random.bob.go | 10 ++++ db/factory/user_.bob.go | 44 ++++++++++++++++++ db/migrations/00065_user_roles.sql | 11 +++++ db/models/user_.bob.go | 33 +++++++++++-- db/sql/user_by_username.bob.go | 14 +++--- db/sql/user_by_username.bob.sql | 2 +- 10 files changed, 188 insertions(+), 12 deletions(-) create mode 100644 db/migrations/00065_user_roles.sql diff --git a/auth/auth.go b/auth/auth.go index 6599d328..9a40c8e4 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -151,6 +151,7 @@ func SignupUser(ctx context.Context, username string, name string, password stri OrganizationID: omit.From(o.ID), PasswordHash: omit.From(passwordHash), PasswordHashType: omit.From(enums.HashtypeBcrypt14), + Role: omit.From(enums.UserroleAccountOwner), Username: omit.From(username), } u, err := models.Users.Insert(&u_setter).One(ctx, db.PGInstance.BobDB) diff --git a/db/dbinfo/user_.bob.go b/db/dbinfo/user_.bob.go index 44fdd868..fcee24c3 100644 --- a/db/dbinfo/user_.bob.go +++ b/db/dbinfo/user_.bob.go @@ -123,6 +123,15 @@ var Users = Table[ Generated: false, AutoIncr: false, }, + Role: column{ + Name: "role", + DBType: "public.userrole", + Default: "", + Comment: "", + Nullable: false, + Generated: false, + AutoIncr: false, + }, }, Indexes: userIndexes{ UserPkey: index{ @@ -200,11 +209,12 @@ type userColumns struct { Username column PasswordHashType column PasswordHash column + Role column } func (c userColumns) AsSlice() []column { return []column{ - c.ID, c.ArcgisAccessToken, c.ArcgisLicense, c.ArcgisRefreshToken, c.ArcgisRefreshTokenExpires, c.ArcgisRole, c.DisplayName, c.Email, c.OrganizationID, c.Username, c.PasswordHashType, c.PasswordHash, + c.ID, c.ArcgisAccessToken, c.ArcgisLicense, c.ArcgisRefreshToken, c.ArcgisRefreshTokenExpires, c.ArcgisRole, c.DisplayName, c.Email, c.OrganizationID, c.Username, c.PasswordHashType, c.PasswordHash, c.Role, } } diff --git a/db/enums/enums.bob.go b/db/enums/enums.bob.go index 4139bbb7..d708b364 100644 --- a/db/enums/enums.bob.go +++ b/db/enums/enums.bob.go @@ -1437,3 +1437,75 @@ func (e *PublicreportReportstatustype) Scan(value any) error { return nil } + +// Enum values for Userrole +const ( + UserroleRoot Userrole = "root" + UserroleAccountOwner Userrole = "account-owner" +) + +func AllUserrole() []Userrole { + return []Userrole{ + UserroleRoot, + UserroleAccountOwner, + } +} + +type Userrole string + +func (e Userrole) String() string { + return string(e) +} + +func (e Userrole) Valid() bool { + switch e { + case UserroleRoot, UserroleAccountOwner: + return true + default: + return false + } +} + +// useful when testing in other packages +func (e Userrole) All() []Userrole { + return AllUserrole() +} + +func (e Userrole) MarshalText() ([]byte, error) { + return []byte(e), nil +} + +func (e *Userrole) UnmarshalText(text []byte) error { + return e.Scan(text) +} + +func (e Userrole) MarshalBinary() ([]byte, error) { + return []byte(e), nil +} + +func (e *Userrole) UnmarshalBinary(data []byte) error { + return e.Scan(data) +} + +func (e Userrole) Value() (driver.Value, error) { + return string(e), nil +} + +func (e *Userrole) Scan(value any) error { + switch x := value.(type) { + case string: + *e = Userrole(x) + case []byte: + *e = Userrole(x) + case nil: + return fmt.Errorf("cannot nil into Userrole") + default: + return fmt.Errorf("cannot scan type %T: %v", value, value) + } + + if !e.Valid() { + return fmt.Errorf("invalid Userrole value: %s", *e) + } + + return nil +} diff --git a/db/factory/bobfactory_main.bob.go b/db/factory/bobfactory_main.bob.go index e593b5d0..2c7712dd 100644 --- a/db/factory/bobfactory_main.bob.go +++ b/db/factory/bobfactory_main.bob.go @@ -3804,6 +3804,7 @@ func (f *Factory) FromExistingUser(m *models.User) *UserTemplate { o.Username = func() string { return m.Username } o.PasswordHashType = func() enums.Hashtype { return m.PasswordHashType } o.PasswordHash = func() string { return m.PasswordHash } + o.Role = func() enums.Userrole { return m.Role } ctx := context.Background() if len(m.R.PublicUserUser) > 0 { diff --git a/db/factory/bobfactory_random.bob.go b/db/factory/bobfactory_random.bob.go index d95a2459..7fe8364a 100644 --- a/db/factory/bobfactory_random.bob.go +++ b/db/factory/bobfactory_random.bob.go @@ -231,6 +231,16 @@ func random_enums_PublicreportReportstatustype(f *faker.Faker, limits ...string) return all[f.IntBetween(0, len(all)-1)] } +func random_enums_Userrole(f *faker.Faker, limits ...string) enums.Userrole { + if f == nil { + f = &defaultFaker + } + + var e enums.Userrole + all := e.All() + return all[f.IntBetween(0, len(all)-1)] +} + func random_float32(f *faker.Faker, limits ...string) float32 { if f == nil { f = &defaultFaker diff --git a/db/factory/user_.bob.go b/db/factory/user_.bob.go index 35cad5d3..38937f1e 100644 --- a/db/factory/user_.bob.go +++ b/db/factory/user_.bob.go @@ -50,6 +50,7 @@ type UserTemplate struct { Username func() string PasswordHashType func() enums.Hashtype PasswordHash func() string + Role func() enums.Userrole r userR f *Factory @@ -298,6 +299,10 @@ func (o UserTemplate) BuildSetter() *models.UserSetter { val := o.PasswordHash() m.PasswordHash = omit.From(val) } + if o.Role != nil { + val := o.Role() + m.Role = omit.From(val) + } return m } @@ -356,6 +361,9 @@ func (o UserTemplate) Build() *models.User { if o.PasswordHash != nil { m.PasswordHash = o.PasswordHash() } + if o.Role != nil { + m.Role = o.Role() + } o.setModelRels(m) @@ -396,6 +404,10 @@ func ensureCreatableUser(m *models.UserSetter) { val := random_string(nil) m.PasswordHash = omit.From(val) } + if !(m.Role.IsValue()) { + val := random_enums_Userrole(nil) + m.Role = omit.From(val) + } } // insertOptRels creates and inserts any optional the relationships on *models.User @@ -707,6 +719,7 @@ func (m userMods) RandomizeAllColumns(f *faker.Faker) UserMod { UserMods.RandomUsername(f), UserMods.RandomPasswordHashType(f), UserMods.RandomPasswordHash(f), + UserMods.RandomRole(f), } } @@ -1214,6 +1227,37 @@ func (m userMods) RandomPasswordHash(f *faker.Faker) UserMod { }) } +// Set the model columns to this value +func (m userMods) Role(val enums.Userrole) UserMod { + return UserModFunc(func(_ context.Context, o *UserTemplate) { + o.Role = func() enums.Userrole { return val } + }) +} + +// Set the Column from the function +func (m userMods) RoleFunc(f func() enums.Userrole) UserMod { + return UserModFunc(func(_ context.Context, o *UserTemplate) { + o.Role = f + }) +} + +// Clear any values for the column +func (m userMods) UnsetRole() UserMod { + return UserModFunc(func(_ context.Context, o *UserTemplate) { + o.Role = nil + }) +} + +// Generates a random value for the column using the given faker +// if faker is nil, a default faker is used +func (m userMods) RandomRole(f *faker.Faker) UserMod { + return UserModFunc(func(_ context.Context, o *UserTemplate) { + o.Role = func() enums.Userrole { + return random_enums_Userrole(f) + } + }) +} + func (m userMods) WithParentsCascading() UserMod { return UserModFunc(func(ctx context.Context, o *UserTemplate) { if isDone, _ := userWithParentsCascadingCtx.Value(ctx); isDone { diff --git a/db/migrations/00065_user_roles.sql b/db/migrations/00065_user_roles.sql new file mode 100644 index 00000000..35766ba8 --- /dev/null +++ b/db/migrations/00065_user_roles.sql @@ -0,0 +1,11 @@ +-- +goose Up +CREATE TYPE UserRole AS ENUM ( + 'root', + 'account-owner' +); +ALTER TABLE user_ ADD COLUMN role UserRole; +UPDATE user_ SET role = 'account-owner'; +ALTER TABLE user_ ALTER COLUMN role SET NOT NULL; +-- +goose Down +ALTER TABLE user_ DROP COLUMN role; +DROP TYPE UserRole; diff --git a/db/models/user_.bob.go b/db/models/user_.bob.go index dab7addb..ba453061 100644 --- a/db/models/user_.bob.go +++ b/db/models/user_.bob.go @@ -39,6 +39,7 @@ type User struct { Username string `db:"username" ` PasswordHashType enums.Hashtype `db:"password_hash_type" ` PasswordHash string `db:"password_hash" ` + Role enums.Userrole `db:"role" ` R userR `db:"-" ` @@ -72,7 +73,7 @@ type userR struct { func buildUserColumns(alias string) userColumns { return userColumns{ ColumnsExpr: expr.NewColumnsExpr( - "id", "arcgis_access_token", "arcgis_license", "arcgis_refresh_token", "arcgis_refresh_token_expires", "arcgis_role", "display_name", "email", "organization_id", "username", "password_hash_type", "password_hash", + "id", "arcgis_access_token", "arcgis_license", "arcgis_refresh_token", "arcgis_refresh_token_expires", "arcgis_role", "display_name", "email", "organization_id", "username", "password_hash_type", "password_hash", "role", ).WithParent("user_"), tableAlias: alias, ID: psql.Quote(alias, "id"), @@ -87,6 +88,7 @@ func buildUserColumns(alias string) userColumns { Username: psql.Quote(alias, "username"), PasswordHashType: psql.Quote(alias, "password_hash_type"), PasswordHash: psql.Quote(alias, "password_hash"), + Role: psql.Quote(alias, "role"), } } @@ -105,6 +107,7 @@ type userColumns struct { Username psql.Expression PasswordHashType psql.Expression PasswordHash psql.Expression + Role psql.Expression } func (c userColumns) Alias() string { @@ -131,10 +134,11 @@ type UserSetter struct { Username omit.Val[string] `db:"username" ` PasswordHashType omit.Val[enums.Hashtype] `db:"password_hash_type" ` PasswordHash omit.Val[string] `db:"password_hash" ` + Role omit.Val[enums.Userrole] `db:"role" ` } func (s UserSetter) SetColumns() []string { - vals := make([]string, 0, 12) + vals := make([]string, 0, 13) if s.ID.IsValue() { vals = append(vals, "id") } @@ -171,6 +175,9 @@ func (s UserSetter) SetColumns() []string { if s.PasswordHash.IsValue() { vals = append(vals, "password_hash") } + if s.Role.IsValue() { + vals = append(vals, "role") + } return vals } @@ -211,6 +218,9 @@ func (s UserSetter) Overwrite(t *User) { if s.PasswordHash.IsValue() { t.PasswordHash = s.PasswordHash.MustGet() } + if s.Role.IsValue() { + t.Role = s.Role.MustGet() + } } func (s *UserSetter) Apply(q *dialect.InsertQuery) { @@ -219,7 +229,7 @@ func (s *UserSetter) Apply(q *dialect.InsertQuery) { }) q.AppendValues(bob.ExpressionFunc(func(ctx context.Context, w io.StringWriter, d bob.Dialect, start int) ([]any, error) { - vals := make([]bob.Expression, 12) + vals := make([]bob.Expression, 13) if s.ID.IsValue() { vals[0] = psql.Arg(s.ID.MustGet()) } else { @@ -292,6 +302,12 @@ func (s *UserSetter) Apply(q *dialect.InsertQuery) { vals[11] = psql.Raw("DEFAULT") } + if s.Role.IsValue() { + vals[12] = psql.Arg(s.Role.MustGet()) + } else { + vals[12] = psql.Raw("DEFAULT") + } + return bob.ExpressSlice(ctx, w, d, start, vals, "", ", ", "") })) } @@ -301,7 +317,7 @@ func (s UserSetter) UpdateMod() bob.Mod[*dialect.UpdateQuery] { } func (s UserSetter) Expressions(prefix ...string) []bob.Expression { - exprs := make([]bob.Expression, 0, 12) + exprs := make([]bob.Expression, 0, 13) if s.ID.IsValue() { exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ @@ -387,6 +403,13 @@ func (s UserSetter) Expressions(prefix ...string) []bob.Expression { }}) } + if s.Role.IsValue() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "role")...), + psql.Arg(s.Role), + }}) + } + return exprs } @@ -1526,6 +1549,7 @@ type userWhere[Q psql.Filterable] struct { Username psql.WhereMod[Q, string] PasswordHashType psql.WhereMod[Q, enums.Hashtype] PasswordHash psql.WhereMod[Q, string] + Role psql.WhereMod[Q, enums.Userrole] } func (userWhere[Q]) AliasedAs(alias string) userWhere[Q] { @@ -1546,6 +1570,7 @@ func buildUserWhere[Q psql.Filterable](cols userColumns) userWhere[Q] { Username: psql.Where[Q, string](cols.Username), PasswordHashType: psql.Where[Q, enums.Hashtype](cols.PasswordHashType), PasswordHash: psql.Where[Q, string](cols.PasswordHash), + Role: psql.Where[Q, enums.Userrole](cols.Role), } } diff --git a/db/sql/user_by_username.bob.go b/db/sql/user_by_username.bob.go index 3ae2301d..adf769bc 100644 --- a/db/sql/user_by_username.bob.go +++ b/db/sql/user_by_username.bob.go @@ -22,7 +22,7 @@ import ( //go:embed user_by_username.bob.sql var formattedQueries_user_by_username string -var userByUsernameSQL = formattedQueries_user_by_username[152:780] +var userByUsernameSQL = formattedQueries_user_by_username[152:806] type UserByUsernameQuery = orm.ModQuery[*dialect.SelectQuery, userByUsername, UserByUsernameRow, []UserByUsernameRow, userByUsernameTransformer] @@ -55,6 +55,7 @@ func UserByUsername(Username string) *UserByUsernameQuery { row.ScheduleScanByIndex(9, &t.Username) row.ScheduleScanByIndex(10, &t.PasswordHashType) row.ScheduleScanByIndex(11, &t.PasswordHash) + row.ScheduleScanByIndex(12, &t.Role) return &t, nil }, func(v any) (UserByUsernameRow, error) { return *(v.(*UserByUsernameRow)), nil @@ -62,9 +63,9 @@ func UserByUsername(Username string) *UserByUsernameQuery { }, }, Mod: bob.ModFunc[*dialect.SelectQuery](func(q *dialect.SelectQuery) { - q.AppendSelect(expressionTypArgs.subExpr(7, 551)) - q.SetTable(expressionTypArgs.subExpr(557, 569)) - q.AppendWhere(expressionTypArgs.subExpr(577, 628)) + q.AppendSelect(expressionTypArgs.subExpr(7, 577)) + q.SetTable(expressionTypArgs.subExpr(583, 595)) + q.AppendWhere(expressionTypArgs.subExpr(603, 654)) }), } } @@ -82,6 +83,7 @@ type UserByUsernameRow = struct { Username string `db:"username"` PasswordHashType enums.Hashtype `db:"password_hash_type"` PasswordHash string `db:"password_hash"` + Role enums.Userrole `db:"role"` } type userByUsernameTransformer = bob.SliceTransformer[UserByUsernameRow, []UserByUsernameRow] @@ -94,8 +96,8 @@ func (o userByUsername) args() iter.Seq[orm.ArgWithPosition] { return func(yield func(arg orm.ArgWithPosition) bool) { if !yield(orm.ArgWithPosition{ Name: "username", - Start: 588, - Stop: 590, + Start: 614, + Stop: 616, Expression: o.Username, }) { return diff --git a/db/sql/user_by_username.bob.sql b/db/sql/user_by_username.bob.sql index 414622c9..a4073059 100644 --- a/db/sql/user_by_username.bob.sql +++ b/db/sql/user_by_username.bob.sql @@ -2,6 +2,6 @@ -- This file is meant to be re-generated in place and/or deleted at any time. -- UserByUsername -SELECT "user_"."id" AS "id", "user_"."arcgis_access_token" AS "arcgis_access_token", "user_"."arcgis_license" AS "arcgis_license", "user_"."arcgis_refresh_token" AS "arcgis_refresh_token", "user_"."arcgis_refresh_token_expires" AS "arcgis_refresh_token_expires", "user_"."arcgis_role" AS "arcgis_role", "user_"."display_name" AS "display_name", "user_"."email" AS "email", "user_"."organization_id" AS "organization_id", "user_"."username" AS "username", "user_"."password_hash_type" AS "password_hash_type", "user_"."password_hash" AS "password_hash" FROM public.user_ WHERE +SELECT "user_"."id" AS "id", "user_"."arcgis_access_token" AS "arcgis_access_token", "user_"."arcgis_license" AS "arcgis_license", "user_"."arcgis_refresh_token" AS "arcgis_refresh_token", "user_"."arcgis_refresh_token_expires" AS "arcgis_refresh_token_expires", "user_"."arcgis_role" AS "arcgis_role", "user_"."display_name" AS "display_name", "user_"."email" AS "email", "user_"."organization_id" AS "organization_id", "user_"."username" AS "username", "user_"."password_hash_type" AS "password_hash_type", "user_"."password_hash" AS "password_hash", "user_"."role" AS "role" FROM public.user_ WHERE username = $1 AND password_hash_type = 'bcrypt-14';