built out the passrecovery servlet
This commit is contained in:
+33
-1
@@ -52,6 +52,20 @@ func AmNewPasswordChangeRequest(uid int32, username string, email string) *Passw
|
||||
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.
|
||||
type User struct {
|
||||
Mutex sync.RWMutex
|
||||
@@ -167,13 +181,31 @@ func (u *User) NewEmailConfirmationNumber() error {
|
||||
u.Mutex.Lock()
|
||||
defer u.Mutex.Unlock()
|
||||
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 {
|
||||
u.EmailConfNum = newnum
|
||||
}
|
||||
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.
|
||||
* Parameters:
|
||||
* uid - The UID of the user.
|
||||
|
||||
@@ -13,10 +13,12 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"git.erbosoft.com/amy/amsterdam/database"
|
||||
"git.erbosoft.com/amy/amsterdam/email"
|
||||
"git.erbosoft.com/amy/amsterdam/ui"
|
||||
"git.erbosoft.com/amy/amsterdam/util"
|
||||
"github.com/labstack/gommon/log"
|
||||
)
|
||||
|
||||
@@ -394,3 +396,44 @@ func NewAccount(ctxt ui.AmContext) (string, any, error) {
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ func setupEcho() *echo.Echo {
|
||||
e.GET("/newacct2", ui.AmWrap(NewAccountForm))
|
||||
e.GET("/verify", ui.AmWrap(VerifyEmailForm))
|
||||
e.POST("/verify", ui.AmWrap(VerifyEMail))
|
||||
e.GET("/passrecovery/:uid/:auth", ui.AmWrap(PasswordRecovery))
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
@@ -47,6 +47,8 @@ type AmContext interface {
|
||||
SetRC(int)
|
||||
GetScratch(string) any
|
||||
SetScratch(string, any)
|
||||
URLParam(string) string
|
||||
URLParamInt(string) (int, error)
|
||||
URLPath() string
|
||||
VarMap() jet.VarMap
|
||||
}
|
||||
@@ -228,6 +230,7 @@ func (c *amContext) SetRC(rc int) {
|
||||
c.httprc = rc
|
||||
}
|
||||
|
||||
// GetScratch returns a value in the per-request scratchpad.
|
||||
func (c *amContext) GetScratch(name string) any {
|
||||
if c.scratchpad == nil {
|
||||
return nil
|
||||
@@ -235,6 +238,7 @@ func (c *amContext) GetScratch(name string) any {
|
||||
return c.scratchpad[name]
|
||||
}
|
||||
|
||||
// SetScratch sets a value in the per-request scratchpad.
|
||||
func (c *amContext) SetScratch(name string, val any) {
|
||||
if c.scratchpad == nil {
|
||||
c.scratchpad = make(map[string]any)
|
||||
@@ -242,6 +246,16 @@ func (c *amContext) SetScratch(name string, val any) {
|
||||
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.
|
||||
func (c *amContext) URLPath() string {
|
||||
return c.echoContext.Request().URL.Path
|
||||
|
||||
@@ -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>
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
crand "crypto/rand"
|
||||
"io"
|
||||
mrand "math/rand"
|
||||
"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.
|
||||
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.
|
||||
func GenerateRandomAuthString() string {
|
||||
b := make([]byte, authStringLen)
|
||||
@@ -39,3 +66,20 @@ func GenerateRandomAuthString() string {
|
||||
func GenerateRandomConfirmationNumber() int32 {
|
||||
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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user