dialog loading, validation, more logic in forms, IP address banning

This commit is contained in:
2025-09-26 23:45:38 -06:00
parent be56b06d7a
commit 5082e2bbc2
10 changed files with 422 additions and 11 deletions
+39
View File
@@ -13,6 +13,7 @@ package ui
import (
"bytes"
"net/http"
"strconv"
"git.erbosoft.com/amy/amsterdam/database"
"github.com/CloudyKit/jet/v6"
@@ -25,8 +26,11 @@ import (
// AmContext is the interface for Amsterdam's wrapper context that exposes the required functionality.
type AmContext interface {
CurrentUser() *database.User
FormField(string) string
FormFieldInt(string) (int, error)
RC() int
OutputType() string
Parameter(string) string
Render(string) error
SubRender(string) ([]byte, error)
Session() *sessions.Session
@@ -57,6 +61,27 @@ func (c *amContext) CurrentUser() *database.User {
return u
}
/* FormField returns the value of a form field from the request.
* Parameters:
* name - The name of the field to retrieve.
* Returns:
* The value given to that named field.
*/
func (c *amContext) FormField(name string) string {
return c.echoContext.FormValue(name)
}
/* FormField returns the value of a form field from the request, as an integer.
* Parameters:
* name - The name of the field to retrieve.
* Returns:
* The value given to that named field.
* Standard Go error status.
*/
func (c *amContext) FormFieldInt(name string) (int, error) {
return strconv.Atoi(c.echoContext.FormValue(name))
}
// RC returns the HTTP result code for the current operation.
func (c *amContext) RC() int {
return c.httprc
@@ -67,6 +92,20 @@ func (c *amContext) OutputType() string {
return c.outputType
}
/* Parameter returns the value of a parameter (query parameter or form field) from the request.
* Parameters:
* name - The name of the field to retrieve.
* Returns:
* The value given to that named field.
*/
func (c *amContext) Parameter(name string) string {
rc := c.echoContext.QueryParam(name)
if rc == "" && c.echoContext.Request().Method == "POST" {
rc = c.echoContext.FormValue(name)
}
return rc
}
/* Render renders a template to the output. Called at the top level only.
* Parameters:
* name = The name of the tempate to be rendered.
+191
View File
@@ -13,7 +13,9 @@ package ui
import (
"embed"
"fmt"
"net/mail"
"git.erbosoft.com/amy/amsterdam/database"
"gopkg.in/yaml.v3"
)
@@ -28,6 +30,7 @@ type DialogItem struct {
MaxLength int `yaml:"maxlength,omitempty"`
Value string `yaml:"value,omitempty"`
Param string `yaml:"param,omitempty"`
AuxData any
}
// Dialog holds the dialog definition.
@@ -72,6 +75,18 @@ func AmLoadDialog(name string) (*Dialog, error) {
return nil, err
}
// DateValues returns the date values stored in a date field.
func (fld *DialogItem) DateValues() []int {
if fld.Type == "date" && fld.AuxData != nil {
return fld.AuxData.([]int)
}
rc := make([]int, 3)
rc[0] = -1
rc[1] = -1
rc[2] = -1
return rc
}
/* Field returns a pointer to a dialog's field, given its name.
* Parameters:
* name - The name of the field to find.
@@ -107,3 +122,179 @@ func (d *Dialog) Render(ctxt AmContext) (string, any, error) {
ctxt.VarMap().Set("amsterdam_dialog", d)
return "framed_template", "dialog.jet", nil
}
/* LoadFromForm loads the values in a dialog from the form fields in the request.
* Parameters:
* ctxt - The AmContext for this request.
*/
func (d *Dialog) LoadFromForm(ctxt AmContext) {
for _, fld := range d.Fields {
if fld.Type == "date" {
fld.Value = ""
dvals := make([]int, 3)
var err error
dvals[0], err = ctxt.FormFieldInt(fmt.Sprintf("%s_month", fld.Name))
if err != nil {
dvals[0] = -1
fld.Value = fmt.Sprintf("!undefined month %s: %v", fld.Name, err)
}
dvals[1], err = ctxt.FormFieldInt(fmt.Sprintf("%s_day", fld.Name))
if err != nil {
dvals[1] = -1
if fld.Value == "" {
fld.Value = fmt.Sprintf("!undefined day %s: %v", fld.Name, err)
}
}
dvals[2], err = ctxt.FormFieldInt(fmt.Sprintf("%s_year", fld.Name))
if err != nil {
dvals[2] = -1
if fld.Value == "" {
fld.Value = fmt.Sprintf("!undefined year %s: %v", fld.Name, err)
}
}
if dvals[0] > 0 && dvals[1] > 0 && dvals[2] > 0 {
fld.Value = fmt.Sprintf("%04d%02d%02d", dvals[2], dvals[0], dvals[1])
} else if fld.Value == "" && fld.Required {
if dvals[0] <= 0 {
fld.Value = fmt.Sprintf("!month not set %s", fld.Name)
} else if dvals[1] <= 0 {
fld.Value = fmt.Sprintf("!day not set %s", fld.Name)
} else if dvals[2] <= 0 {
fld.Value = fmt.Sprintf("!year not set %s", fld.Name)
}
}
fld.AuxData = dvals
} else {
fld.Value = ctxt.FormField(fld.Name)
}
}
}
// validatorFunc is a function that validates the contents of a dialog item.
type validatorFunc func(*DialogItem) error
// nilValidator is a validator function that doesn't do anything.
func nilValidator(*DialogItem) error {
return nil
}
/* validateTextField validates a text field.
* Parameters:
* fld - The field to be validated.
* Returns:
* Standard Go error status.
*/
func validateTextField(fld *DialogItem) error {
if len(fld.Value) == 0 && fld.Required {
return fmt.Errorf("value of field \"%s\" is required", fld.Caption)
}
if len(fld.Value) > fld.MaxLength {
return fmt.Errorf("value of field \"%s\" can be no longer than %d characters", fld.Caption, fld.MaxLength)
}
return nil
}
/* validateAmsIdField validates an Amsterdam ID field.
* Parameters:
* fld - The field to be validated.
* Returns:
* Standard Go error status.
*/
func validateAmsIdField(fld *DialogItem) error {
err := validateTextField(fld)
if err == nil {
if !database.AmIsValidAmsterdamID(fld.Value) {
err = fmt.Errorf("value of field \"%s\" is not a valid identifier", fld.Caption)
}
}
return err
}
/* validateEmailField validates an E-mail address field.
* Parameters:
* fld - The field to be validated.
* Returns:
* Standard Go error status.
*/
func validateEmailField(fld *DialogItem) error {
err := validateTextField(fld)
if err == nil {
_, err = mail.ParseAddress(fld.Value)
}
return err
}
/* validateCountryField validates a country code field.
* Parameters:
* fld - The field to be validated.
* Returns:
* Standard Go error status.
*/
func validateCountryField(fld *DialogItem) error {
if fld.Value == "XX" && fld.Required {
return fmt.Errorf("country field \"%s\" not set", fld.Caption)
}
return nil
}
/* validateDateField validates a date field.
* Parameters:
* fld - The field to be validated.
* Returns:
* Standard Go error status.
*/
func validateDateField(fld *DialogItem) error {
if len(fld.Value) == 0 && fld.Required {
return fmt.Errorf("date value %s not set", fld.Caption)
}
if fld.Value[0] == '!' {
return fmt.Errorf("date value %s erroneous: %s", fld.Caption, fld.Value[1:])
}
if fld.AuxData == nil {
return fmt.Errorf("date value %s not set properly", fld.Caption)
}
dv := fld.AuxData.([]int)
if dv[0] > 12 || dv[1] > 31 {
return fmt.Errorf("date value %s malformed", fld.Caption)
}
q := fmt.Sprintf("%04d%02d%02d", dv[2], dv[0], dv[1])
if q != fld.Value {
return fmt.Errorf("date value %s should be %s but is %s", fld.Caption, q, fld.Value)
}
return nil
}
// validators maps the field types to validator functions.
var validators = map[string]validatorFunc{
"ams_id": validateAmsIdField,
"button": nilValidator,
"checkbox": nilValidator,
"countrylist": validateCountryField,
"date": validateDateField,
"email": validateEmailField,
"header": nilValidator,
"hidden": nilValidator,
"password": validateTextField,
"text": validateTextField,
}
/* Validate validates the values in the dialog.
* Returns:
* Standard Go error status.
*/
func (d *Dialog) Validate() error {
for _, fld := range d.Fields {
if len(fld.Value) > 0 || fld.Required {
vfunc := validators[fld.Type]
if vfunc != nil {
err := vfunc(&fld)
if err != nil {
return err
}
} else {
return fmt.Errorf("don't know how to validate field %s of type %s", fld.Name, fld.Type)
}
}
}
return nil
}
+13
View File
@@ -12,7 +12,9 @@ package ui
import (
"fmt"
"net/http"
"git.erbosoft.com/amy/amsterdam/database"
"github.com/labstack/echo/v4"
)
@@ -21,6 +23,8 @@ func sendPageData(ctxt echo.Context, amctxt AmContext, command string, data any)
switch command {
case "bytes":
err = ctxt.Blob(amctxt.RC(), amctxt.OutputType(), data.([]byte))
case "redirect":
err = ctxt.Redirect(http.StatusFound, data.(string))
case "string":
err = ctxt.String(amctxt.RC(), fmt.Sprintf("%v", data))
case "template":
@@ -64,6 +68,15 @@ func AmWrap(myfunc func(AmContext) (string, any, error)) echo.HandlerFunc {
ctxt.Logger().Errorf("Session creation error: %v", aerr)
return aerr
}
banmsg, banerr := database.AmTestIPBan(ctxt.RealIP())
if banerr != nil {
ctxt.Logger().Warnf("address %s could not be tested: %v", ctxt.RealIP(), banerr)
// but let the request pass anyway
} else if banmsg != "" {
amctxt.VarMap().Set("amsterdam_pageTitle", "IP Address Banned")
amctxt.VarMap().Set("message", banmsg)
return sendPageData(ctxt, amctxt, "framed_template", "ipban.jet")
}
what, rc, err := myfunc(amctxt)
if err == nil {
if err = amctxt.Session().Save(ctxt.Request(), ctxt.Response()); err != nil {
+1 -1
View File
@@ -16,7 +16,7 @@
</p>
<div class="flex justify-center gap-4 mt-6">
<button type="button" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded font-medium transition-colors"
onclick="window.location.assign('/newacct2')">I Accept</button>
onclick="window.location.assign('/newacct2?tgt={{ target | url }}')">I Accept</button>
<button type="button" class="bg-red-600 hover:bg-red-700 text-white px-6 py-2 rounded font-medium transition-colors"
onclick="window.location.assign('/')">I Decline</button>
</div>
+7 -6
View File
@@ -80,6 +80,7 @@
</select>
</div>
{{ else if .Type == "date" }}
{{ dv := .DateValues() }}
<div class="flex items-center">
<label class="w-64 text-right pr-4 text-black text-sm">
{{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }}
@@ -88,21 +89,21 @@
<div class="flex gap-2">
<select name="{{ .Name }}_month"
class="px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="-1" selected>---</option>
<option value="-1" {{ if dv[0] == -1 }}selected{{ end }}>---</option>
{{ range i := GetMonthList() }}
<option value="{{ i + 1 }}">{{ . }}</option>
<option value="{{ i + 1 }}" {{ if dv[0] == i + 1 }}selected{{ end }}>{{ . }}</option>
{{ end }}
</select>
<select name="{{ .Name }}_day" class="px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="-1" selected>---</option>
<option value="-1" {{ if dv[1] == -1 }}selected{{ end }}>---</option>
{{ range MakeIntRange(1, 32, 1) }}
<option value="{{ . }}">{{ . }}</option>
<option value="{{ . }}" {{ if dv[1] == . }}selected{{ end }}>{{ . }}</option>
{{ end }}
</select>
<select name="{{ .Name }}_year" class="px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="-1" selected>---</option>
<option value="-1" {{ if dv[2] == -1 }}selected{{ end }}>---</option>
{{ range MakeYearRange(.Param) }}
<option value="{{ . }}">{{ . }}</option>
<option value="{{ . }}" {{ if dv[2] == . }}selected{{ end }}>{{ . }}</option>
{{ end }}
</select>
</div>
+17
View File
@@ -0,0 +1,17 @@
{*
* 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">This IP Address Has Been Banned</h1>
<hr class="border-2 border-gray-400 w-4/5 mb-4">
<p class="text-black text-sm mb-4"><strong>{{ message }}</strong></p>
</div>
</div>
</div>