Always include an organization for every user

This commit is contained in:
Eli Ribble 2026-01-06 15:06:16 +00:00
parent 447e52c6a0
commit 53ee020fe0
10 changed files with 85 additions and 85 deletions

View file

@ -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
}

View file

@ -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,
},

View file

@ -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 }

View file

@ -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...)

View file

@ -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)
}
})
}

View file

@ -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;

View file

@ -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
}

View file

@ -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
}

View file

@ -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"`

View file

@ -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