From 8ad88c4957f3e4be3b2a6aa08d0bf0c4f522e34d Mon Sep 17 00:00:00 2001 From: Amy Gale Ruth Bowersox Date: Sat, 4 Oct 2025 16:59:09 -0600 Subject: [PATCH] beginning the code for new user account --- database/emailban.go | 25 +++++++++++ database/sidebox.go | 15 +++++++ database/user.go | 91 ++++++++++++++++++++++++++++++++++++++ login.go | 55 +++++++++++++++++++++++ ui/dialog.go | 11 +++++ ui/dialogs/newaccount.yaml | 2 +- 6 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 database/emailban.go diff --git a/database/emailban.go b/database/emailban.go new file mode 100644 index 0000000..bf03779 --- /dev/null +++ b/database/emailban.go @@ -0,0 +1,25 @@ +/* + * 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 + +/* AmIsEmailAddressBanned returns true if the given E-mail address is on the "banned" list. + * Parameters: + * address - The E-mail address to be checked. + * Returns: + * true if the address is banned, false if not. + * Standard Go error status. + */ +func AmIsEmailAddressBanned(address string) (bool, error) { + rs, err := amdb.Query("SELECT by_uid FROM emailban WHERE address = ?", address) + if err != nil { + return false, err + } + return rs.Next(), nil +} diff --git a/database/sidebox.go b/database/sidebox.go index 201fa4d..c56ade8 100644 --- a/database/sidebox.go +++ b/database/sidebox.go @@ -16,6 +16,21 @@ type Sidebox struct { Param *string `db:"param"` } +// copySideboxes copies sideboxes from one user to another. +func copySideboxes(toUid int32, fromUid int32) error { + sbox := make([]Sidebox, 0, 3) + err := amdb.Select(sbox, "SELECT * from sideboxes WHERE uid = ?", fromUid) + if err == nil { + for _, sb := range sbox { + _, err := amdb.Exec("INSERT INTO sideboxes (uid, boxid, sequence, param) VALUES (?, ?, ?, ?)", toUid, sb.Boxid, sb.Sequence, sb.Param) + if err != nil { + break + } + } + } + return err +} + /* AmGetSideboxes returns all the configured sideboxes for a user. * Parameters: * uid = The ID of the user to retrieve sideboxes for. diff --git a/database/user.go b/database/user.go index f10c354..ffe4024 100644 --- a/database/user.go +++ b/database/user.go @@ -15,6 +15,7 @@ import ( "errors" "fmt" "hash/crc32" + "math/rand" "strconv" "strings" "sync" @@ -46,6 +47,13 @@ type User struct { DOB *time.Time `db:"dob"` } +// UserProperties represents a property entry for a user. +type UserProperties struct { + Uid int32 `db:"uid"` + Index int32 `db:"ndx"` + Data *string `db:"data"` +} + // userCache is the cache for User objects. var userCache *lru.TwoQueueCache = nil @@ -363,3 +371,86 @@ func AmAuthenticateUserByToken(authString string, remoteIP string) (*User, error ar = AmNewAudit(AuditLoginOK, user.Uid, remoteIP) return user, nil } + +// newEmailConfirmationNumber returns a new E-mail confirmation number. +func newEmailConfirmationNumber() int32 { + return rand.Int31n(9000000) + 1000000 +} + +/* AmCreateNewUser creates a new user record in the database. + * Parameters: + * username - New user name. + * password - New password. + * reminder - Password reminder string. + * dob - User date of birth. + * remoteIP - Remote IP address for audit record. + * Returns: + * Pointer to new user record. + * Standard Go error status. + */ +func AmCreateNewUser(username string, password string, reminder string, dob *time.Time, remoteIP string) (*User, error) { + var ar *AuditRecord = nil + defer func() { + AmStoreAudit(ar) + }() + + amdb.Exec("LOCK TABLES users WRITE, userprefs WRITE, propuser WRITE, commmember WRITE, sideboxes WRITE, confhotlist WRITE;") + defer amdb.Exec("UNLOCK TABLES;") + + // Test if the user name is already taken. + rs, err := amdb.Query("SELECT uid FROM users WHERE username = ?", username) + if err != nil { + return nil, err + } else if rs.Next() { + log.Warnf("username \"%s\" already exists", username) + return nil, errors.New("that user name already exists. Please try again") + } + + // Insert the user record. + _, err2 := amdb.Exec(`INSERT INTO users (username, passhash, verify_email, lockout, email_confnum, + base_lvl, created, lastaccess, passreminder, description, dob) VALUES (?, ?, 0, 0, ?, ?, NOW(), NOW(), ?, '', ?)`, + username, hashPassword(password), newEmailConfirmationNumber(), AmDefaultRole("Global.NewUser").Level(), reminder, *dob) + if err2 != nil { + return nil, err2 + } + // Read back the user, which also puts it in the cache. + user, err3 := AmGetUserByName(username) + if err3 != nil { + return nil, err3 + } + log.Debugf("...created new user \"%s\" with UID %d", username, user.Uid) + + // add user preferences + _, err = amdb.Exec("INSERT INTO userprefs (uid) VALUES (?)", user.Uid) + if err != nil { + return nil, err + } + + // add user properties + props := make([]UserProperties, 0) + anon, _ := getAnonUserID() + err = amdb.Select(props, "SELECT * FROM propuser WHERE uid = ?", anon) + if err != nil { + return nil, err + } + for _, p := range props { + _, err := amdb.Exec("INSTERT INTO propuser (uid, ndx, data) VALUES (?, ?, ?)", user.Uid, p.Index, p.Data) + if err != nil { + return nil, err + } + } + + // add user sideboxes + err = copySideboxes(user.Uid, anon) + if err != nil { + return nil, err + } + + // TODO: auto-join communities + + // TODO: copy conference hotlists + + // operation was a success - add an audit record + ar = AmNewAudit(AuditAccountCreated, user.Uid, remoteIP) + return user, nil +} diff --git a/login.go b/login.go index e7c3097..9bc2ff6 100644 --- a/login.go +++ b/login.go @@ -187,6 +187,14 @@ func VerifyEmailForm(ctxt ui.AmContext) (string, any, error) { return ui.ErrorPage(ctxt, err) } +/* VerifyEmail handles E-mail address verification. + * 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 VerifyEMail(ctxt ui.AmContext) (string, any, error) { // If user is not logged in, this is an error. user := ctxt.CurrentUser() @@ -300,3 +308,50 @@ func NewAccountForm(ctxt ui.AmContext) (string, any, error) { } return ui.ErrorPage(ctxt, err) } + +func NewAccount(ctxt ui.AmContext) (string, any, error) { + // If user is already logged in, this is an error. + if !ctxt.CurrentUser().IsAnon { + return ui.ErrorPage(ctxt, fmt.Errorf("you cannot create a new account while logged in on an existing one. You must log out first")) + } + + dlg, err := ui.AmLoadDialog("newaccount") + 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 == "create" { + err = dlg.Validate() + if err == nil { + if dlg.Field("pass1").Value != dlg.Field("pass2").Value { + return dlg.RenderError(ctxt, "The typed passwords do not match.") + } + var banned bool + banned, err = database.AmIsEmailAddressBanned(dlg.Field("email").Value) + if err == nil { + if banned { + return dlg.RenderError(ctxt, "This E-mail address may not register a new account.") + } + // Create new user account + var user *database.User + 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 + } + } + } + return dlg.RenderError(ctxt, err.Error()) + } + return dlg.RenderError(ctxt, "No known button click on POST to new account.") + } + return ui.ErrorPage(ctxt, err) +} diff --git a/ui/dialog.go b/ui/dialog.go index bd65126..e8e87cd 100644 --- a/ui/dialog.go +++ b/ui/dialog.go @@ -16,6 +16,7 @@ import ( "net/mail" "strconv" "strings" + "time" "git.erbosoft.com/amy/amsterdam/database" "gopkg.in/yaml.v3" @@ -108,6 +109,16 @@ func (fld *DialogItem) ValueRange() (int, int) { return -1, -1 } +// AsDate returns the value of a date field as a Go date. +func (fld *DialogItem) AsDate() *time.Time { + if fld.Type == "date" && fld.AuxData != nil { + v := fld.AuxData.([]int) + rc := time.Date(v[2], time.Month(v[0]), v[1], 0, 0, 0, 0, time.Now().Location()) + return &rc + } + return nil +} + /* Field returns a pointer to a dialog's field, given its name. * Parameters: * name - The name of the field to find. diff --git a/ui/dialogs/newaccount.yaml b/ui/dialogs/newaccount.yaml index 4a0772e..09bc833 100644 --- a/ui/dialogs/newaccount.yaml +++ b/ui/dialogs/newaccount.yaml @@ -10,7 +10,7 @@ name: "newaccount" formName: "createform" menuSelector: "top" title: "Create New Account" -action: "/TODO/newacct2" +action: "/newacct2" instructions: > To create a new account, please enter your information below. fields: