From 5794cb8a10ef89d953ce11b76dad8cf700e86efd Mon Sep 17 00:00:00 2001 From: Amy Gale Ruth Bowersox Date: Fri, 27 Feb 2026 15:59:01 -0700 Subject: [PATCH] additional work on Import User Accounts (unfinished) --- database/community.go | 6 ++++ database/user.go | 23 +++++++++++--- exports/vcard_xml.go | 2 +- exports/viu_xml.go | 72 ++++++++++++++++++++++++++++++++++++++++++- sysadmin.go | 10 +++++- 5 files changed, 106 insertions(+), 7 deletions(-) diff --git a/database/community.go b/database/community.go index df53770..57ac1c2 100644 --- a/database/community.go +++ b/database/community.go @@ -655,6 +655,9 @@ func AmGetCommunityTx(ctx context.Context, tx *sqlx.Tx, id int32) (*Community, e func AmGetCommunityByAlias(ctx context.Context, alias string) (*Community, error) { var cid int32 if err := amdb.GetContext(ctx, &cid, "SELECT commid FROM communities WHERE alias = ?", alias); err != nil { + if err == sql.ErrNoRows { + err = ErrNoCommunity + } return nil, err } return AmGetCommunity(ctx, cid) @@ -672,6 +675,9 @@ func AmGetCommunityByAlias(ctx context.Context, alias string) (*Community, error func AmGetCommunityByAliasTx(ctx context.Context, tx *sqlx.Tx, alias string) (*Community, error) { var cid int32 if err := tx.GetContext(ctx, &cid, "SELECT commid FROM communities WHERE alias = ?", alias); err != nil { + if err == sql.ErrNoRows { + err = ErrNoCommunity + } return nil, err } return AmGetCommunityTx(ctx, tx, cid) diff --git a/database/user.go b/database/user.go index e66e28c..5c385cb 100644 --- a/database/user.go +++ b/database/user.go @@ -35,6 +35,9 @@ import ( // ErrNoUser is an error returned if the user is not found in the database. var ErrNoUser error = errors.New("no such user") +// ErrUserExists is an error returned if the user name already exists when trying to create a user. +var ErrUserExists error = errors.New("that user name already exists. Please try again") + // UserPrefs represents the user's preferences in a table (one row per user). type UserPrefs struct { Uid int32 `db:"uid"` // user ID @@ -63,14 +66,15 @@ func (p *UserPrefs) Save(ctx context.Context, u, setter *User, ipaddr string) er if u != nil && u.Uid != p.Uid { return errors.New("internal mismatch of IDs") } - var old *UserPrefs + var old *UserPrefs = nil if setter.Uid != u.Uid { var pref UserPrefs err := amdb.GetContext(ctx, &pref, "SELECT * FROM userprefs WHERE uid = ?", u.Uid) - if err != nil { + if err == nil { + old = &pref + } else if err != sql.ErrNoRows { return err } - old = &pref } _, err := amdb.NamedExecContext(ctx, "UPDATE userprefs SET localeid = :localeid, tzid = :tzid WHERE uid = :uid", p) if err == nil && u != nil { @@ -444,6 +448,17 @@ func (u *User) SetSecurityData(ctx context.Context, baseLevel uint16, lockout, v return err } +// SetHashedPassword sets the hashed password for the user. Should only be used by import. +func (u *User) SetHashedPassword(ctx context.Context, hashValue string) error { + u.Mutex.Lock() + defer u.Mutex.Unlock() + _, err := amdb.ExecContext(ctx, "UPDATE users SET passhash = ? WHERE uid = ?", hashValue, u.Uid) + if err != nil { + u.Passhash = hashValue + } + return err +} + /* AmGetUser returns a reference to the specified user. * Parameters: * ctx - Standard Go context value. @@ -736,7 +751,7 @@ func AmCreateNewUser(ctx context.Context, username string, password string, remi err := tx.GetContext(ctx, &tmpuid, "SELECT uid FROM users WHERE username = ?", username) if err == nil { log.Warnf("username \"%s\" already exists", username) - return nil, errors.New("that user name already exists. Please try again") + return nil, ErrUserExists } else if err != sql.ErrNoRows { return nil, err } diff --git a/exports/vcard_xml.go b/exports/vcard_xml.go index 92b1c1c..6cd8dec 100644 --- a/exports/vcard_xml.go +++ b/exports/vcard_xml.go @@ -282,7 +282,7 @@ func VCardFromContactInfo(ctx context.Context, target *VCard, ci *database.Conta return nil } -// VCardSetContactINfo fills the ContactInfo object with data from the VCard. +// VCardSetContactInfo fills the ContactInfo object with data from the VCard. func VCardSetContactInfo(ci *database.ContactInfo, vc *VCard) { ci.GivenName = util.IIF(vc.Name.Given == "", nil, &vc.Name.Given) ci.FamilyName = util.IIF(vc.Name.Family == "", nil, &vc.Name.Family) diff --git a/exports/viu_xml.go b/exports/viu_xml.go index 7d64b6a..4208fda 100644 --- a/exports/viu_xml.go +++ b/exports/viu_xml.go @@ -198,7 +198,9 @@ func VIUStreamCommunityMemberList(ctx context.Context, w io.Writer, comm *databa return err } +// VIUCreateUser creates a new user based on the import data. func VIUCreateUser(ctx context.Context, udata *VIUUser, loader *database.User, ipaddr string) error { + // Do some initial error checking and pre-parsing. if !database.AmIsValidAmsterdamID(udata.Username) { return fmt.Errorf("the username \"%s\" is not a valid Amsterdam ID", udata.Username) } @@ -220,11 +222,32 @@ func VIUCreateUser(ctx context.Context, udata *VIUUser, loader *database.User, i if udata.Password.Prehashed { pwd = "" } + role := database.AmRole(udata.Options.Role) + if role == nil { + return fmt.Errorf("the security role \"%s\" is not found", udata.Options.Role) + } + // Create the user and set its direct information. user, err := database.AmCreateNewUser(ctx, udata.Username, pwd, udata.PasswordReminder, dob, ipaddr) if err != nil { return err } + err = user.SetProfileData(ctx, udata.PasswordReminder, dob, util.IIF(udata.Description == "", nil, &udata.Description), loader, ipaddr) + if err != nil { + return err + } + err = user.SetSecurityData(ctx, role.Level(), udata.Options.Locked, udata.Options.Confirmed, loader, ipaddr) + if err != nil { + return err + } + if udata.Password.Prehashed { + err = user.SetHashedPassword(ctx, udata.Password.Hash) + if err != nil { + return err + } + } + + // Set the contact info. ci := database.AmNewUserContactInfo(user.Uid) VCardSetContactInfo(ci, &(udata.VCard)) ci.PrivateAddr = udata.Options.HideAddr @@ -239,10 +262,57 @@ func VIUCreateUser(ctx context.Context, udata *VIUUser, loader *database.User, i if err != nil { return err } - // TODO + + // Set the user preferences and flags. + prefs := database.UserPrefs{ + Uid: user.Uid, + LocaleID: udata.Options.Locale, + TimeZoneID: udata.Options.ZoneHint, + } + err = prefs.Save(ctx, user, loader, ipaddr) + if err != nil { + return err + } + flags := util.NewOptionSet() + flags.Set(database.UserFlagDisallowSetPhoto, udata.Options.NoPhoto) + flags.Set(database.UserFlagMassMailOptOut, udata.Options.OptOut) + flags.Set(database.UserFlagPicturesInPosts, udata.Options.PostPictures) + err = user.SaveFlags(ctx, flags) + if err != nil { + return err + } + + // Autojoin base communities. + if udata.Options.AutoJoin { + err := database.AmAutoJoinCommunities(ctx, user) + if err != nil { + return err + } + } + + // Set community membership based on the joins listed. + for _, j := range udata.Joins { + role = database.AmRole(j.Role) + if role == nil { + return fmt.Errorf("the security role \"%s\" is not found", udata.Options.Role) + } + comm, err := database.AmGetCommunityByAlias(ctx, j.Community) + if err == database.ErrNoCommunity { + log.Warnf("community \"%s\" not found, skipping", j.Community) + continue + } else if err != nil { + return err + } + err = comm.SetMembership(ctx, user, role.Level(), false, loader.Uid, ipaddr) + if err != nil { + return err + } + } + return nil } +// VIUImportUserList takes a list of user accounts in VIU XML format and imports them. func VIUImportUserList(ctx context.Context, r io.Reader, loader *database.User, ipaddr string) (int, []string, error) { dec := xml.NewDecoder(r) var importData VIUBase diff --git a/sysadmin.go b/sysadmin.go index 60d2ebb..2f2c99a 100644 --- a/sysadmin.go +++ b/sysadmin.go @@ -21,6 +21,7 @@ import ( "time" "git.erbosoft.com/amy/amsterdam/database" + "git.erbosoft.com/amy/amsterdam/exports" "git.erbosoft.com/amy/amsterdam/ui" "git.erbosoft.com/amy/amsterdam/util" "github.com/CloudyKit/jet/v6" @@ -792,8 +793,15 @@ func UserImport(ctxt ui.AmContext) (string, any) { ctxt.SetFrameTitle("Import User Accounts") return "framed", "import_users.jet" } - + count, scroll, err := exports.VIUImportUserList(ctxt.Ctx(), f, ctxt.CurrentUser(), ctxt.RemoteIP()) f.Close() + if err != nil { + ctxt.VarMap().Set("errorMessage", err.Error()) + ctxt.SetFrameTitle("Import User Accounts") + return "framed", "import_users.jet" + } + _ = count + _ = scroll return "error", "Not yet implemented" }