added the E-mail address validation form
This commit is contained in:
@@ -92,6 +92,42 @@ func (u *User) NewAuthToken() (string, error) {
|
||||
return fmt.Sprintf("AQAT:%d|%s|%d|", u.Uid, newToken, checkValue), nil
|
||||
}
|
||||
|
||||
/* ConfirmEMailAddress checks the E-mail confirmation number and sets "verified" status if it's OK.
|
||||
* Parameters:
|
||||
* confnum - The entered confirmation number.
|
||||
* remoteIP - The remote IP address for audit messages.
|
||||
* Returns:
|
||||
* Standard Go error status.
|
||||
*/
|
||||
func (u *User) ConfirmEMailAddress(confnum int32, remoteIP string) error {
|
||||
var ar *AuditRecord = nil
|
||||
defer func() {
|
||||
AmStoreAudit(ar)
|
||||
}()
|
||||
|
||||
log.Debugf("ConfirmEMailAddress for UID %d", u.Uid)
|
||||
u.Mutex.Lock()
|
||||
defer u.Mutex.Unlock()
|
||||
if u.VerifyEMail || AmTestPermission("Global.NoEmailVerify", u.BaseLevel) {
|
||||
log.Debug("...user has either already confirmed or is exempt")
|
||||
return nil
|
||||
}
|
||||
if confnum != u.EmailConfNum {
|
||||
log.Warn("...confirmation number incorrect")
|
||||
ar = AmNewAudit(AuditVerifyEmailFail, u.Uid, remoteIP, "Invalid confirmation number")
|
||||
return errors.New("confirmation number is incorrect. Please try again")
|
||||
}
|
||||
_, err := amdb.Exec("UPDATE users SET verify_email = 1, base_lvl = ? WHERE uid = ?",
|
||||
AmDefaultRole("Global.AfterVerify").Level(), u.Uid)
|
||||
if err == nil {
|
||||
u.VerifyEMail = true
|
||||
u.BaseLevel = AmDefaultRole("Global.AfterVerify").Level()
|
||||
// TODO: auto-join communities if necessary
|
||||
ar = AmNewAudit(AuditVerifyEmailOK, u.Uid, remoteIP)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
/* AmGetUser returns a reference to the specified user.
|
||||
* Parameters:
|
||||
* uid - The UID of the user.
|
||||
|
||||
@@ -12,6 +12,7 @@ package main
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"git.erbosoft.com/amy/amsterdam/database"
|
||||
"git.erbosoft.com/amy/amsterdam/email"
|
||||
@@ -118,8 +119,11 @@ func Login(ctxt ui.AmContext) (string, any, error) {
|
||||
log.Errorf("unable to generate auth string for uid %d: %v", user.Uid, cerr)
|
||||
}
|
||||
}
|
||||
// TODO: bounce to E-mail verify if we can do so
|
||||
return "redirect", target, nil
|
||||
if user.VerifyEMail {
|
||||
return "redirect", target, nil
|
||||
} else {
|
||||
return "redirect", "/verify?tgt=" + url.PathEscape(target), nil
|
||||
}
|
||||
}
|
||||
dlg.Field("pass").Value = ""
|
||||
return dlg.RenderError(ctxt, "No known button click on POST to login function.")
|
||||
@@ -149,6 +153,100 @@ func Logout(ctxt ui.AmContext) (string, any, error) {
|
||||
return "redirect", target, nil
|
||||
}
|
||||
|
||||
/* VerifyEmailForm renders the E-mail address verification form.
|
||||
* 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 VerifyEmailForm(ctxt ui.AmContext) (string, any, error) {
|
||||
// Get target URI.
|
||||
target := ctxt.Parameter("tgt")
|
||||
if target == "" {
|
||||
target = "/"
|
||||
}
|
||||
|
||||
// If user is not logged in, this is an error.
|
||||
user := ctxt.CurrentUser()
|
||||
if !user.IsAnon {
|
||||
return ui.ErrorPage(ctxt, errors.New("you must log in before you can verify your account's E-mail address"))
|
||||
}
|
||||
|
||||
// If user is already verified, this is a no-op.
|
||||
if user.VerifyEMail {
|
||||
return "redirect", target, nil
|
||||
}
|
||||
|
||||
dlg, err := ui.AmLoadDialog("verify_email")
|
||||
if err == nil {
|
||||
dlg.Field("tgt").Value = target
|
||||
return dlg.Render(ctxt)
|
||||
}
|
||||
return ui.ErrorPage(ctxt, err)
|
||||
}
|
||||
|
||||
func VerifyEMail(ctxt ui.AmContext) (string, any, error) {
|
||||
// If user is not logged in, this is an error.
|
||||
user := ctxt.CurrentUser()
|
||||
if !user.IsAnon {
|
||||
return ui.ErrorPage(ctxt, errors.New("you must log in before you can verify your account's E-mail address"))
|
||||
}
|
||||
|
||||
dlg, err := ui.AmLoadDialog("verify_email")
|
||||
if err == nil {
|
||||
dlg.LoadFromForm(ctxt)
|
||||
target := dlg.Field("tgt").Value
|
||||
if target == "" {
|
||||
target = "/"
|
||||
}
|
||||
|
||||
// If user is already verified, this is a no-op.
|
||||
if user.VerifyEMail {
|
||||
return "redirect", target, nil
|
||||
}
|
||||
|
||||
action := dlg.WhichButton(ctxt)
|
||||
if action == "cancel" { // Cancel button pressed
|
||||
return "redirect", target, nil
|
||||
}
|
||||
if action == "sendagain" {
|
||||
var ci *database.ContactInfo
|
||||
ci, err = user.ContactInfo()
|
||||
if err == nil {
|
||||
if ci != nil && ci.Email != nil && *ci.Email != "" {
|
||||
msg := email.AmNewEmailMessage(user.Uid, ctxt.RemoteIP())
|
||||
msg.AddTo(*ci.Email, "")
|
||||
msg.SetTemplate("verify_email.jet")
|
||||
msg.AddVariable("username", user.Username)
|
||||
msg.AddVariable("confnum", user.EmailConfNum)
|
||||
msg.Send()
|
||||
} else {
|
||||
err = errors.New("cannot find email address")
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
return dlg.RenderInfo(ctxt, "Verification message has been sent to your E-mail address.")
|
||||
} else {
|
||||
return dlg.RenderError(ctxt, err.Error())
|
||||
}
|
||||
}
|
||||
if action == "ok" {
|
||||
err = dlg.Validate()
|
||||
if err == nil {
|
||||
err = user.ConfirmEMailAddress(dlg.Field("num").AuxData.(int32), ctxt.RemoteIP())
|
||||
if err == nil {
|
||||
return "redirect", target, nil
|
||||
}
|
||||
}
|
||||
return dlg.RenderError(ctxt, err.Error())
|
||||
}
|
||||
return dlg.RenderError(ctxt, "No known button click on POST to verify function.")
|
||||
}
|
||||
return ui.ErrorPage(ctxt, err)
|
||||
}
|
||||
|
||||
/* NewAccountUserAgreement renders the Amsterdam user agreement for new accounts.
|
||||
* Parameters:
|
||||
* ctxt - The AmContext for the request.
|
||||
@@ -166,7 +264,7 @@ func NewAccountUserAgreement(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"))
|
||||
return ui.ErrorPage(ctxt, errors.New("you cannot create a new account while logged in on an existing one. You must log out first"))
|
||||
}
|
||||
|
||||
ctxt.VarMap().Set("target", target)
|
||||
|
||||
@@ -48,6 +48,8 @@ func setupEcho() *echo.Echo {
|
||||
e.GET("/logout", ui.AmWrap(Logout))
|
||||
e.GET("/newacct", ui.AmWrap(NewAccountUserAgreement))
|
||||
e.GET("/newacct2", ui.AmWrap(NewAccountForm))
|
||||
e.GET("/verify", ui.AmWrap(VerifyEmailForm))
|
||||
e.POST("/verify", ui.AmWrap(VerifyEMail))
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.erbosoft.com/amy/amsterdam/database"
|
||||
"gopkg.in/yaml.v3"
|
||||
@@ -95,6 +97,17 @@ func (fld *DialogItem) IsChecked() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// ValueRange returns the minimum and maximum values for an integer field.
|
||||
func (fld *DialogItem) ValueRange() (int, int) {
|
||||
if fld.Type == "integer" && fld.Param != "" {
|
||||
parms := strings.Split(fld.Param, "-")
|
||||
low, _ := strconv.Atoi(parms[0])
|
||||
high, _ := strconv.Atoi(parms[1])
|
||||
return low, high
|
||||
}
|
||||
return -1, -1
|
||||
}
|
||||
|
||||
/* Field returns a pointer to a dialog's field, given its name.
|
||||
* Parameters:
|
||||
* name - The name of the field to find.
|
||||
@@ -259,6 +272,34 @@ func validateTextField(fld *DialogItem) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
/* validateIntegerField validates an integer field.
|
||||
* Parameters:
|
||||
* fld - The field to be validated.
|
||||
* Returns:
|
||||
* Standard Go error status.
|
||||
*/
|
||||
func validateIntegerField(fld *DialogItem) error {
|
||||
err := validateTextField(fld)
|
||||
if err == nil {
|
||||
var v int
|
||||
v, err = strconv.Atoi(fld.Value)
|
||||
if err == nil {
|
||||
fld.AuxData = v // cache parsed value
|
||||
lo, hi := fld.ValueRange()
|
||||
if lo != -1 && hi != -1 {
|
||||
if v < lo {
|
||||
return fmt.Errorf("value of field \"%s\" cannot be less than %d", fld.Caption, lo)
|
||||
} else if v > hi {
|
||||
return fmt.Errorf("value of field \"%s\" cannot be greater than %d", fld.Caption, hi)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("value of field \"%s\" is not a valid integer", fld.Caption)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/* validateAmsIdField validates an Amsterdam ID field.
|
||||
* Parameters:
|
||||
* fld - The field to be validated.
|
||||
@@ -339,6 +380,7 @@ var validators = map[string]validatorFunc{
|
||||
"email": validateEmailField,
|
||||
"header": nilValidator,
|
||||
"hidden": nilValidator,
|
||||
"integer": validateIntegerField,
|
||||
"password": validateTextField,
|
||||
"text": validateTextField,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
#
|
||||
# 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/.
|
||||
#
|
||||
name: "verify"
|
||||
formName: "verifyform"
|
||||
menuSelector: "top"
|
||||
title: "Verify E-mail Address"
|
||||
action: "/verify"
|
||||
instructions: >
|
||||
Check your E-mail, then enter the confirmation number that was E-mailed to you in the field below.
|
||||
(The E-mail might take a few minutes or so to get to you, so be patient.) Once you do so, your account
|
||||
will be fully validated. If you have not received your confirmation within a few minutes, click on the
|
||||
<b>Send Again</b> button below.
|
||||
fields:
|
||||
- type: "hidden"
|
||||
name: "tgt"
|
||||
value: ""
|
||||
- type: "integer"
|
||||
name: "num"
|
||||
caption: "Confirmation Number"
|
||||
size: 7
|
||||
param: "1000000-9999999"
|
||||
- type: "button"
|
||||
name: "ok"
|
||||
caption: "OK"
|
||||
param: "blue"
|
||||
- type: "button"
|
||||
name: "sendagain"
|
||||
caption: "Send Again"
|
||||
param: "gray"
|
||||
- type: "button"
|
||||
name: "cancel"
|
||||
caption: "Cancel"
|
||||
param: "red"
|
||||
@@ -13,6 +13,7 @@ package ui
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"git.erbosoft.com/amy/amsterdam/config"
|
||||
"git.erbosoft.com/amy/amsterdam/database"
|
||||
@@ -99,6 +100,11 @@ func AmWrap(myfunc func(AmContext) (string, any, error)) echo.HandlerFunc {
|
||||
} else {
|
||||
log.Warnf("unable to rotate login cookie: %v", cerr)
|
||||
}
|
||||
if !user.VerifyEMail {
|
||||
// bounce to E-mail verification before we go anywhere
|
||||
return sendPageData(ctxt, amctxt, "redirect",
|
||||
"/verify?tgt="+url.PathEscape(ctxt.Request().URL.Path))
|
||||
}
|
||||
} else {
|
||||
log.Errorf("login cookie bogus, do not use: %v", cerr)
|
||||
amctxt.ClearLoginCookie()
|
||||
|
||||
@@ -67,6 +67,22 @@
|
||||
value="{{ .Value }}"
|
||||
class="flex-1 px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" />
|
||||
</div>
|
||||
{{ else if .Type == "integer" }}
|
||||
<div class="flex items-center">
|
||||
<label for="{{ .Name }}" class="w-64 text-right pr-4 text-black text-sm">
|
||||
{{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }}:
|
||||
{{ if .Required }}<span class="text-red-600">*</span>{{ end }}
|
||||
</label>
|
||||
<input type="text" id="{{ .Name }}" name="{{ .Name }}"
|
||||
{{ if .Size > 0 }}size="{{ .Size }}"{{ end }}
|
||||
{{ if .MaxLength > 0 }}maxlength="{{ .MaxLength }}"{{ end }}
|
||||
value="{{ .Value }}"
|
||||
class="flex-1 px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" />
|
||||
{{ lo, hi := .ValueRange() }}
|
||||
{{ if lo != -1 && hi != -1 }}
|
||||
<span class="text-sm">({{ lo }}-{{ hi }})</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ else if .Type == "password" }}
|
||||
<div class="flex items-center">
|
||||
<label for="{{ .Name }}" class="w-64 text-right pr-4 text-black text-sm">
|
||||
|
||||
Reference in New Issue
Block a user