built out the passrecovery servlet

This commit is contained in:
2025-10-06 21:39:21 -06:00
parent fe360e23d3
commit 7db709be55
6 changed files with 157 additions and 1 deletions
+33 -1
View File
@@ -52,6 +52,20 @@ func AmNewPasswordChangeRequest(uid int32, username string, email string) *Passw
return &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
}
// User represents a user in the Amsterdam database. // User represents a user in the Amsterdam database.
type User struct { type User struct {
Mutex sync.RWMutex Mutex sync.RWMutex
@@ -167,13 +181,31 @@ func (u *User) NewEmailConfirmationNumber() error {
u.Mutex.Lock() u.Mutex.Lock()
defer u.Mutex.Unlock() defer u.Mutex.Unlock()
newnum := util.GenerateRandomConfirmationNumber() newnum := util.GenerateRandomConfirmationNumber()
_, err := amdb.Exec("UPDATE user SET email_confnum = ? WHERE uid = ?", newnum, u.Uid) _, err := amdb.Exec("UPDATE users SET email_confnum = ? WHERE uid = ?", newnum, u.Uid)
if err != nil { if err != nil {
u.EmailConfNum = newnum u.EmailConfNum = newnum
} }
return err return err
} }
// ChangePassword resets a user's password.
func (u *User) ChangePassword(password string, remoteIP string) error {
var ar *AuditRecord = nil
defer func() {
AmStoreAudit(ar)
}()
u.Mutex.Lock()
defer u.Mutex.Unlock()
pval := hashPassword(password)
_, err := amdb.Exec("UPDATE users SET passhash = ? WHERE uid = ?", pval, u.Uid)
if err == nil {
u.Passhash = pval
ar = AmNewAudit(AuditChangePassword, u.Uid, remoteIP, "via password change request")
}
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.
+43
View File
@@ -13,10 +13,12 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/url" "net/url"
"time"
"git.erbosoft.com/amy/amsterdam/database" "git.erbosoft.com/amy/amsterdam/database"
"git.erbosoft.com/amy/amsterdam/email" "git.erbosoft.com/amy/amsterdam/email"
"git.erbosoft.com/amy/amsterdam/ui" "git.erbosoft.com/amy/amsterdam/ui"
"git.erbosoft.com/amy/amsterdam/util"
"github.com/labstack/gommon/log" "github.com/labstack/gommon/log"
) )
@@ -394,3 +396,44 @@ func NewAccount(ctxt ui.AmContext) (string, any, error) {
} }
return ui.ErrorPage(ctxt, err) return ui.ErrorPage(ctxt, err)
} }
func PasswordRecovery(ctxt ui.AmContext) (string, any, error) {
var emailaddy string
uid, err := ctxt.URLParamInt("uid")
if err == nil {
auth, err := ctxt.URLParamInt("auth")
if err == nil {
pchange := database.AmGetPasswordChangeRequest(int32(uid))
if pchange == nil {
return ui.ErrorPage(ctxt, errors.New("password change request not found"))
}
if auth != int(pchange.Authentication) {
return ui.ErrorPage(ctxt, errors.New("invalid password change request"))
}
if time.Now().Compare(pchange.Expires) > 0 {
return ui.ErrorPage(ctxt, errors.New("password change request has expired"))
}
emailaddy = pchange.Email
}
}
if err == nil {
user, err := database.AmGetUser(int32(uid))
if err == nil {
newpass := util.GenerateRandomPassword()
err = user.ChangePassword(newpass, ctxt.RemoteIP())
if err == nil {
// send the password change message
msg := email.AmNewEmailMessage(user.Uid, ctxt.RemoteIP())
msg.AddTo(emailaddy, "")
msg.SetTemplate("pass_change.jet")
msg.AddVariable("username", user.Username)
msg.AddVariable("password", newpass)
msg.Send()
ctxt.VarMap().Set("amsterdam_pageTitle", "Your Password Has Been Changed")
return "framed_template", "password_changed.jet", nil
}
}
}
return ui.ErrorPage(ctxt, err)
}
+1
View File
@@ -50,6 +50,7 @@ func setupEcho() *echo.Echo {
e.GET("/newacct2", ui.AmWrap(NewAccountForm)) e.GET("/newacct2", ui.AmWrap(NewAccountForm))
e.GET("/verify", ui.AmWrap(VerifyEmailForm)) e.GET("/verify", ui.AmWrap(VerifyEmailForm))
e.POST("/verify", ui.AmWrap(VerifyEMail)) e.POST("/verify", ui.AmWrap(VerifyEMail))
e.GET("/passrecovery/:uid/:auth", ui.AmWrap(PasswordRecovery))
return e return e
} }
+14
View File
@@ -47,6 +47,8 @@ type AmContext interface {
SetRC(int) SetRC(int)
GetScratch(string) any GetScratch(string) any
SetScratch(string, any) SetScratch(string, any)
URLParam(string) string
URLParamInt(string) (int, error)
URLPath() string URLPath() string
VarMap() jet.VarMap VarMap() jet.VarMap
} }
@@ -228,6 +230,7 @@ func (c *amContext) SetRC(rc int) {
c.httprc = rc c.httprc = rc
} }
// GetScratch returns a value in the per-request scratchpad.
func (c *amContext) GetScratch(name string) any { func (c *amContext) GetScratch(name string) any {
if c.scratchpad == nil { if c.scratchpad == nil {
return nil return nil
@@ -235,6 +238,7 @@ func (c *amContext) GetScratch(name string) any {
return c.scratchpad[name] return c.scratchpad[name]
} }
// SetScratch sets a value in the per-request scratchpad.
func (c *amContext) SetScratch(name string, val any) { func (c *amContext) SetScratch(name string, val any) {
if c.scratchpad == nil { if c.scratchpad == nil {
c.scratchpad = make(map[string]any) c.scratchpad = make(map[string]any)
@@ -242,6 +246,16 @@ func (c *amContext) SetScratch(name string, val any) {
c.scratchpad[name] = val c.scratchpad[name] = val
} }
// URLParam returns the value of a URL parameter.
func (c *amContext) URLParam(name string) string {
return c.echoContext.Param(name)
}
// URLParamINt returns the value of a URL parameter parsed as an integer.
func (c *amContext) URLParamInt(name string) (int, error) {
return strconv.Atoi(c.echoContext.Param(name))
}
// URLPath returns the path component of the request URL. // URLPath returns the path component of the request URL.
func (c *amContext) URLPath() string { func (c *amContext) URLPath() string {
return c.echoContext.Request().URL.Path return c.echoContext.Request().URL.Path
+22
View File
@@ -0,0 +1,22 @@
{*
* 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/.
*}
<div class="flex">
<div class="flex-1 p-4">
<div class="mb-8">
<h1 class="text-blue-800 text-4xl font-bold mb-2">Your Password Has Been Changed</h1>
<hr class="border-2 border-gray-400 w-4/5 mb-4">
The password for your account has been changed, and a new password has been E-mailed to you at
your account's defined E-mail address. (Check your E-mail again!). After getting the new password,
please log in and change your password (yes, again!) as soon as possible.
<p class="text-black text-sm mb-4">
<a href="/">Click here</a> to return to the home page.
</p>
</div>
</div>
</div>
+44
View File
@@ -14,6 +14,7 @@ import (
crand "crypto/rand" crand "crypto/rand"
"io" "io"
mrand "math/rand" mrand "math/rand"
"strings"
) )
// authAlphabet is the set of characters from which we generate auth strings. // authAlphabet is the set of characters from which we generate auth strings.
@@ -22,6 +23,32 @@ const authAlphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuv
// authStringLen is the standard lengtth of authentication strings. // authStringLen is the standard lengtth of authentication strings.
const authStringLen = 32 const authStringLen = 32
// syllabary is used to generate random passwords.
var syllabary = [...]string{
"ba",
"be",
"bi", "bo", "bu",
"da", "de", "di", "do", "du",
"cha", "chi", "cho", "chu",
"fa", "fe", "fi", "fo", "fu",
"ga", "ge", "gi", "go", "gu",
"ha", "he", "hi", "ho", "hu",
"ja", "je", "ji", "jo", "ju",
"ka", "ke", "ki", "ko", "ku",
"la", "le", "li", "lo", "lu",
"ma", "me", "mi", "mo", "mu",
"na", "ne", "ni", "no", "nu",
"pa", "pe", "pi", "po", "pu",
"ra", "re", "ri", "ro", "ru",
"sa", "se", "si", "so", "su",
"sha", "she", "sho", "shu",
"ta", "te", "ti", "to", "tu",
"va", "ve", "vi", "vo", "vu",
"wa", "we", "wi", "wo", "wu",
"ya", "ye", "yi", "yo", "yu",
"za", "ze", "zi", "zo", "zu",
}
// GenerateRandomAuthString generates a random authentication string. // GenerateRandomAuthString generates a random authentication string.
func GenerateRandomAuthString() string { func GenerateRandomAuthString() string {
b := make([]byte, authStringLen) b := make([]byte, authStringLen)
@@ -39,3 +66,20 @@ func GenerateRandomAuthString() string {
func GenerateRandomConfirmationNumber() int32 { func GenerateRandomConfirmationNumber() int32 {
return mrand.Int31n(9000000) + 1000000 return mrand.Int31n(9000000) + 1000000
} }
// GenerateRandomPassword generates a random password string.
func GenerateRandomPassword() string {
var b strings.Builder
rd := make([]byte, 7)
if _, err := io.ReadFull(crand.Reader, rd); err != nil {
// can't happen (at least on a modern OS)
panic("failed to read random: " + err.Error())
}
for i := 0; i < 4; i++ { // add random syllables
b.WriteString(syllabary[int(rd[i])%len(syllabary)])
}
for i := 4; i < 7; i++ { // add random digits
b.WriteByte(byte('0' + int(rd[i])%10))
}
return b.String()
}