fully implemented profile editing
This commit is contained in:
@@ -125,6 +125,7 @@ func (ci *ContactInfo) Save() (bool, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
contactCache.Add(ci.ContactId, ci)
|
||||||
} else {
|
} else {
|
||||||
res, err := amdb.NamedExec(`INSERT INTO contacts (given_name, family_name, middle_init, prefix, suffix, company, addr1,
|
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,
|
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
|
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.
|
// contactCache is the cache for ContactInfo objects.
|
||||||
var contactCache *lru.TwoQueueCache = nil
|
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"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PasswordChangeRequest represents a temporary password change request.
|
// UserPrefs represents the user's preferences in a table (one row per user).
|
||||||
type PasswordChangeRequest struct {
|
type UserPrefs struct {
|
||||||
Uid int32
|
Uid int32 `db:"uid"`
|
||||||
Username string
|
TimeZoneID string `db:"tzid"`
|
||||||
Email string
|
LocaleID string `db:"localeid"`
|
||||||
Authentication int32
|
|
||||||
Expires time.Time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// passwordRequests contains a map of password change requests currently managed.
|
// ReadLocale reads the locale out of the prefs, adjusting for Go use.
|
||||||
var passwordRequests map[int32]*PasswordChangeRequest = make(map[int32]*PasswordChangeRequest)
|
func (p *UserPrefs) ReadLocale() string {
|
||||||
|
return strings.Replace(p.LocaleID, "_", "-", -1)
|
||||||
|
}
|
||||||
|
|
||||||
/* AmNewPasswordChangeRequest creates a new password change request and enrolls it.
|
// WriteLocale writes the locale into the prefs, adjusting for backward compatibility.
|
||||||
* Parameters:
|
func (p *UserPrefs) WriteLocale(loc string) {
|
||||||
* uid - The UID of the user.
|
p.LocaleID = strings.Replace(loc, "-", "_", -1)
|
||||||
* username - The user name of the user.
|
}
|
||||||
* email - The E-mail address of the user.
|
|
||||||
* Returns:
|
// Clone duplicates the user preferences.
|
||||||
* Pointer to the new PasswordChangeRequest.
|
func (p *UserPrefs) Clone() *UserPrefs {
|
||||||
*/
|
rc := *p
|
||||||
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
|
return &rc
|
||||||
}
|
}
|
||||||
|
|
||||||
/* AmGetPasswordChangeRequest retrieves the password change request for a UID.
|
// Save saves off the user preferences, replacing the prefs on the user if necessary.
|
||||||
* Parameters:
|
func (p *UserPrefs) Save(u *User) error {
|
||||||
* uid - The UID to retrieve the request for.
|
if u != nil && u.Uid != p.Uid {
|
||||||
* Returns:
|
return errors.New("internal mismatch of IDs")
|
||||||
* The PasswordChangeRequest pointer, or nil.
|
|
||||||
*/
|
|
||||||
func AmGetPasswordChangeRequest(uid int32) *PasswordChangeRequest {
|
|
||||||
rc := passwordRequests[uid]
|
|
||||||
if rc != nil {
|
|
||||||
delete(passwordRequests, uid)
|
|
||||||
}
|
}
|
||||||
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.
|
// User represents a user in the Amsterdam database.
|
||||||
@@ -85,6 +79,8 @@ type User struct {
|
|||||||
PassReminder string `db:"passreminder"`
|
PassReminder string `db:"passreminder"`
|
||||||
Description *string `db:"description"`
|
Description *string `db:"description"`
|
||||||
DOB *time.Time `db:"dob"`
|
DOB *time.Time `db:"dob"`
|
||||||
|
flags *util.OptionSet
|
||||||
|
prefs *UserPrefs
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserProperties represents a property entry for a user.
|
// UserProperties represents a property entry for a user.
|
||||||
@@ -94,22 +90,44 @@ type UserProperties struct {
|
|||||||
Data *string `db:"data"`
|
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.
|
// userCache is the cache for User objects.
|
||||||
var userCache *lru.TwoQueueCache = nil
|
var userCache *lru.TwoQueueCache = nil
|
||||||
|
|
||||||
// getUserMutex is a mutex on AmGetUser.
|
// getUserMutex is a mutex on AmGetUser.
|
||||||
var getUserMutex sync.Mutex
|
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.
|
// anonUid is the UID of the "anonymous" user.
|
||||||
var anonUid int32 = -1
|
var anonUid int32 = -1
|
||||||
|
|
||||||
// init initializes the user cache.
|
// init initializes the caches.
|
||||||
func init() {
|
func init() {
|
||||||
var err error
|
var err error
|
||||||
userCache, err = lru.New2Q(100)
|
userCache, err = lru.New2Q(100)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
userPropCache, err = lru.New(100)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContactInfo returns the contact info structure for the user.
|
// ContactInfo returns the contact info structure for the user.
|
||||||
@@ -219,6 +237,83 @@ func (u *User) ChangePassword(password string, remoteIP string) error {
|
|||||||
return err
|
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.
|
/* AmGetUser returns a reference to the specified user.
|
||||||
* Parameters:
|
* Parameters:
|
||||||
* uid - The UID of the user.
|
* 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)
|
ar = AmNewAudit(AuditAccountCreated, user.Uid, remoteIP)
|
||||||
return user, nil
|
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/CloudyKit/jet/v6 v6.3.1
|
||||||
github.com/alexflint/go-arg v1.6.0
|
github.com/alexflint/go-arg v1.6.0
|
||||||
github.com/biter777/countries v1.7.5
|
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/go-sql-driver/mysql v1.9.3
|
||||||
github.com/gorilla/sessions v1.4.0
|
github.com/gorilla/sessions v1.4.0
|
||||||
github.com/hashicorp/golang-lru v1.0.2
|
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/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 h1:MJ+n3+rSxWQdqVJU8eBy9RqcdH6ePPn4PJHocVWUa+Q=
|
||||||
github.com/biter777/countries v1.7.5/go.mod h1:1HSpZ526mYqKJcpT5Ti1kcGQ0L0SrXWIaptUWjFfv2E=
|
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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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.POST("/verify", ui.AmWrap(VerifyEMail))
|
||||||
e.GET("/passrecovery/:uid/:auth", ui.AmWrap(PasswordRecovery))
|
e.GET("/passrecovery/:uid/:auth", ui.AmWrap(PasswordRecovery))
|
||||||
e.GET("/profile", ui.AmWrap(EditProfileForm))
|
e.GET("/profile", ui.AmWrap(EditProfileForm))
|
||||||
|
e.POST("/profile", ui.AmWrap(EditProfile))
|
||||||
|
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|||||||
+49
-7
@@ -47,6 +47,7 @@ type Dialog struct {
|
|||||||
Action string `yaml:"action"`
|
Action string `yaml:"action"`
|
||||||
Instructions string `yaml:"instructions,omitempty"`
|
Instructions string `yaml:"instructions,omitempty"`
|
||||||
Fields []DialogItem `yaml:"fields"`
|
Fields []DialogItem `yaml:"fields"`
|
||||||
|
fldmap map[string]*DialogItem
|
||||||
}
|
}
|
||||||
|
|
||||||
// VRange is used as a return type for ValueRange.
|
// VRange is used as a return type for ValueRange.
|
||||||
@@ -68,11 +69,13 @@ func AmLoadDialog(name string) (*Dialog, error) {
|
|||||||
var d Dialog
|
var d Dialog
|
||||||
err = yaml.Unmarshal(b, &d)
|
err = yaml.Unmarshal(b, &d)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// "nil-patch" certain fields
|
// "nil-patch" certain fields and create the fast-lookup map
|
||||||
if d.MenuSelector == "" {
|
if d.MenuSelector == "" {
|
||||||
d.MenuSelector = "nochange"
|
d.MenuSelector = "nochange"
|
||||||
}
|
}
|
||||||
|
d.fldmap = make(map[string]*DialogItem)
|
||||||
for i, fld := range d.Fields {
|
for i, fld := range d.Fields {
|
||||||
|
d.fldmap[fld.Name] = &(d.Fields[i])
|
||||||
if fld.Type == "button" && fld.Param == "" {
|
if fld.Type == "button" && fld.Param == "" {
|
||||||
d.Fields[i].Param = "blue"
|
d.Fields[i].Param = "blue"
|
||||||
}
|
}
|
||||||
@@ -106,6 +109,17 @@ func (fld *DialogItem) IsChecked() bool {
|
|||||||
return false
|
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.
|
// ValueInt returns the value of the field as an integer.
|
||||||
func (fld *DialogItem) ValueInt() (int, error) {
|
func (fld *DialogItem) ValueInt() (int, error) {
|
||||||
return strconv.Atoi(fld.Value)
|
return strconv.Atoi(fld.Value)
|
||||||
@@ -134,6 +148,25 @@ func (fld *DialogItem) AsDate() *time.Time {
|
|||||||
return nil
|
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.
|
// ValPtr returns the value of a field as a string pointer, or nil if the field is empty.
|
||||||
func (fld *DialogItem) ValPtr() *string {
|
func (fld *DialogItem) ValPtr() *string {
|
||||||
if fld.Value == "" {
|
if fld.Value == "" {
|
||||||
@@ -142,6 +175,20 @@ func (fld *DialogItem) ValPtr() *string {
|
|||||||
return &fld.Value
|
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.
|
/* Field returns a pointer to a dialog's field, given its name.
|
||||||
* Parameters:
|
* Parameters:
|
||||||
* name - The name of the field to find.
|
* name - The name of the field to find.
|
||||||
@@ -149,12 +196,7 @@ func (fld *DialogItem) ValPtr() *string {
|
|||||||
* Pointer to the field, or nil.
|
* Pointer to the field, or nil.
|
||||||
*/
|
*/
|
||||||
func (d *Dialog) Field(name string) *DialogItem {
|
func (d *Dialog) Field(name string) *DialogItem {
|
||||||
for i, f := range d.Fields {
|
return d.fldmap[name]
|
||||||
if f.Name == name {
|
|
||||||
return &(d.Fields[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Render sets up the rendering parameters to send this dialog to the output.
|
/* Render sets up the rendering parameters to send this dialog to the output.
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ fields:
|
|||||||
value: ""
|
value: ""
|
||||||
- type: "header"
|
- type: "header"
|
||||||
name: "header1"
|
name: "header1"
|
||||||
caption: "Name"
|
caption: "Password"
|
||||||
subcaption: "To change your password, enter a new password into the fields below."
|
subcaption: "To change your password, enter a new password into the fields below."
|
||||||
- type: "password"
|
- type: "password"
|
||||||
name: "pass1"
|
name: "pass1"
|
||||||
|
|||||||
+135
-2
@@ -11,8 +11,11 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"git.erbosoft.com/amy/amsterdam/database"
|
||||||
"git.erbosoft.com/amy/amsterdam/ui"
|
"git.erbosoft.com/amy/amsterdam/ui"
|
||||||
|
"git.erbosoft.com/amy/amsterdam/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
/* EditProfileForm renders the Amsterdam profile editing form.
|
/* EditProfileForm renders the Amsterdam profile editing form.
|
||||||
@@ -36,8 +39,138 @@ func EditProfileForm(ctxt ui.AmContext) (string, any, error) {
|
|||||||
dlg, err := ui.AmLoadDialog("profile")
|
dlg, err := ui.AmLoadDialog("profile")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
dlg.Field("tgt").Value = target
|
dlg.Field("tgt").Value = target
|
||||||
// TODO: load fields from current user
|
var ci *database.ContactInfo
|
||||||
return dlg.Render(ctxt)
|
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)
|
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