diff --git a/database/contactinfo.go b/database/contactinfo.go index c5e8a47..cd0d9db 100644 --- a/database/contactinfo.go +++ b/database/contactinfo.go @@ -10,6 +10,7 @@ package database import ( + "errors" "fmt" "sync" "time" @@ -19,32 +20,132 @@ import ( // ContactInfo stores the contact information for a user or community. type ContactInfo struct { - ContactId int32 `db:"contactid"` - GivenName string `db:"given_name"` - FamilyName string `db:"family_name"` - MiddleInit string `db:"middle_init"` - Prefix *string `db:"prefix"` - Suffix *string `db:"suffix"` - Company *string `db:"company"` - Addr1 *string `db:"addr1"` - Addr2 *string `db:"addr2"` - Locality *string `db:"locality"` - Region *string `db:"region"` - PostalCode *string `db:"pcode"` - Country *string `db:"country"` - Phone *string `db:"phone"` - Fax *string `db:"fax"` - Mobile *string `db:"mobile"` - Email *string `db:"email"` - PrivateAddr bool `db:"pvt_addr"` - PrivatePhone bool `db:"pvt_phone"` - PrivateFax bool `db:"pvt_fax"` - PrivateEmail bool `db:"pvt_email"` - OwnerUid int32 `db:"owner_uid"` - OwnerCommId int32 `db:"owner_commid"` - PhotoURL *string `db:"photo_url"` - URL *string `db:"url"` - LastUpdate *time.Time + Mutex sync.Mutex + ContactId int32 `db:"contactid"` + GivenName string `db:"given_name"` + FamilyName string `db:"family_name"` + MiddleInit string `db:"middle_init"` + Prefix *string `db:"prefix"` + Suffix *string `db:"suffix"` + Company *string `db:"company"` + Addr1 *string `db:"addr1"` + Addr2 *string `db:"addr2"` + Locality *string `db:"locality"` + Region *string `db:"region"` + PostalCode *string `db:"pcode"` + Country *string `db:"country"` + Phone *string `db:"phone"` + Fax *string `db:"fax"` + Mobile *string `db:"mobile"` + Email *string `db:"email"` + PrivateAddr bool `db:"pvt_addr"` + PrivatePhone bool `db:"pvt_phone"` + PrivateFax bool `db:"pvt_fax"` + PrivateEmail bool `db:"pvt_email"` + OwnerUid int32 `db:"owner_uid"` + OwnerCommId int32 `db:"owner_commid"` + PhotoURL *string `db:"photo_url"` + URL *string `db:"url"` + LastUpdate *time.Time `db:"lastupdate"` +} + +// lookupCommunityContact looks up the ID of a contact for a community. +func lookupCommunityContact(id int32) (int32, error) { + var rc int32 = -1 + rs, err := amdb.Query("SELECT contactid FROM contacts WHERE onwer_commid = ?", id) + if err == nil { + if rs.Next() { + rs.Scan(&rc) + } + } + return rc, err +} + +// lookupUserContact looks up the ID of a contact for a user. +func lookupUserContact(uid int32) (int32, error) { + var rc int32 = -1 + rs, err := amdb.Query("SELECT contactid FROM contacts WHERE owner_uid = ? AND owner_commid = -1", uid) + if err == nil { + if rs.Next() { + rs.Scan(&rc) + } + } + return rc, err +} + +/* Save saves the contact info to the database. + * Returns: + * true if the E-mail address on this account has been changed, false if not. + * Standard Go error status. + */ +func (ci *ContactInfo) Save() (bool, error) { + ci.Mutex.Lock() + defer ci.Mutex.Unlock() + updateMode := false + emailChange := true + if ci.ContactId <= 0 { + var nx int32 + var err error + if ci.OwnerCommId > 0 { + nx, err = lookupCommunityContact(ci.OwnerCommId) + } else { + nx, err = lookupUserContact(ci.OwnerUid) + } + if err != nil { + return false, err + } + if nx > 0 { + ci.ContactId = nx + updateMode = true + emailChange = false + } + } else { + updateMode = true + emailChange = false + } + if !emailChange { + rs, err := amdb.Query("SELECT contactid FROM contacts WHERE contactid = ? AND email = ?", ci.ContactId, ci.Email) + if err != nil { + return false, err + } + if !rs.Next() { + emailChange = true + } + } + if updateMode { + _, err := amdb.Exec(`UPDATE contacts SET given_name = ?, family_name = ?, middle_init = ?, prefix = ?, suffix = ?, company = ?, + addr1 = ?, addr2 = ?, locality = ?, region = ?, pcode = ?, country = ?, phone = ?, fax = ?, mobile = ?, email = ?, + pvt_addr = ?, pvt_phone = ?, pvt_fax = ?, pvt_email = ?, photo_url = ?, url = ?, lastupdate = NOW() + WHERE contactid = ?`, ci.GivenName, ci.FamilyName, ci.MiddleInit, ci.Prefix, ci.Suffix, ci.Company, + ci.Addr1, ci.Addr2, ci.Locality, ci.Region, ci.PostalCode, ci.Country, ci.Phone, ci.Fax, ci.Mobile, ci.Email, + ci.PrivateAddr, ci.PrivatePhone, ci.PrivateFax, ci.PrivateEmail, ci.PhotoURL, ci.URL, ci.ContactId) + if err != nil { + return false, err + } + } else { + res, err := amdb.Exec(`INSERT INTO contacts (given_name, family_name, middle_init, prefix, suffix, company, addr1, + addr2, locality, region, pcode, country, phone, fax, mobile, email, pvt_addr, pvt_phone, pvt_fax, + pvt_email, owner_uid, owner_commid, photo_url, url, lastupdate) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())`, + ci.GivenName, ci.FamilyName, ci.MiddleInit, ci.Prefix, ci.Suffix, ci.Company, ci.Addr1, ci.Addr2, + ci.Locality, ci.Region, ci.PostalCode, ci.Country, ci.Phone, ci.Fax, ci.Mobile, ci.Email, + ci.PrivateAddr, ci.PrivatePhone, ci.PrivateFax, ci.PrivateEmail, ci.OwnerUid, ci.OwnerCommId, ci.PhotoURL, ci.URL) + if err != nil { + return false, err + } + lii, _ := res.LastInsertId() + ci.ContactId = int32(lii) + contactCache.Add(ci.ContactId, ci) + } + rs, err := amdb.Query("SELECT lastupdate FROM contacts WHERE contactid = ?", ci.ContactId) + if err != nil { + return false, err + } + if !rs.Next() { + return false, errors.New("internal error rereading update timestamp") + } + rs.Scan(&ci.LastUpdate) + return emailChange, nil } // contactCache is the cache for ContactInfo objects. @@ -82,7 +183,8 @@ func internalContactInfo(id int32) (*ContactInfo, error) { * Parameters: * id - The contact info ID top retrieve. * Returns: - * + * ContactInfo retrieved, or nil. + * Standard Go error status. */ func AmGetContactInfo(id int32) (*ContactInfo, error) { getContactMutex.Lock() @@ -100,3 +202,14 @@ func AmGetContactInfo(id int32) (*ContactInfo, error) { } return nil, err } + +/* AmNewUserContactInfo creates a new contact info record for the user. + * Parameters: + * uid - The UID of the owner of this contact info. + * Returns: + * New ContactInfo structure. + */ +func AmNewUserContactInfo(uid int32) *ContactInfo { + rc := ContactInfo{OwnerUid: uid, OwnerCommId: -1} + return &rc +} diff --git a/database/user.go b/database/user.go index ffe4024..5b2f4fb 100644 --- a/database/user.go +++ b/database/user.go @@ -136,6 +136,18 @@ func (u *User) ConfirmEMailAddress(confnum int32, remoteIP string) error { return err } +// NewEmailConfirmationNumber creates a new confirmation number for a user and saves it off. +func (u *User) NewEmailConfirmationNumber() error { + u.Mutex.Lock() + defer u.Mutex.Unlock() + newnum := newEmailConfirmationNumber() + _, err := amdb.Exec("UPDATE user SET email_confnum = ? WHERE uid = ?", newnum, u.Uid) + if err != nil { + u.EmailConfNum = newnum + } + return err +} + /* AmGetUser returns a reference to the specified user. * Parameters: * uid - The UID of the user. diff --git a/login.go b/login.go index 9bc2ff6..072db1e 100644 --- a/login.go +++ b/login.go @@ -187,6 +187,20 @@ func VerifyEmailForm(ctxt ui.AmContext) (string, any, error) { return ui.ErrorPage(ctxt, err) } +func sendEmailConfirmationEmail(user *database.User, ci *database.ContactInfo, remoteIP string) error { + if ci != nil && ci.Email != nil && *ci.Email != "" { + msg := email.AmNewEmailMessage(user.Uid, remoteIP) + msg.AddTo(*ci.Email, "") + msg.SetTemplate("verify_email.jet") + msg.AddVariable("username", user.Username) + msg.AddVariable("confnum", user.EmailConfNum) + msg.Send() + return nil + } else { + return errors.New("cannot find email address") + } +} + /* VerifyEmail handles E-mail address verification. * Parameters: * ctxt - The AmContext for the request. @@ -223,15 +237,9 @@ func VerifyEMail(ctxt ui.AmContext) (string, any, error) { var ci *database.ContactInfo ci, err = user.ContactInfo() if err == nil { - if ci != nil && ci.Email != nil && *ci.Email != "" { - msg := email.AmNewEmailMessage(user.Uid, ctxt.RemoteIP()) - msg.AddTo(*ci.Email, "") - msg.SetTemplate("verify_email.jet") - msg.AddVariable("username", user.Username) - msg.AddVariable("confnum", user.EmailConfNum) - msg.Send() - } else { - err = errors.New("cannot find email address") + err = user.NewEmailConfirmationNumber() + if err == nil { + err = sendEmailConfirmationEmail(user, ci, ctxt.RemoteIP()) } } if err == nil { @@ -309,6 +317,14 @@ func NewAccountForm(ctxt ui.AmContext) (string, any, error) { return ui.ErrorPage(ctxt, err) } +/* NewAccount handles creating a new Amsterdam account. + * Parameters: + * ctxt - The AmContext for the request. + * Returns: + * Command string dictating what to be rendered. + * Data as a parameter for the command string. + * Standard Go error status. + */ func NewAccount(ctxt ui.AmContext) (string, any, error) { // If user is already logged in, this is an error. if !ctxt.CurrentUser().IsAnon { @@ -344,8 +360,30 @@ func NewAccount(ctxt ui.AmContext) (string, any, error) { user, err = database.AmCreateNewUser(dlg.Field("user").Value, dlg.Field("pass1").Value, dlg.Field("remind").Value, dlg.Field("dob").AsDate(), ctxt.RemoteIP()) if err == nil { - // TODO: set up contact info - _ = user + // create and save contact info + ci := database.AmNewUserContactInfo(user.Uid) + ci.Prefix = dlg.Field("prefix").ValPtr() + ci.GivenName = dlg.Field("first").Value + ci.MiddleInit = dlg.Field("mid").Value + if ci.MiddleInit == "" { + ci.MiddleInit = " " + } + ci.FamilyName = dlg.Field("last").Value + ci.Suffix = dlg.Field("suffix").ValPtr() + ci.Locality = dlg.Field("loc").ValPtr() + ci.Region = dlg.Field("reg").ValPtr() + ci.PostalCode = dlg.Field("pcode").ValPtr() + ci.Country = dlg.Field("country").ValPtr() + ci.Email = dlg.Field("email").ValPtr() + _, err = ci.Save() + if err == nil { + err = sendEmailConfirmationEmail(user, ci, ctxt.RemoteIP()) + } + if err == nil { + // user is now logged in! redirect to E-mail verification + ctxt.ReplaceUser(user) + return "redirect", "/verify?tgt=" + url.PathEscape(target), nil + } } } } diff --git a/ui/dialog.go b/ui/dialog.go index e8e87cd..5b295b7 100644 --- a/ui/dialog.go +++ b/ui/dialog.go @@ -119,6 +119,14 @@ func (fld *DialogItem) AsDate() *time.Time { return nil } +// ValPtr returns the value of a field as a string pointer, or nil if the field is empty. +func (fld *DialogItem) ValPtr() *string { + if fld.Value == "" { + return nil + } + return &fld.Value +} + /* Field returns a pointer to a dialog's field, given its name. * Parameters: * name - The name of the field to find.