fully implemented profile editing
This commit is contained in:
@@ -125,6 +125,7 @@ func (ci *ContactInfo) Save() (bool, error) {
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
contactCache.Add(ci.ContactId, ci)
|
||||
} else {
|
||||
res, err := amdb.NamedExec(`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,
|
||||
@@ -151,6 +152,39 @@ func (ci *ContactInfo) Save() (bool, error) {
|
||||
return emailChange, nil
|
||||
}
|
||||
|
||||
// Clone makes a copy of the ContactInfo.
|
||||
func (ci *ContactInfo) Clone() *ContactInfo {
|
||||
newstr := ContactInfo{
|
||||
ContactId: ci.ContactId,
|
||||
GivenName: ci.GivenName,
|
||||
FamilyName: ci.FamilyName,
|
||||
MiddleInit: ci.MiddleInit,
|
||||
Prefix: ci.Prefix,
|
||||
Suffix: ci.Suffix,
|
||||
Company: ci.Company,
|
||||
Addr1: ci.Addr1,
|
||||
Addr2: ci.Addr2,
|
||||
Locality: ci.Locality,
|
||||
Region: ci.Region,
|
||||
PostalCode: ci.PostalCode,
|
||||
Country: ci.Country,
|
||||
Phone: ci.Phone,
|
||||
Fax: ci.Fax,
|
||||
Mobile: ci.Mobile,
|
||||
Email: ci.Mobile,
|
||||
PrivateAddr: ci.PrivateAddr,
|
||||
PrivatePhone: ci.PrivatePhone,
|
||||
PrivateFax: ci.PrivateFax,
|
||||
PrivateEmail: ci.PrivateEmail,
|
||||
OwnerUid: ci.OwnerUid,
|
||||
OwnerCommId: ci.OwnerCommId,
|
||||
PhotoURL: ci.PhotoURL,
|
||||
URL: ci.URL,
|
||||
LastUpdate: ci.LastUpdate,
|
||||
}
|
||||
return &newstr
|
||||
}
|
||||
|
||||
// contactCache is the cache for ContactInfo objects.
|
||||
var contactCache *lru.TwoQueueCache = nil
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Amsterdam Web Communities System
|
||||
* Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
// The database package contains database management and storage logic.
|
||||
package database
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.erbosoft.com/amy/amsterdam/util"
|
||||
)
|
||||
|
||||
// PasswordChangeRequest represents a temporary password change request.
|
||||
type PasswordChangeRequest struct {
|
||||
Uid int32
|
||||
Username string
|
||||
Email string
|
||||
Authentication int32
|
||||
Expires time.Time
|
||||
}
|
||||
|
||||
// passwordRequests contains a map of password change requests currently managed.
|
||||
var passwordRequests map[int32]*PasswordChangeRequest = make(map[int32]*PasswordChangeRequest)
|
||||
|
||||
/* AmNewPasswordChangeRequest creates a new password change request and enrolls it.
|
||||
* Parameters:
|
||||
* uid - The UID of the user.
|
||||
* username - The user name of the user.
|
||||
* email - The E-mail address of the user.
|
||||
* Returns:
|
||||
* Pointer to the new PasswordChangeRequest.
|
||||
*/
|
||||
func AmNewPasswordChangeRequest(uid int32, username string, email string) *PasswordChangeRequest {
|
||||
rc := PasswordChangeRequest{Uid: uid, Username: username, Email: email,
|
||||
Authentication: util.GenerateRandomConfirmationNumber(), Expires: time.Now().Add(time.Hour)}
|
||||
passwordRequests[uid] = &rc
|
||||
return &rc
|
||||
}
|
||||
|
||||
/* AmGetPasswordChangeRequest retrieves the password change request for a UID.
|
||||
* Parameters:
|
||||
* uid - The UID to retrieve the request for.
|
||||
* Returns:
|
||||
* The PasswordChangeRequest pointer, or nil.
|
||||
*/
|
||||
func AmGetPasswordChangeRequest(uid int32) *PasswordChangeRequest {
|
||||
rc := passwordRequests[uid]
|
||||
if rc != nil {
|
||||
delete(passwordRequests, uid)
|
||||
}
|
||||
return rc
|
||||
}
|
||||
+198
-33
@@ -25,45 +25,39 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// PasswordChangeRequest represents a temporary password change request.
|
||||
type PasswordChangeRequest struct {
|
||||
Uid int32
|
||||
Username string
|
||||
Email string
|
||||
Authentication int32
|
||||
Expires time.Time
|
||||
// UserPrefs represents the user's preferences in a table (one row per user).
|
||||
type UserPrefs struct {
|
||||
Uid int32 `db:"uid"`
|
||||
TimeZoneID string `db:"tzid"`
|
||||
LocaleID string `db:"localeid"`
|
||||
}
|
||||
|
||||
// passwordRequests contains a map of password change requests currently managed.
|
||||
var passwordRequests map[int32]*PasswordChangeRequest = make(map[int32]*PasswordChangeRequest)
|
||||
// ReadLocale reads the locale out of the prefs, adjusting for Go use.
|
||||
func (p *UserPrefs) ReadLocale() string {
|
||||
return strings.Replace(p.LocaleID, "_", "-", -1)
|
||||
}
|
||||
|
||||
/* AmNewPasswordChangeRequest creates a new password change request and enrolls it.
|
||||
* Parameters:
|
||||
* uid - The UID of the user.
|
||||
* username - The user name of the user.
|
||||
* email - The E-mail address of the user.
|
||||
* Returns:
|
||||
* Pointer to the new PasswordChangeRequest.
|
||||
*/
|
||||
func AmNewPasswordChangeRequest(uid int32, username string, email string) *PasswordChangeRequest {
|
||||
rc := PasswordChangeRequest{Uid: uid, Username: username, Email: email,
|
||||
Authentication: util.GenerateRandomConfirmationNumber(), Expires: time.Now().Add(time.Hour)}
|
||||
passwordRequests[uid] = &rc
|
||||
// WriteLocale writes the locale into the prefs, adjusting for backward compatibility.
|
||||
func (p *UserPrefs) WriteLocale(loc string) {
|
||||
p.LocaleID = strings.Replace(loc, "-", "_", -1)
|
||||
}
|
||||
|
||||
// Clone duplicates the user preferences.
|
||||
func (p *UserPrefs) Clone() *UserPrefs {
|
||||
rc := *p
|
||||
return &rc
|
||||
}
|
||||
|
||||
/* AmGetPasswordChangeRequest retrieves the password change request for a UID.
|
||||
* Parameters:
|
||||
* uid - The UID to retrieve the request for.
|
||||
* Returns:
|
||||
* The PasswordChangeRequest pointer, or nil.
|
||||
*/
|
||||
func AmGetPasswordChangeRequest(uid int32) *PasswordChangeRequest {
|
||||
rc := passwordRequests[uid]
|
||||
if rc != nil {
|
||||
delete(passwordRequests, uid)
|
||||
// Save saves off the user preferences, replacing the prefs on the user if necessary.
|
||||
func (p *UserPrefs) Save(u *User) error {
|
||||
if u != nil && u.Uid != p.Uid {
|
||||
return errors.New("internal mismatch of IDs")
|
||||
}
|
||||
return rc
|
||||
_, err := amdb.NamedExec("UPDATE userprefs SET localeid = :localeid, tzid = :tzid WHERE uid = :uid", p)
|
||||
if err == nil && u != nil {
|
||||
u.prefs = p
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// User represents a user in the Amsterdam database.
|
||||
@@ -85,6 +79,8 @@ type User struct {
|
||||
PassReminder string `db:"passreminder"`
|
||||
Description *string `db:"description"`
|
||||
DOB *time.Time `db:"dob"`
|
||||
flags *util.OptionSet
|
||||
prefs *UserPrefs
|
||||
}
|
||||
|
||||
// UserProperties represents a property entry for a user.
|
||||
@@ -94,22 +90,44 @@ type UserProperties struct {
|
||||
Data *string `db:"data"`
|
||||
}
|
||||
|
||||
// User property indexes defined.
|
||||
const (
|
||||
UserPropFlags = int32(0) // "flags" user property
|
||||
)
|
||||
|
||||
// Flag values for user property index UserPropFlags defined.
|
||||
const (
|
||||
UserFlagPicturesInPosts = uint(0)
|
||||
UserFlagDisallowSetPhoto = uint(1)
|
||||
UserFlagMassMailOptOut = uint(2)
|
||||
)
|
||||
|
||||
// userCache is the cache for User objects.
|
||||
var userCache *lru.TwoQueueCache = nil
|
||||
|
||||
// getUserMutex is a mutex on AmGetUser.
|
||||
var getUserMutex sync.Mutex
|
||||
|
||||
// userPropCache is the cache for UserProperties objects.
|
||||
var userPropCache *lru.Cache = nil
|
||||
|
||||
// getUserPropMutex is a mutex on AmGetUserProperty.
|
||||
var getUserPropMutex sync.Mutex
|
||||
|
||||
// anonUid is the UID of the "anonymous" user.
|
||||
var anonUid int32 = -1
|
||||
|
||||
// init initializes the user cache.
|
||||
// init initializes the caches.
|
||||
func init() {
|
||||
var err error
|
||||
userCache, err = lru.New2Q(100)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
userPropCache, err = lru.New(100)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// ContactInfo returns the contact info structure for the user.
|
||||
@@ -219,6 +237,83 @@ func (u *User) ChangePassword(password string, remoteIP string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// GetFlags retrieves the flags from the properties.
|
||||
func (u *User) Flags() (*util.OptionSet, error) {
|
||||
u.Mutex.Lock()
|
||||
defer u.Mutex.Unlock()
|
||||
if u.flags == nil {
|
||||
s, err := AmGetUserProperty(u.Uid, UserPropFlags)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s == nil {
|
||||
return nil, fmt.Errorf("missing flags for user %d", u.Uid)
|
||||
}
|
||||
u.flags = util.OptionSetFromString(*s)
|
||||
}
|
||||
return u.flags, nil
|
||||
}
|
||||
|
||||
// SaveFlags writes the flags to the database and stores them.
|
||||
func (u *User) SaveFlags(f *util.OptionSet) error {
|
||||
s := f.AsString()
|
||||
u.Mutex.Lock()
|
||||
defer u.Mutex.Unlock()
|
||||
err := AmSetUserProperty(u.Uid, UserPropFlags, &s)
|
||||
if err == nil {
|
||||
u.flags = f
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// FlagValue returns the boolean value of one of the user flags.
|
||||
func (u *User) FlagValue(ndx uint) bool {
|
||||
f, err := u.Flags()
|
||||
if err != nil {
|
||||
log.Errorf("flag retrieval error for user %d: %v", u.Uid, err)
|
||||
return false
|
||||
}
|
||||
return f.Get(ndx)
|
||||
}
|
||||
|
||||
// Prefs returns the user's preferences record.
|
||||
func (u *User) Prefs() (*UserPrefs, error) {
|
||||
u.Mutex.Lock()
|
||||
defer u.Mutex.Unlock()
|
||||
if u.prefs == nil {
|
||||
var dbdata []UserPrefs
|
||||
err := amdb.Select(&dbdata, "SELECT * FROM userprefs WHERE uid = ?", u.Uid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(dbdata) != 1 {
|
||||
return nil, fmt.Errorf("invalid preferences records for user %d", u.Uid)
|
||||
}
|
||||
u.prefs = &(dbdata[0])
|
||||
}
|
||||
return u.prefs, nil
|
||||
}
|
||||
|
||||
/* SetProfileData sets the "profile" variables for this user.
|
||||
* Parameters:
|
||||
* reminder - Password reminder string.
|
||||
* dob - Date of birth field.
|
||||
* descr - Description string.
|
||||
* Returns:
|
||||
* Standard Go error status.
|
||||
*/
|
||||
func (u *User) SetProfileData(reminder string, dob *time.Time, descr *string) error {
|
||||
u.Mutex.Lock()
|
||||
defer u.Mutex.Unlock()
|
||||
_, err := amdb.Exec("UPDATE users SET passreminder = ?, dob = ?, description = ? WHERE uid = ?", reminder, dob, descr, u.Uid)
|
||||
if err == nil {
|
||||
u.PassReminder = reminder
|
||||
u.DOB = dob
|
||||
u.Description = descr
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
/* AmGetUser returns a reference to the specified user.
|
||||
* Parameters:
|
||||
* uid - The UID of the user.
|
||||
@@ -545,3 +640,73 @@ func AmCreateNewUser(username string, password string, reminder string, dob *tim
|
||||
ar = AmNewAudit(AuditAccountCreated, user.Uid, remoteIP)
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func internalGetProp(uid int32, ndx int32) (*UserProperties, error) {
|
||||
var err error = nil
|
||||
key := fmt.Sprintf("%d:%d", uid, ndx)
|
||||
getUserPropMutex.Lock()
|
||||
defer getUserPropMutex.Unlock()
|
||||
rc, ok := userPropCache.Get(key)
|
||||
if !ok {
|
||||
var dbdata []UserProperties
|
||||
err = amdb.Select(&dbdata, "SELECT * from propuser WHERE uid = ? AND ndx = ?", uid, ndx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(dbdata) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if len(dbdata) > 1 {
|
||||
return nil, fmt.Errorf("AmGetUserProperty(%d): too many responses(%d)", uid, len(dbdata))
|
||||
}
|
||||
rc = &(dbdata[0])
|
||||
userPropCache.Add(key, rc)
|
||||
}
|
||||
return rc.(*UserProperties), nil
|
||||
}
|
||||
|
||||
/* AmGetUserProperty retrieves the value of a user property.
|
||||
* Parameters:
|
||||
* uid - The UID of the user to get the property for.
|
||||
* ndx - The index of the property to retrieve.
|
||||
* Returns:
|
||||
* Value of the property string.
|
||||
* Standard Go error status.
|
||||
*/
|
||||
func AmGetUserProperty(uid int32, ndx int32) (*string, error) {
|
||||
p, err := internalGetProp(uid, ndx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p.Data, nil
|
||||
}
|
||||
|
||||
/* AmSetUserProperty sets the value of a user property.
|
||||
* Parameters:
|
||||
* uid - The UID of the user to set the property for.
|
||||
* ndx - The index of the property to set.
|
||||
* val - The new value of the property.
|
||||
* Returns:
|
||||
* Standard Go error status.
|
||||
*/
|
||||
func AmSetUserProperty(uid int32, ndx int32, val *string) error {
|
||||
p, err := internalGetProp(uid, ndx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
getUserPropMutex.Lock()
|
||||
defer getUserPropMutex.Unlock()
|
||||
if p != nil {
|
||||
_, err = amdb.Exec("UPDATE propuser SET data = ? WHERE uid = ? AND ndx = ?", val, uid, ndx)
|
||||
if err == nil {
|
||||
p.Data = val
|
||||
}
|
||||
} else {
|
||||
prop := UserProperties{Uid: uid, Index: ndx, Data: val}
|
||||
_, err := amdb.NamedExec("INSERT INTO propuser (uid, ndx, data) VALUES(:uid, :ndx, :data)", prop)
|
||||
if err == nil {
|
||||
userPropCache.Add(fmt.Sprintf("%d:%d", uid, ndx), prop)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ require (
|
||||
github.com/CloudyKit/jet/v6 v6.3.1
|
||||
github.com/alexflint/go-arg v1.6.0
|
||||
github.com/biter777/countries v1.7.5
|
||||
github.com/bits-and-blooms/bitset v1.24.0
|
||||
github.com/go-sql-driver/mysql v1.9.3
|
||||
github.com/gorilla/sessions v1.4.0
|
||||
github.com/hashicorp/golang-lru v1.0.2
|
||||
|
||||
@@ -10,6 +10,8 @@ github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+W
|
||||
github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
|
||||
github.com/biter777/countries v1.7.5 h1:MJ+n3+rSxWQdqVJU8eBy9RqcdH6ePPn4PJHocVWUa+Q=
|
||||
github.com/biter777/countries v1.7.5/go.mod h1:1HSpZ526mYqKJcpT5Ti1kcGQ0L0SrXWIaptUWjFfv2E=
|
||||
github.com/bits-and-blooms/bitset v1.24.0 h1:H4x4TuulnokZKvHLfzVRTHJfFfnHEeSYJizujEZvmAM=
|
||||
github.com/bits-and-blooms/bitset v1.24.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
|
||||
@@ -53,6 +53,7 @@ func setupEcho() *echo.Echo {
|
||||
e.POST("/verify", ui.AmWrap(VerifyEMail))
|
||||
e.GET("/passrecovery/:uid/:auth", ui.AmWrap(PasswordRecovery))
|
||||
e.GET("/profile", ui.AmWrap(EditProfileForm))
|
||||
e.POST("/profile", ui.AmWrap(EditProfile))
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
+49
-7
@@ -47,6 +47,7 @@ type Dialog struct {
|
||||
Action string `yaml:"action"`
|
||||
Instructions string `yaml:"instructions,omitempty"`
|
||||
Fields []DialogItem `yaml:"fields"`
|
||||
fldmap map[string]*DialogItem
|
||||
}
|
||||
|
||||
// VRange is used as a return type for ValueRange.
|
||||
@@ -68,11 +69,13 @@ func AmLoadDialog(name string) (*Dialog, error) {
|
||||
var d Dialog
|
||||
err = yaml.Unmarshal(b, &d)
|
||||
if err == nil {
|
||||
// "nil-patch" certain fields
|
||||
// "nil-patch" certain fields and create the fast-lookup map
|
||||
if d.MenuSelector == "" {
|
||||
d.MenuSelector = "nochange"
|
||||
}
|
||||
d.fldmap = make(map[string]*DialogItem)
|
||||
for i, fld := range d.Fields {
|
||||
d.fldmap[fld.Name] = &(d.Fields[i])
|
||||
if fld.Type == "button" && fld.Param == "" {
|
||||
d.Fields[i].Param = "blue"
|
||||
}
|
||||
@@ -106,6 +109,17 @@ func (fld *DialogItem) IsChecked() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// SetChecked sets the value of a checkbox.
|
||||
func (fld *DialogItem) SetChecked(val bool) {
|
||||
if fld.Type == "checkbox" {
|
||||
if val {
|
||||
fld.Value = "Y"
|
||||
} else {
|
||||
fld.Value = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ValueInt returns the value of the field as an integer.
|
||||
func (fld *DialogItem) ValueInt() (int, error) {
|
||||
return strconv.Atoi(fld.Value)
|
||||
@@ -134,6 +148,25 @@ func (fld *DialogItem) AsDate() *time.Time {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetDate sets the value of the dialog item as a date.
|
||||
func (fld *DialogItem) SetDate(d *time.Time) {
|
||||
if fld.Type == "date" {
|
||||
dvs := make([]int, 3)
|
||||
if d == nil {
|
||||
dvs[0] = -1
|
||||
dvs[1] = -1
|
||||
dvs[2] = -1
|
||||
fld.Value = ""
|
||||
} else {
|
||||
dvs[0] = int(d.Month()) - int(time.January) + 1
|
||||
dvs[1] = d.Day()
|
||||
dvs[2] = d.Year()
|
||||
fld.Value = fmt.Sprintf("%04d%02d%02d", dvs[2], dvs[0], dvs[1])
|
||||
}
|
||||
fld.AuxData = dvs
|
||||
}
|
||||
}
|
||||
|
||||
// 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 == "" {
|
||||
@@ -142,6 +175,20 @@ func (fld *DialogItem) ValPtr() *string {
|
||||
return &fld.Value
|
||||
}
|
||||
|
||||
// SetVal sets the value of a field from a string pointer.
|
||||
func (fld *DialogItem) SetVal(p *string) {
|
||||
if p == nil {
|
||||
fld.Value = ""
|
||||
} else {
|
||||
fld.Value = *p
|
||||
}
|
||||
}
|
||||
|
||||
// IsEmpty returns true if the field is empty.
|
||||
func (fld *DialogItem) IsEmpty() bool {
|
||||
return len(fld.Value) == 0
|
||||
}
|
||||
|
||||
/* Field returns a pointer to a dialog's field, given its name.
|
||||
* Parameters:
|
||||
* name - The name of the field to find.
|
||||
@@ -149,12 +196,7 @@ func (fld *DialogItem) ValPtr() *string {
|
||||
* Pointer to the field, or nil.
|
||||
*/
|
||||
func (d *Dialog) Field(name string) *DialogItem {
|
||||
for i, f := range d.Fields {
|
||||
if f.Name == name {
|
||||
return &(d.Fields[i])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return d.fldmap[name]
|
||||
}
|
||||
|
||||
/* Render sets up the rendering parameters to send this dialog to the output.
|
||||
|
||||
@@ -17,7 +17,7 @@ fields:
|
||||
value: ""
|
||||
- type: "header"
|
||||
name: "header1"
|
||||
caption: "Name"
|
||||
caption: "Password"
|
||||
subcaption: "To change your password, enter a new password into the fields below."
|
||||
- type: "password"
|
||||
name: "pass1"
|
||||
|
||||
+135
-2
@@ -11,8 +11,11 @@ package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
|
||||
"git.erbosoft.com/amy/amsterdam/database"
|
||||
"git.erbosoft.com/amy/amsterdam/ui"
|
||||
"git.erbosoft.com/amy/amsterdam/util"
|
||||
)
|
||||
|
||||
/* EditProfileForm renders the Amsterdam profile editing form.
|
||||
@@ -36,8 +39,138 @@ func EditProfileForm(ctxt ui.AmContext) (string, any, error) {
|
||||
dlg, err := ui.AmLoadDialog("profile")
|
||||
if err == nil {
|
||||
dlg.Field("tgt").Value = target
|
||||
// TODO: load fields from current user
|
||||
return dlg.Render(ctxt)
|
||||
var ci *database.ContactInfo
|
||||
ci, err = u.ContactInfo()
|
||||
if err == nil {
|
||||
var prefs *database.UserPrefs
|
||||
prefs, err = u.Prefs()
|
||||
if err == nil {
|
||||
dlg.Field("remind").Value = u.PassReminder
|
||||
dlg.Field("prefix").SetVal(ci.Prefix)
|
||||
dlg.Field("first").Value = ci.GivenName
|
||||
dlg.Field("mid").SetVal(ci.MiddleInit)
|
||||
dlg.Field("last").Value = ci.FamilyName
|
||||
dlg.Field("suffix").SetVal(ci.Suffix)
|
||||
dlg.Field("company").SetVal(ci.Company)
|
||||
dlg.Field("addr1").SetVal(ci.Addr1)
|
||||
dlg.Field("addr2").SetVal(ci.Addr2)
|
||||
dlg.Field("pvt_addr").SetChecked(ci.PrivateAddr)
|
||||
dlg.Field("loc").SetVal(ci.Locality)
|
||||
dlg.Field("reg").SetVal(ci.Region)
|
||||
dlg.Field("pcode").SetVal(ci.PostalCode)
|
||||
dlg.Field("country").SetVal(ci.Country)
|
||||
dlg.Field("phone").SetVal(ci.Phone)
|
||||
dlg.Field("mobile").SetVal(ci.Mobile)
|
||||
dlg.Field("pvt_phone").SetChecked(ci.PrivatePhone)
|
||||
dlg.Field("fax").SetVal(ci.Fax)
|
||||
dlg.Field("pvt_fax").SetChecked(ci.PrivateFax)
|
||||
dlg.Field("email").SetVal(ci.Email)
|
||||
dlg.Field("pvt_email").SetChecked(ci.PrivateEmail)
|
||||
dlg.Field("url").SetVal(ci.URL)
|
||||
dlg.Field("dob").SetDate(u.DOB)
|
||||
dlg.Field("descr").SetVal(u.Description)
|
||||
// TODO: do something for user photo
|
||||
dlg.Field("pic_in_post").SetChecked(u.FlagValue(database.UserFlagPicturesInPosts))
|
||||
dlg.Field("no_mass_mail").SetChecked(u.FlagValue(database.UserFlagMassMailOptOut))
|
||||
dlg.Field("locale").Value = prefs.ReadLocale()
|
||||
dlg.Field("tz").Value = prefs.TimeZoneID
|
||||
return dlg.Render(ctxt)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ui.ErrorPage(ctxt, err)
|
||||
}
|
||||
|
||||
func EditProfile(ctxt ui.AmContext) (string, any, error) {
|
||||
u := ctxt.CurrentUser()
|
||||
if u.IsAnon {
|
||||
return ui.ErrorPage(ctxt, errors.New("you are not logged in"))
|
||||
}
|
||||
dlg, err := ui.AmLoadDialog("profile")
|
||||
if err == nil {
|
||||
dlg.LoadFromForm(ctxt)
|
||||
target := dlg.Field("tgt").Value
|
||||
if target == "" {
|
||||
target = "/"
|
||||
}
|
||||
|
||||
action := dlg.WhichButton(ctxt)
|
||||
if action == "cancel" { // Cancel button pressed
|
||||
return "redirect", target, nil
|
||||
}
|
||||
if action == "update" {
|
||||
var ci *database.ContactInfo
|
||||
ci, err = u.ContactInfo()
|
||||
if err == nil {
|
||||
var prefs *database.UserPrefs
|
||||
emailChange := false
|
||||
prefs, err = u.Prefs()
|
||||
if err == nil && !(dlg.Field("pass1").IsEmpty() && dlg.Field("pass2").IsEmpty()) {
|
||||
p1 := dlg.Field("pass1").Value
|
||||
if p1 == dlg.Field("pass2").Value {
|
||||
err = u.ChangePassword(p1, ctxt.RemoteIP())
|
||||
} else {
|
||||
err = errors.New("passwords do not match")
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
nci := ci.Clone()
|
||||
nci.Prefix = dlg.Field("prefix").ValPtr()
|
||||
nci.GivenName = dlg.Field("first").Value
|
||||
nci.MiddleInit = dlg.Field("mid").ValPtr()
|
||||
nci.FamilyName = dlg.Field("last").Value
|
||||
nci.Suffix = dlg.Field("suffix").ValPtr()
|
||||
nci.Company = dlg.Field("company").ValPtr()
|
||||
nci.Addr1 = dlg.Field("addr1").ValPtr()
|
||||
nci.Addr2 = dlg.Field("addr2").ValPtr()
|
||||
nci.PrivateAddr = dlg.Field("pvt_addr").IsChecked()
|
||||
nci.Locality = dlg.Field("loc").ValPtr()
|
||||
nci.Region = dlg.Field("reg").ValPtr()
|
||||
nci.PostalCode = dlg.Field("pcode").ValPtr()
|
||||
nci.Country = dlg.Field("country").ValPtr()
|
||||
nci.Phone = dlg.Field("phone").ValPtr()
|
||||
nci.Mobile = dlg.Field("mobile").ValPtr()
|
||||
nci.PrivatePhone = dlg.Field("pvt_phone").IsChecked()
|
||||
nci.Fax = dlg.Field("fax").ValPtr()
|
||||
nci.PrivateFax = dlg.Field("pvt_fax").IsChecked()
|
||||
nci.Email = dlg.Field("email").ValPtr()
|
||||
nci.PrivateEmail = dlg.Field("pvt_email").IsChecked()
|
||||
nci.URL = dlg.Field("url").ValPtr()
|
||||
emailChange, err = nci.Save()
|
||||
ci = nci
|
||||
}
|
||||
if err == nil {
|
||||
nprefs := prefs.Clone()
|
||||
nprefs.WriteLocale(dlg.Field("locale").Value)
|
||||
nprefs.TimeZoneID = dlg.Field("tz").Value
|
||||
err = nprefs.Save(u)
|
||||
}
|
||||
if err == nil {
|
||||
var f *util.OptionSet
|
||||
f, err = u.Flags()
|
||||
if err == nil {
|
||||
nf := f.Clone()
|
||||
nf.Set(database.UserFlagPicturesInPosts, dlg.Field("pic_in_post").IsChecked())
|
||||
nf.Set(database.UserFlagMassMailOptOut, dlg.Field("no_mass_mail").IsChecked())
|
||||
err = u.SaveFlags(nf)
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
err = u.SetProfileData(dlg.Field("remind").Value, dlg.Field("dob").AsDate(), dlg.Field("descr").ValPtr())
|
||||
}
|
||||
if err == nil {
|
||||
if emailChange {
|
||||
err = sendEmailConfirmationEmail(u, ci, ctxt.RemoteIP())
|
||||
if err == nil {
|
||||
return "redirect", "/verify?tgt=" + url.PathEscape(target), nil
|
||||
}
|
||||
} else {
|
||||
return "redirect", target, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return dlg.RenderError(ctxt, "No known button click on POST to profile.")
|
||||
}
|
||||
return ui.ErrorPage(ctxt, err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Amsterdam Web Communities System
|
||||
* Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
// Package util contains utility definitions.
|
||||
package util
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/bits-and-blooms/bitset"
|
||||
)
|
||||
|
||||
// optionAlphabet is the alphabet from which OptionSets serialize to and from strings.
|
||||
const optionAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%&()*+,-./:;<=>?@[]^_`{|}~"
|
||||
|
||||
// OptionSet is a bit set that can be persisted as a specially-constructed string.
|
||||
type OptionSet struct {
|
||||
bits *bitset.BitSet
|
||||
}
|
||||
|
||||
// Get retrieves the value of a bit from the given set.
|
||||
func (s *OptionSet) Get(ndx uint) bool {
|
||||
return s.bits.Test(ndx)
|
||||
}
|
||||
|
||||
// Set sets the value of a bit in the given set.
|
||||
func (s *OptionSet) Set(ndx uint, v bool) {
|
||||
if v {
|
||||
s.bits = s.bits.Set(ndx)
|
||||
} else {
|
||||
s.bits = s.bits.Clear(ndx)
|
||||
}
|
||||
}
|
||||
|
||||
// AsString returns the option set's value as a string.
|
||||
func (s *OptionSet) AsString() string {
|
||||
var b strings.Builder
|
||||
for i, e := s.bits.NextSet(0); e; i, e = s.bits.NextSet(i + 1) {
|
||||
b.WriteByte(optionAlphabet[int(i)])
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// Clone creates a clone of this OptionSet.
|
||||
func (s *OptionSet) Clone() *OptionSet {
|
||||
return &OptionSet{bits: s.bits.Clone()}
|
||||
}
|
||||
|
||||
// NewOptionSet creates and returns an empty option set.
|
||||
func NewOptionSet() *OptionSet {
|
||||
return &OptionSet{bits: bitset.New(uint(len(optionAlphabet)))}
|
||||
}
|
||||
|
||||
// OptionSetFromString converts a string into a corresponding OptionSet.
|
||||
func OptionSetFromString(s string) *OptionSet {
|
||||
bs := bitset.New(uint(len(optionAlphabet)))
|
||||
for _, ch := range s {
|
||||
bs = bs.Set(uint(strings.IndexRune(optionAlphabet, ch)))
|
||||
}
|
||||
return &OptionSet{bits: bs}
|
||||
}
|
||||
Reference in New Issue
Block a user