From 53ee020fe01f0f947c611f1c76e2782b79279baf Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Tue, 6 Jan 2026 15:06:16 +0000 Subject: [PATCH] Always include an organization for every user --- auth/auth.go | 23 +++++-- db/dbinfo/user_.bob.go | 4 +- db/factory/bobfactory_main.bob.go | 2 +- db/factory/organization.bob.go | 2 +- db/factory/user_.bob.go | 80 +++++++++-------------- db/migrations/00023_user_org_not_null.sql | 5 ++ db/models/organization.bob.go | 9 +-- db/models/user_.bob.go | 29 ++++---- db/sql/user_by_username.bob.go | 2 +- endpoint.go | 14 ++-- 10 files changed, 85 insertions(+), 85 deletions(-) create mode 100644 db/migrations/00023_user_org_not_null.sql diff --git a/auth/auth.go b/auth/auth.go index cee0fa71..7a5e3def 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -13,6 +13,7 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/db/sql" "github.com/Gleipnir-Technology/nidus-sync/debug" "github.com/aarondl/opt/omit" + "github.com/aarondl/opt/omitnull" "github.com/rs/zerolog/log" "golang.org/x/crypto/bcrypt" ) @@ -122,22 +123,34 @@ func SigninUser(r *http.Request, username string, password string) (*models.User return user, nil } -func SignupUser(username string, name string, password string) (*models.User, error) { +func SignupUser(ctx context.Context, username string, name string, password string) (*models.User, error) { passwordHash, err := hashPassword(password) if err != nil { - return nil, fmt.Errorf("Cannot signup user: %w", err) + return nil, fmt.Errorf("Cannot signup user, failed to create hashed password: %w", err) } - setter := models.UserSetter{ + o_setter := models.OrganizationSetter{ + Name: omitnull.From(fmt.Sprintf("%s's organization", username)), + ArcgisID: omitnull.From(""), + ArcgisName: omitnull.From(""), + FieldseekerURL: omitnull.From(""), + } + o, err := models.Organizations.Insert(&o_setter).One(ctx, db.PGInstance.BobDB) + if err != nil { + return nil, fmt.Errorf("Failed to create organization: %w", err) + } + log.Info().Int32("id", o.ID).Msg("Created organization") + u_setter := models.UserSetter{ DisplayName: omit.From(name), + OrganizationID: omit.From(o.ID), PasswordHash: omit.From(passwordHash), PasswordHashType: omit.From(enums.HashtypeBcrypt14), Username: omit.From(username), } - u, err := models.Users.Insert(&setter).One(context.TODO(), db.PGInstance.BobDB) + u, err := models.Users.Insert(&u_setter).One(ctx, db.PGInstance.BobDB) if err != nil { return nil, fmt.Errorf("Failed to create user: %w", err) } - log.Info().Int("ID", int(u.ID)).Str("username", u.Username).Msg("Created user") + log.Info().Int32("id", u.ID).Str("username", u.Username).Msg("Created user") return u, nil } diff --git a/db/dbinfo/user_.bob.go b/db/dbinfo/user_.bob.go index 99a9e838..556adb13 100644 --- a/db/dbinfo/user_.bob.go +++ b/db/dbinfo/user_.bob.go @@ -90,9 +90,9 @@ var Users = Table[ OrganizationID: column{ Name: "organization_id", DBType: "integer", - Default: "NULL", + Default: "", Comment: "", - Nullable: true, + Nullable: false, Generated: false, AutoIncr: false, }, diff --git a/db/factory/bobfactory_main.bob.go b/db/factory/bobfactory_main.bob.go index 17d2a5a8..fb604907 100644 --- a/db/factory/bobfactory_main.bob.go +++ b/db/factory/bobfactory_main.bob.go @@ -2523,7 +2523,7 @@ func (f *Factory) FromExistingUser(m *models.User) *UserTemplate { o.ArcgisRole = func() null.Val[string] { return m.ArcgisRole } o.DisplayName = func() string { return m.DisplayName } o.Email = func() null.Val[string] { return m.Email } - o.OrganizationID = func() null.Val[int32] { return m.OrganizationID } + o.OrganizationID = func() int32 { return m.OrganizationID } o.Username = func() string { return m.Username } o.PasswordHashType = func() enums.Hashtype { return m.PasswordHashType } o.PasswordHash = func() string { return m.PasswordHash } diff --git a/db/factory/organization.bob.go b/db/factory/organization.bob.go index 4bf9527f..79f6a071 100644 --- a/db/factory/organization.bob.go +++ b/db/factory/organization.bob.go @@ -630,7 +630,7 @@ func (t OrganizationTemplate) setModelRels(o *models.Organization) { for _, r := range t.r.User { related := r.o.BuildMany(r.number) for _, rel := range related { - rel.OrganizationID = null.From(o.ID) // h2 + rel.OrganizationID = o.ID // h2 rel.R.Organization = o } rel = append(rel, related...) diff --git a/db/factory/user_.bob.go b/db/factory/user_.bob.go index 1662c011..6425b915 100644 --- a/db/factory/user_.bob.go +++ b/db/factory/user_.bob.go @@ -46,7 +46,7 @@ type UserTemplate struct { ArcgisRole func() null.Val[string] DisplayName func() string Email func() null.Val[string] - OrganizationID func() null.Val[int32] + OrganizationID func() int32 Username func() string PasswordHashType func() enums.Hashtype PasswordHash func() string @@ -186,7 +186,7 @@ func (t UserTemplate) setModelRels(o *models.User) { if t.r.Organization != nil { rel := t.r.Organization.o.Build() rel.R.User = append(rel.R.User, o) - o.OrganizationID = null.From(rel.ID) // h2 + o.OrganizationID = rel.ID // h2 o.R.Organization = rel } } @@ -230,7 +230,7 @@ func (o UserTemplate) BuildSetter() *models.UserSetter { } if o.OrganizationID != nil { val := o.OrganizationID() - m.OrganizationID = omitnull.FromNull(val) + m.OrganizationID = omit.From(val) } if o.Username != nil { val := o.Username() @@ -326,6 +326,10 @@ func ensureCreatableUser(m *models.UserSetter) { val := random_string(nil, "200") m.DisplayName = omit.From(val) } + if !(m.OrganizationID.IsValue()) { + val := random_int32(nil) + m.OrganizationID = omit.From(val) + } if !(m.Username.IsValue()) { val := random_string(nil) m.Username = omit.From(val) @@ -466,25 +470,6 @@ func (o *UserTemplate) insertOptRels(ctx context.Context, exec bob.Executor, m * } } - isOrganizationDone, _ := userRelOrganizationCtx.Value(ctx) - if !isOrganizationDone && o.r.Organization != nil { - ctx = userRelOrganizationCtx.WithValue(ctx, true) - if o.r.Organization.o.alreadyPersisted { - m.R.Organization = o.r.Organization.o.Build() - } else { - var rel6 *models.Organization - rel6, err = o.r.Organization.o.Create(ctx, exec) - if err != nil { - return err - } - err = m.AttachOrganization(ctx, exec, rel6) - if err != nil { - return err - } - } - - } - return err } @@ -495,11 +480,30 @@ func (o *UserTemplate) Create(ctx context.Context, exec bob.Executor) (*models.U opt := o.BuildSetter() ensureCreatableUser(opt) + if o.r.Organization == nil { + UserMods.WithNewOrganization().Apply(ctx, o) + } + + var rel6 *models.Organization + + if o.r.Organization.o.alreadyPersisted { + rel6 = o.r.Organization.o.Build() + } else { + rel6, err = o.r.Organization.o.Create(ctx, exec) + if err != nil { + return nil, err + } + } + + opt.OrganizationID = omit.From(rel6.ID) + m, err := models.Users.Insert(opt).One(ctx, exec) if err != nil { return nil, err } + m.R.Organization = rel6 + if err := o.insertOptRels(ctx, exec, m); err != nil { return nil, err } @@ -973,14 +977,14 @@ func (m userMods) RandomEmailNotNull(f *faker.Faker) UserMod { } // Set the model columns to this value -func (m userMods) OrganizationID(val null.Val[int32]) UserMod { +func (m userMods) OrganizationID(val int32) UserMod { return UserModFunc(func(_ context.Context, o *UserTemplate) { - o.OrganizationID = func() null.Val[int32] { return val } + o.OrganizationID = func() int32 { return val } }) } // Set the Column from the function -func (m userMods) OrganizationIDFunc(f func() null.Val[int32]) UserMod { +func (m userMods) OrganizationIDFunc(f func() int32) UserMod { return UserModFunc(func(_ context.Context, o *UserTemplate) { o.OrganizationID = f }) @@ -995,32 +999,10 @@ func (m userMods) UnsetOrganizationID() UserMod { // Generates a random value for the column using the given faker // if faker is nil, a default faker is used -// The generated value is sometimes null func (m userMods) RandomOrganizationID(f *faker.Faker) UserMod { return UserModFunc(func(_ context.Context, o *UserTemplate) { - o.OrganizationID = func() null.Val[int32] { - if f == nil { - f = &defaultFaker - } - - val := random_int32(f) - return null.From(val) - } - }) -} - -// Generates a random value for the column using the given faker -// if faker is nil, a default faker is used -// The generated value is never null -func (m userMods) RandomOrganizationIDNotNull(f *faker.Faker) UserMod { - return UserModFunc(func(_ context.Context, o *UserTemplate) { - o.OrganizationID = func() null.Val[int32] { - if f == nil { - f = &defaultFaker - } - - val := random_int32(f) - return null.From(val) + o.OrganizationID = func() int32 { + return random_int32(f) } }) } diff --git a/db/migrations/00023_user_org_not_null.sql b/db/migrations/00023_user_org_not_null.sql new file mode 100644 index 00000000..e453fdf6 --- /dev/null +++ b/db/migrations/00023_user_org_not_null.sql @@ -0,0 +1,5 @@ +-- +goose Up +ALTER TABLE user_ ALTER COLUMN organization_id SET NOT NULL; + +-- +goose Down +ALTER TABLE user_ ALTER COLUMN organization_id DROP NOT NULL; diff --git a/db/models/organization.bob.go b/db/models/organization.bob.go index bc8413af..1139d02c 100644 --- a/db/models/organization.bob.go +++ b/db/models/organization.bob.go @@ -3348,7 +3348,7 @@ func (organization0 *Organization) AttachNoteImages(ctx context.Context, exec bo func insertOrganizationUser0(ctx context.Context, exec bob.Executor, users1 []*UserSetter, organization0 *Organization) (UserSlice, error) { for i := range users1 { - users1[i].OrganizationID = omitnull.From(organization0.ID) + users1[i].OrganizationID = omit.From(organization0.ID) } ret, err := Users.Insert(bob.ToMods(users1...)).All(ctx, exec) @@ -3361,7 +3361,7 @@ func insertOrganizationUser0(ctx context.Context, exec bob.Executor, users1 []*U func attachOrganizationUser0(ctx context.Context, exec bob.Executor, count int, users1 UserSlice, organization0 *Organization) (UserSlice, error) { setter := &UserSetter{ - OrganizationID: omitnull.From(organization0.ID), + OrganizationID: omit.From(organization0.ID), } err := users1.UpdateAll(ctx, exec, *setter) @@ -6169,10 +6169,7 @@ func (os OrganizationSlice) LoadUser(ctx context.Context, exec bob.Executor, mod for _, rel := range users { - if !rel.OrganizationID.IsValue() { - continue - } - if !(rel.OrganizationID.IsValue() && o.ID == rel.OrganizationID.MustGet()) { + if !(o.ID == rel.OrganizationID) { continue } diff --git a/db/models/user_.bob.go b/db/models/user_.bob.go index 7e23b40b..7063a0e8 100644 --- a/db/models/user_.bob.go +++ b/db/models/user_.bob.go @@ -35,7 +35,7 @@ type User struct { ArcgisRole null.Val[string] `db:"arcgis_role" ` DisplayName string `db:"display_name" ` Email null.Val[string] `db:"email" ` - OrganizationID null.Val[int32] `db:"organization_id" ` + OrganizationID int32 `db:"organization_id" ` Username string `db:"username" ` PasswordHashType enums.Hashtype `db:"password_hash_type" ` PasswordHash string `db:"password_hash" ` @@ -122,7 +122,7 @@ type UserSetter struct { ArcgisRole omitnull.Val[string] `db:"arcgis_role" ` DisplayName omit.Val[string] `db:"display_name" ` Email omitnull.Val[string] `db:"email" ` - OrganizationID omitnull.Val[int32] `db:"organization_id" ` + OrganizationID omit.Val[int32] `db:"organization_id" ` Username omit.Val[string] `db:"username" ` PasswordHashType omit.Val[enums.Hashtype] `db:"password_hash_type" ` PasswordHash omit.Val[string] `db:"password_hash" ` @@ -154,7 +154,7 @@ func (s UserSetter) SetColumns() []string { if !s.Email.IsUnset() { vals = append(vals, "email") } - if !s.OrganizationID.IsUnset() { + if s.OrganizationID.IsValue() { vals = append(vals, "organization_id") } if s.Username.IsValue() { @@ -194,8 +194,8 @@ func (s UserSetter) Overwrite(t *User) { if !s.Email.IsUnset() { t.Email = s.Email.MustGetNull() } - if !s.OrganizationID.IsUnset() { - t.OrganizationID = s.OrganizationID.MustGetNull() + if s.OrganizationID.IsValue() { + t.OrganizationID = s.OrganizationID.MustGet() } if s.Username.IsValue() { t.Username = s.Username.MustGet() @@ -263,8 +263,8 @@ func (s *UserSetter) Apply(q *dialect.InsertQuery) { vals[7] = psql.Raw("DEFAULT") } - if !s.OrganizationID.IsUnset() { - vals[8] = psql.Arg(s.OrganizationID.MustGetNull()) + if s.OrganizationID.IsValue() { + vals[8] = psql.Arg(s.OrganizationID.MustGet()) } else { vals[8] = psql.Raw("DEFAULT") } @@ -354,7 +354,7 @@ func (s UserSetter) Expressions(prefix ...string) []bob.Expression { }}) } - if !s.OrganizationID.IsUnset() { + if s.OrganizationID.IsValue() { exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ psql.Quote(append(prefix, "organization_id")...), psql.Arg(s.OrganizationID), @@ -760,7 +760,7 @@ func (o *User) Organization(mods ...bob.Mod[*dialect.SelectQuery]) Organizations } func (os UserSlice) Organization(mods ...bob.Mod[*dialect.SelectQuery]) OrganizationsQuery { - pkOrganizationID := make(pgtypes.Array[null.Val[int32]], 0, len(os)) + pkOrganizationID := make(pgtypes.Array[int32], 0, len(os)) for _, o := range os { if o == nil { continue @@ -1186,7 +1186,7 @@ func (user0 *User) AttachUserOauthTokens(ctx context.Context, exec bob.Executor, func attachUserOrganization0(ctx context.Context, exec bob.Executor, count int, user0 *User, organization1 *Organization) (*User, error) { setter := &UserSetter{ - OrganizationID: omitnull.From(organization1.ID), + OrganizationID: omit.From(organization1.ID), } err := user0.Update(ctx, exec, setter) @@ -1241,7 +1241,7 @@ type userWhere[Q psql.Filterable] struct { ArcgisRole psql.WhereNullMod[Q, string] DisplayName psql.WhereMod[Q, string] Email psql.WhereNullMod[Q, string] - OrganizationID psql.WhereNullMod[Q, int32] + OrganizationID psql.WhereMod[Q, int32] Username psql.WhereMod[Q, string] PasswordHashType psql.WhereMod[Q, enums.Hashtype] PasswordHash psql.WhereMod[Q, string] @@ -1261,7 +1261,7 @@ func buildUserWhere[Q psql.Filterable](cols userColumns) userWhere[Q] { ArcgisRole: psql.WhereNull[Q, string](cols.ArcgisRole), DisplayName: psql.Where[Q, string](cols.DisplayName), Email: psql.WhereNull[Q, string](cols.Email), - OrganizationID: psql.WhereNull[Q, int32](cols.OrganizationID), + OrganizationID: psql.Where[Q, int32](cols.OrganizationID), Username: psql.Where[Q, string](cols.Username), PasswordHashType: psql.Where[Q, enums.Hashtype](cols.PasswordHashType), PasswordHash: psql.Where[Q, string](cols.PasswordHash), @@ -1885,11 +1885,8 @@ func (os UserSlice) LoadOrganization(ctx context.Context, exec bob.Executor, mod } for _, rel := range organizations { - if !o.OrganizationID.IsValue() { - continue - } - if !(o.OrganizationID.IsValue() && o.OrganizationID.MustGet() == rel.ID) { + if !(o.OrganizationID == rel.ID) { continue } diff --git a/db/sql/user_by_username.bob.go b/db/sql/user_by_username.bob.go index ade13164..e3eef602 100644 --- a/db/sql/user_by_username.bob.go +++ b/db/sql/user_by_username.bob.go @@ -78,7 +78,7 @@ type UserByUsernameRow = struct { ArcgisRole null.Val[string] `db:"arcgis_role"` DisplayName string `db:"display_name"` Email null.Val[string] `db:"email"` - OrganizationID null.Val[int32] `db:"organization_id"` + OrganizationID int32 `db:"organization_id"` Username string `db:"username"` PasswordHashType enums.Hashtype `db:"password_hash_type"` PasswordHash string `db:"password_hash"` diff --git a/endpoint.go b/endpoint.go index ac748a58..19b78321 100644 --- a/endpoint.go +++ b/endpoint.go @@ -133,9 +133,15 @@ func getQRCodeReport(w http.ResponseWriter, r *http.Request) { func getRoot(w http.ResponseWriter, r *http.Request) { user, err := auth.GetAuthenticatedUser(r) - if err != nil && !errors.Is(err, &auth.NoCredentialsError{}) { - respondError(w, "Failed to get root", err, http.StatusInternalServerError) - return + if err != nil { + // No credentials or user not found: go to login + if errors.Is(err, &auth.NoCredentialsError{}) || errors.Is(err, &auth.NoUserError{}) { + http.Redirect(w, r, "/signin", http.StatusFound) + return + } else { + respondError(w, "Failed to get root", err, http.StatusInternalServerError) + return + } } if user == nil { errorCode := r.URL.Query().Get("error") @@ -297,7 +303,7 @@ func postSignup(w http.ResponseWriter, r *http.Request) { return } - user, err := auth.SignupUser(username, name, password) + user, err := auth.SignupUser(r.Context(), username, name, password) if err != nil { respondError(w, "Failed to signup user", err, http.StatusInternalServerError) return